-
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 9 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,160 @@ | ||
| 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. `--kind choices` reads a JSON document | ||
| // shaped {"prompt"?, "multi"?, "options": [{"id", "label", "detail"?}]}. | ||
| const KINDS = new Set<RequestedKind>(["auto", "markdown", "code", "diff", "csv", "json", "html", "choices"]); | ||
| 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); | ||
| const config = 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); | ||
| markdownUrl = createFragmentUrl(DEFAULT_VIEWER_URL, encoded.transportFragmentBody); | ||
|
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. WARNING: The markdown/discord URL surface is never budget-checked
Reply with |
||
| } | ||
|
|
||
| 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 ..."); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { mkdir, readFile, writeFile } from "node:fs/promises"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
|
|
||
| export type ConfigKey = "INSTANCE_URL" | "TOKEN"; | ||
|
|
||
| export type StoredConfig = { | ||
| instanceUrl?: string; | ||
| token?: string; | ||
| }; | ||
|
|
||
| export type ResolvedConfig = StoredConfig & { | ||
| configPath: string; | ||
| }; | ||
|
|
||
| /** The env shape these helpers read: a bag of optional string vars, not the framework-augmented ProcessEnv. */ | ||
| export type EnvLookup = Readonly<Record<string, string | undefined>>; | ||
|
|
||
| function normalizeConfigKey(key: string): ConfigKey { | ||
| const normalized = key.replace(/[-_]/g, "").toLowerCase(); | ||
| if (normalized === "instanceurl") return "INSTANCE_URL"; | ||
| if (normalized === "token") return "TOKEN"; | ||
| throw new Error(`Unknown config key "${key}". Expected INSTANCE_URL or TOKEN.`); | ||
| } | ||
|
|
||
| /** Resolves the XDG-compatible agent-render config file path. */ | ||
| export function getConfigPath(env: EnvLookup = process.env): string { | ||
| const configHome = env.XDG_CONFIG_HOME?.trim(); | ||
| const home = env.HOME?.trim() || os.homedir(); | ||
| return path.join(configHome || path.join(home, ".config"), "agent-render", "config.json"); | ||
| } | ||
|
|
||
| /** Reads stored CLI configuration, treating a missing file as empty configuration. */ | ||
| export async function readStoredConfig(env: EnvLookup = process.env): Promise<StoredConfig> { | ||
| const configPath = getConfigPath(env); | ||
| let contents: string; | ||
| try { | ||
| contents = await readFile(configPath, "utf8"); | ||
| } catch (error) { | ||
| if ((error as NodeJS.ErrnoException).code === "ENOENT") return {}; | ||
| throw error; | ||
| } | ||
|
|
||
| const parsed: unknown = JSON.parse(contents); | ||
|
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. WARNING: A malformed config file breaks every
Reply with |
||
| if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { | ||
| throw new Error(`Config file ${configPath} must contain a JSON object.`); | ||
| } | ||
|
|
||
| const record = parsed as Record<string, unknown>; | ||
| return { | ||
| instanceUrl: typeof record.instanceUrl === "string" ? record.instanceUrl : undefined, | ||
| token: typeof record.token === "string" ? record.token : undefined, | ||
| }; | ||
| } | ||
|
|
||
| /** Resolves CLI configuration with flags taking precedence over environment and file values. */ | ||
| export async function resolveConfig( | ||
| flags: StoredConfig = {}, | ||
| env: EnvLookup = process.env, | ||
| ): Promise<ResolvedConfig> { | ||
| const stored = await readStoredConfig(env); | ||
| return { | ||
| instanceUrl: flags.instanceUrl ?? env.AGENT_RENDER_INSTANCE_URL ?? stored.instanceUrl, | ||
| token: flags.token ?? env.AGENT_RENDER_TOKEN ?? stored.token, | ||
| configPath: getConfigPath(env), | ||
| }; | ||
| } | ||
|
|
||
| /** Stores one supported CLI configuration value. */ | ||
| export async function setConfigValue( | ||
| keyInput: string, | ||
| value: string, | ||
| env: EnvLookup = process.env, | ||
| ): Promise<string> { | ||
| const key = normalizeConfigKey(keyInput); | ||
| const configPath = getConfigPath(env); | ||
| const config = await readStoredConfig(env); | ||
| if (key === "INSTANCE_URL") config.instanceUrl = value; | ||
| else config.token = value; | ||
|
|
||
| await mkdir(path.dirname(configPath), { recursive: true }); | ||
| await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); | ||
|
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. SUGGESTION:
Reply with |
||
| return configPath; | ||
| } | ||
|
|
||
| /** Reads one supported value directly from the config file. */ | ||
| export async function getConfigValue( | ||
| keyInput: string, | ||
| env: EnvLookup = process.env, | ||
| ): Promise<string | undefined> { | ||
| const key = normalizeConfigKey(keyInput); | ||
| const config = await readStoredConfig(env); | ||
| return key === "INSTANCE_URL" ? config.instanceUrl : config.token; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SUGGESTION: The intro now advertises the new kinds, but the lists below were not updated
The Status section (line 31: "Markdown, code, diff, CSV, and JSON all render in the static shell") and the Included Renderers section still document only the five original kinds — no
htmlorchoicesentries.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.