-
Notifications
You must be signed in to change notification settings - Fork 0
Coding-agent surfaces: kit HTML and choices kinds, CLI, self-hosted hardening, token bench #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 26 commits
18b3b32
2061858
0cb68b4
c55be58
621386f
6700f05
7b48ca0
289c54c
8a6e055
b6214b9
fc6e3bf
bd0189e
0dcfee2
de10986
5d64c29
b293a73
2b42171
0505f46
4d5d9ad
1249900
4501eed
81b9fc0
f40b696
3025aa3
2340ef4
4b15d35
17c4056
e0c5f47
358ecb2
c352fed
aa2d73b
321ee50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { build } from "esbuild"; | ||
| import { chmod, copyFile, rm } from "node:fs/promises"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| await rm("dist", { recursive: true, force: true }); | ||
|
|
||
| await build({ | ||
| entryPoints: ["src/index.ts"], | ||
| outfile: "dist/index.cjs", | ||
| bundle: true, | ||
| platform: "node", | ||
| format: "cjs", | ||
| target: "node20", | ||
| sourcemap: true, | ||
| banner: { js: "#!/usr/bin/env node" }, | ||
| loader: { ".wasm": "file" }, | ||
| alias: { | ||
| "@": fileURLToPath(new URL("../src", import.meta.url)), | ||
| "brotli-wasm": fileURLToPath(new URL("../node_modules/brotli-wasm/index.node.js", import.meta.url)), | ||
| }, | ||
| }); | ||
|
|
||
| await copyFile( | ||
| fileURLToPath(new URL("../node_modules/brotli-wasm/pkg.node/brotli_wasm_bg.wasm", import.meta.url)), | ||
| "dist/brotli_wasm_bg.wasm", | ||
| ); | ||
| await chmod("dist/index.cjs", 0o755); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "name": "@agent-render/cli", | ||
| "version": "0.1.0", | ||
| "description": "Create agent-render artifact links from the command line.", | ||
| "license": "MIT", | ||
| "type": "module", | ||
| "bin": { | ||
| "agent-render": "./dist/index.cjs" | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "engines": { | ||
| "node": ">=20" | ||
| }, | ||
| "scripts": { | ||
| "build": "node build.mjs", | ||
| "prepack": "npm run build", | ||
| "test": "vitest run", | ||
| "typecheck": "tsc --noEmit" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^26.0.0", | ||
| "esbuild": "^0.27.3", | ||
| "typescript": "^5.8.2", | ||
| "vitest": "^4.1.9" | ||
| } | ||
|
Comment on lines
+13
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: As of July 31, 2026, the current latest Node.js Active LTS version is v24.18.1 (codenamed Krypton) [1][2][3]. The Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Package files:"
git ls-files | grep -E '(^|/)package\.json$|config\.ts$|tsconfig\.json$|vite\.config|vitest\.config' | sed 's#^\./##' | head -200
echo
echo "cli/package.json engines and deps/scripts:"
python3 - <<'PY'
import json, pathlib
p=pathlib.Path("cli/package.json")
data=json.loads(p.read_text())
for k in ["engines","scripts","devDependencies","dependencies","type"]:
print(f"{k}: {data.get(k)}")
PY
echo
echo "TypeScript config / Node version settings:"
for f in cli/tsconfig.json cli/tsconfig.*.json cli/vite.config.ts cli/vitest.config.ts; do
if [ -f "$f" ]; then
echo "--- $f"
cat -n "$f"
fi
done
echo
echo "Search for ts-node target/library/version hints:"
rg -n '"target"|"lib"|"types"|"moduleTarget"|"nodeVersion"|"typesVersions"|"`@types/node`"|engines\.node|nodeVersion' -S cli package.json pnpm-workspace.yaml package-lock.json pnpm-lock.yaml 2>/dev/null | head -300Repository: baanish/agent-render Length of output: 2489 🌐 Web query:
💡 Result: To configure your TypeScript project for Node.js 20 or 22, the following guidelines apply regarding Citations:
Align
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| import { readFile } from "node:fs/promises"; | ||
| import path from "node:path"; | ||
| import { stdin as defaultStdin } from "node:process"; | ||
| import { getConfigValue, resolveConfig, setConfigValue } from "./config"; | ||
| import { buildPayloadEnvelope, type ArtifactInput } from "./envelope"; | ||
| import { assertEnvelopeWithinBudget, assertFragmentBudget, createFragmentUrl, encodePayloadEnvelope } from "./encoding"; | ||
| import { formatArtifactOutput, type OutputFormat } from "./format"; | ||
| import { createInstanceArtifact } from "./instance"; | ||
| import type { RequestedKind } from "./kind"; | ||
|
|
||
| type Mode = "auto" | "instance" | "fragment"; | ||
|
|
||
| type CreateOptions = { | ||
| files: string[]; | ||
| kind: RequestedKind; | ||
| title?: string; | ||
| mode: Mode; | ||
| format: OutputFormat; | ||
| stdin: boolean; | ||
| json: boolean; | ||
| instanceUrl?: string; | ||
| token?: string; | ||
| }; | ||
|
|
||
| // `.html` files auto-detect as code (source view); kit rendering is an explicit `--kind html` so | ||
| // arbitrary HTML files are never silently reinterpreted. | ||
| const KINDS = new Set<RequestedKind>(["auto", "markdown", "code", "diff", "csv", "json", "html"]); | ||
| const MODES = new Set<Mode>(["auto", "instance", "fragment"]); | ||
| const FORMATS = new Set<OutputFormat>(["url", "markdown", "discord", "slack", "plain"]); | ||
| const DEFAULT_VIEWER_URL = "https://agent-render.com/"; | ||
|
|
||
| function requireOptionValue(args: string[], index: number, option: string): string { | ||
| const value = args[index + 1]; | ||
| if (!value || value.startsWith("--")) throw new Error(`${option} requires a value.`); | ||
| return value; | ||
| } | ||
|
|
||
| function parseChoice<T extends string>(value: string, choices: Set<T>, option: string): T { | ||
| if (!choices.has(value as T)) { | ||
| throw new Error(`Invalid ${option} value "${value}". Expected one of: ${[...choices].join(", ")}.`); | ||
| } | ||
| return value as T; | ||
| } | ||
|
|
||
| function parseCreateOptions(args: string[]): CreateOptions { | ||
| const options: CreateOptions = { | ||
| files: [], | ||
| kind: "auto", | ||
| mode: "auto", | ||
| format: "url", | ||
| stdin: false, | ||
| json: false, | ||
| }; | ||
|
|
||
| for (let index = 0; index < args.length; index += 1) { | ||
| const arg = args[index]!; | ||
| if (!arg.startsWith("--")) { | ||
| options.files.push(arg); | ||
| continue; | ||
| } | ||
| if (arg === "--stdin") options.stdin = true; | ||
| else if (arg === "--json") options.json = true; | ||
| else if (arg === "--kind") options.kind = parseChoice(requireOptionValue(args, index++, arg), KINDS, arg); | ||
| else if (arg === "--title") options.title = requireOptionValue(args, index++, arg); | ||
| else if (arg === "--mode") options.mode = parseChoice(requireOptionValue(args, index++, arg), MODES, arg); | ||
| else if (arg === "--format") options.format = parseChoice(requireOptionValue(args, index++, arg), FORMATS, arg); | ||
| else if (arg === "--instance-url") options.instanceUrl = requireOptionValue(args, index++, arg); | ||
| else if (arg === "--token") options.token = requireOptionValue(args, index++, arg); | ||
| else throw new Error(`Unknown option "${arg}".`); | ||
| } | ||
|
|
||
| if (options.stdin && options.files.length > 0) throw new Error("--stdin cannot be combined with file paths."); | ||
| if (!options.stdin && options.files.length === 0) throw new Error("Provide at least one file or use --stdin."); | ||
| if (options.stdin && options.kind === "auto") throw new Error("--kind is required with --stdin."); | ||
| return options; | ||
| } | ||
|
|
||
| async function readStdin(): Promise<string> { | ||
| const chunks: Buffer[] = []; | ||
| for await (const chunk of defaultStdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); | ||
| return Buffer.concat(chunks).toString("utf8"); | ||
| } | ||
|
|
||
| async function readInputs(options: CreateOptions): Promise<ArtifactInput[]> { | ||
| if (options.stdin) { | ||
| return [{ filename: options.title?.trim() || "stdin", content: await readStdin() }]; | ||
| } | ||
| return Promise.all(options.files.map(async (filename) => ({ | ||
| filename, | ||
| content: await readFile(filename, "utf8"), | ||
| }))); | ||
| } | ||
|
|
||
| function getOutputLabel(options: CreateOptions, inputs: ArtifactInput[]): string { | ||
| return options.title?.trim() || (inputs.length === 1 ? path.basename(inputs[0]!.filename) : `${inputs.length} artifacts`); | ||
| } | ||
|
|
||
| async function runCreate(args: string[]): Promise<void> { | ||
| const options = parseCreateOptions(args); | ||
| const inputs = await readInputs(options); | ||
| const envelope = buildPayloadEnvelope(inputs, options.kind, options.title); | ||
| assertEnvelopeWithinBudget(envelope); | ||
| // Explicit fragment mode needs no instance settings, so a malformed config file must not fail a | ||
| // purely local encode. | ||
| const config = options.mode === "fragment" | ||
| ? { instanceUrl: undefined, token: undefined } | ||
| : await resolveConfig({ instanceUrl: options.instanceUrl, token: options.token }); | ||
| const mode: Exclude<Mode, "auto"> = options.mode === "auto" | ||
| ? (config.instanceUrl ? "instance" : "fragment") | ||
| : options.mode; | ||
| const label = getOutputLabel(options, inputs); | ||
| let url: string; | ||
| let markdownUrl: string; | ||
|
|
||
| if (mode === "instance") { | ||
| if (!config.instanceUrl) throw new Error("Instance mode requires INSTANCE_URL configuration."); | ||
| url = await createInstanceArtifact(envelope, config.instanceUrl, config.token); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Instance mode sends the encoded envelope without checking the decoded JSON budget. A highly compressible input over 200,000 characters can therefore be accepted by the server's 500,000-character encoded-payload limit and return a UUID successfully, but AGENTS.md reference: AGENTS.md:L91-L92 Useful? React with 👍 / 👎. |
||
| markdownUrl = url; | ||
| } else { | ||
| const encoded = await encodePayloadEnvelope(envelope); | ||
| assertFragmentBudget(encoded.fragmentBody); | ||
| url = createFragmentUrl(DEFAULT_VIEWER_URL, encoded.fragmentBody); | ||
| // The markdown surface is a different candidate (selected by percent-escaped length) with its | ||
| // own visible length, so it needs its own budget check — but only when the chosen format | ||
| // actually emits it, or an oversized markdown candidate would fail a --format url run whose own | ||
| // link is well within budget. | ||
| const usesMarkdownSurface = options.format === "markdown" || options.format === "discord"; | ||
| if (encoded.transportFragmentBody === encoded.fragmentBody) { | ||
| markdownUrl = url; | ||
| } else { | ||
| if (usesMarkdownSurface) assertFragmentBudget(encoded.transportFragmentBody); | ||
| markdownUrl = createFragmentUrl(DEFAULT_VIEWER_URL, encoded.transportFragmentBody); | ||
| } | ||
| } | ||
|
|
||
| const formatted = formatArtifactOutput(options.format, label, url, markdownUrl); | ||
| if (formatted.warning) process.stderr.write(`${formatted.warning}\n`); | ||
| if (options.json) { | ||
| const bytes = inputs.reduce((total, input) => total + Buffer.byteLength(input.content), 0); | ||
| process.stdout.write(`${JSON.stringify({ url, mode, bytes, warning: formatted.warning })}\n`); | ||
| } else { | ||
| process.stdout.write(`${formatted.text}\n`); | ||
| } | ||
| } | ||
|
|
||
| async function runConfig(args: string[]): Promise<void> { | ||
| const [operation, key, value, ...rest] = args; | ||
| if (rest.length > 0 || !operation || !key) { | ||
| throw new Error("Usage: agent-render config set KEY VALUE | agent-render config get KEY"); | ||
| } | ||
| if (operation === "set") { | ||
| if (value === undefined) throw new Error("Usage: agent-render config set KEY VALUE"); | ||
| const configPath = await setConfigValue(key, value); | ||
| process.stderr.write(`Updated ${configPath}\n`); | ||
| return; | ||
| } | ||
| if (operation === "get") { | ||
| if (value !== undefined) throw new Error("Usage: agent-render config get KEY"); | ||
| const stored = await getConfigValue(key); | ||
| if (stored === undefined) throw new Error(`Config key "${key}" is not set.`); | ||
| process.stdout.write(`${stored}\n`); | ||
| return; | ||
| } | ||
| throw new Error(`Unknown config operation "${operation}". Expected set or get.`); | ||
| } | ||
|
|
||
| /** Runs the agent-render command-line interface. */ | ||
| export async function runCli(args: string[]): Promise<void> { | ||
| const [command, ...rest] = args; | ||
| if (command === "create") return runCreate(rest); | ||
| if (command === "config") return runConfig(rest); | ||
| throw new Error("Usage: agent-render create [files...] [options] | agent-render config set|get ..."); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.