Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
18b3b32
viewer: kit html and choices artifact kinds
baanish Jul 31, 2026
2061858
selfhosted: 7-day default TTL, env override, optional password gate
baanish Jul 31, 2026
0cb68b4
cli: @agent-render/cli workspace with create command
baanish Jul 31, 2026
c55be58
bench: measure fragment codecs in tokens, not chars
baanish Jul 31, 2026
621386f
integration: cli html/choices kinds, workspace lockfile, bench report…
baanish Jul 31, 2026
6700f05
build: root typecheck excludes cli workspace and runs its own tsc
baanish Jul 31, 2026
7b48ca0
selfhosted: derive auth key via scrypt, gate Secure cookie on TLS
baanish Jul 31, 2026
289c54c
harden kit html and self-hosted auth after review
baanish Jul 31, 2026
8a6e055
bound choices payloads, guard CLI size, fix review doc/CI gaps
baanish Jul 31, 2026
b6214b9
fix trust-drop race and sanitizer stack overflow
baanish Jul 31, 2026
fc6e3bf
selfhosted: fail fast on unusable config, decouple cookie from password
baanish Jul 31, 2026
bd0189e
enforce choices caps at encode time, harden CLI edge cases
baanish Jul 31, 2026
0dcfee2
align docs with the shipped behavior
baanish Jul 31, 2026
de10986
ignore local planning docs
baanish Jul 31, 2026
5d64c29
keep tuple codecs away from html payloads
baanish Jul 31, 2026
b293a73
replace the choices artifact kind with a kit component
baanish Jul 31, 2026
2b42171
correct the homepage kind count and pin the ar-choices contract
baanish Jul 31, 2026
0505f46
fix the ar-choices example to match its own options
baanish Jul 31, 2026
4d5d9ad
tighten the trusted sandbox, href parsing, and forwarded-proto trust
baanish Jul 31, 2026
1249900
rate-limit failed auth attempts before the KDF
baanish Jul 31, 2026
4501eed
regenerate the skill discovery digests and guard them
baanish Jul 31, 2026
81b9fc0
stop the CLI sending one instance's token to another host
baanish Jul 31, 2026
f40b696
fail closed on an empty password, and stop caching authenticated resp…
baanish Jul 31, 2026
3025aa3
render the ar-choices detail line under its label
baanish Jul 31, 2026
2340ef4
key auth rate limiting per client, not per proxy
baanish Jul 31, 2026
4b15d35
serve trusted HTML from an isolated frame with its own CSP
baanish Jul 31, 2026
17c4056
actually stop the CLI leaking a token across instances
baanish Jul 31, 2026
e0c5f47
close the image beacon channel in the frame CSP
baanish Aug 1, 2026
358ecb2
bind stored tokens to their stored endpoint, decode input strictly
baanish Aug 1, 2026
c352fed
count in-flight derivations against the auth rate limit
baanish Aug 1, 2026
aa2d73b
stop claiming the isolation frame prevents exfiltration
baanish Aug 1, 2026
321ee50
bind every layer's token to the endpoint it names
baanish Aug 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ jobs:

- name: Build
run: npm run build

- name: CLI workspace checks
run: |
npm run typecheck --workspace @agent-render/cli
npm run test --workspace @agent-render/cli
npm run build --workspace @agent-render/cli
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,11 @@ final-light*.png

# Self-hosted server runtime database (local default ./data/agent-render.db; see selfhosted/db.ts)
/data/*.db*

# Token bench esbuild bridge. Lives in the repo (not os.tmpdir()) so the bundle's external
# brotli-wasm specifier still resolves through node_modules; cleaned up on exit, ignored in case
# the run is killed. See scripts/bench-tokens.mjs.
/.token-bench-*/

# Local planning docs; not part of the published repo
/docs/design/
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

`agent-render` is a fully static, zero-retention artifact viewer for AI-generated outputs.

Built for the OpenClaw ecosystem, `agent-render` focuses on fragment-based sharing for markdown, code, diffs, CSV, and JSON so the payload stays in the browser URL fragment instead of being sent to a server.
Built for the OpenClaw ecosystem, `agent-render` focuses on fragment-based sharing for markdown, code, diffs, CSV, JSON, and kit HTML dashboards so the payload stays in the browser URL fragment instead of being sent to a server.

## OpenClaw

Expand All @@ -28,7 +28,7 @@ Built for the OpenClaw ecosystem, `agent-render` focuses on fragment-based shari

## Status

- Markdown, code, diff, CSV, and JSON all render in the static shell
- Markdown, code, diff, CSV, JSON, and kit HTML all render in the static shell
- Fragment transport supports `plain`, `lz`, `deflate`, `arx`, `arx2`, `arx3`, and `arx4`, with automatic shortest-fragment selection across available wire formats
- The `arx` substitution dictionary is served at `/arx-dictionary.json` with a pre-compressed `/arx-dictionary.json.br` variant; the `arx2` tuple-envelope overlay is served at `/arx2-dictionary.json` with a pre-compressed `/arx2-dictionary.json.br` variant; `arx3` reuses those proven bytes and optimizes for compact visible Unicode fragments; `arx4` adds the curated context-mixer priors at `/arx4-priors.json` with a pre-compressed `/arx4-priors.json.br` variant
- The viewer toolbar copies artifact bodies to the clipboard, downloads them as files, and (for markdown) supports browser print-to-PDF
Expand All @@ -41,6 +41,7 @@ Built for the OpenClaw ecosystem, `agent-render` focuses on fragment-based shari
- `diff` - review-style multi-file git patch viewer with unified and split modes
- `csv` - parsed table view with sticky headers and horizontal overflow handling
- `json` - lightweight read-only tree view plus native raw source view, with graceful malformed JSON fallback
- `html` - rich layout built from the shipped design kit; sanitized for fragment links, sandboxed iframe for server-injected payloads

## Principles

Expand Down Expand Up @@ -69,7 +70,7 @@ In addition to the default static/fragment-based product, `agent-render` include
**What it provides:**
- REST API for creating, reading, updating, and deleting artifacts
- UUID-based viewer links that render the same UI as fragment links
- 24-hour sliding TTL with automatic expiry
- 7-day sliding TTL (configurable) with automatic expiry
- SQLite storage — no external database required
- Docker Compose and daemon deployment options

Expand Down
27 changes: 27 additions & 0 deletions cli/build.mjs
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);
28 changes: 28 additions & 0 deletions cli/package.json
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the current latest Node.js LTS version and what @types/node major version tracks it?

💡 Result:

As of July 31, 2026, the current latest Node.js Active LTS version is v24.18.1 (codenamed Krypton) [1][2][3]. The @types/node package typically tracks the major version of Node.js LTS releases [4]. As of the most recent updates in late July 2026, the @types/node package is at version 26.1.2 [5][6]. While @types/node versions often align with Node.js major versions, the DefinitelyTyped repository maintains separate type definitions (e.g., v24, v22) to accommodate the different LTS versions actively in use [7]. Users should install the version of @types/node that corresponds to the specific major version of Node.js they are using in their project [7].

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 -300

Repository: baanish/agent-render

Length of output: 2489


🌐 Web query:

@types/node major version Node 20 TypeScript target node22 lib set node20 support

💡 Result:

To configure your TypeScript project for Node.js 20 or 22, the following guidelines apply regarding @types/node versions, target settings, and library configurations. @types/node Versioning The major version of @types/node is intended to track the major version of Node.js itself [1][2]. For a project running on Node 20 or Node 22, you should install the corresponding major version of @types/node (e.g., @types/node@20 or @types/node@22) to ensure your type definitions match the APIs available in your target runtime [3][1]. TypeScript Target and Lib Settings According to the official TypeScript Node Target Mapping, the recommended compiler options for both Node 20 and Node 22 are identical [4][5]: { "compilerOptions": { "lib": ["ES2023"], "module": "nodenext", "target": "ES2023" } } Key Considerations: 1. Target/Lib: Using ES2023 as both the target and the lib provides a baseline that covers the ECMAScript features supported by these modern Node.js versions [4][5]. 2. Module Resolution: Setting module to nodenext is currently the standard recommendation to ensure correct module resolution for both CommonJS and ESM in modern Node environments [4][5]. 3. Future Updates: While some community-maintained @tsconfig/bases packages may differ slightly in their specific configurations, the Microsoft TypeScript wiki serves as the authoritative source for these mappings [4][6][5]. Note that TypeScript continues to evolve its Node support; watch for future minor updates to these configurations as new TypeScript versions are released [4][5][7].

Citations:


Align @types/node with the supported Node baseline.

engines.node allows Node >=20, while @types/node is ^26.0.0. This makes tsc --noEmit valid against Node 26 APIs that newer Node LTSs add, so code that only runs in cli/src/**/*.ts or cli/tests/**/*.ts can type-check on Node 20-23 as supported but fail at runtime. Pin @types/node closer to the minimum supported Node version, or raise engines.node to the actual supported minimum.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/package.json` around lines 13 - 27, Align the Node type definitions with
the supported runtime in the package.json engines and devDependencies entries:
replace `@types/node` ^26.0.0 with a version matching the Node 20 baseline, unless
the project intentionally raises engines.node to that actual minimum. Keep the
resulting TypeScript API surface consistent with the declared runtime support.

}
168 changes: 168 additions & 0 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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);
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject oversized envelopes before uploading from the CLI

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 decodeFragmentAsync(..., { skipFragmentBudget: true }) still enforces the decoded limit, so the resulting viewer link only displays decoded-too-large. Validate the serialized envelope against the decoded budget before POSTing.

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), so it
// carries its own visible length and needs its own budget check: the viewer enforces the visible
// budget on whatever fragment it opens, so an unchecked link here could render "too-large" for a
// payload whose --format url link works.
if (encoded.transportFragmentBody === encoded.fragmentBody) {
markdownUrl = url;
} else {
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 ...");
}
104 changes: 104 additions & 0 deletions cli/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { chmod, 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;
}

let parsed: unknown;
try {
parsed = JSON.parse(contents);
} catch {
// Name the file: config is read on every create, including fragment mode that needs none, so a
// bare SyntaxError here would fail an unrelated command with no way to find the cause.
throw new Error(`Config file ${configPath} is not valid JSON.`);
}
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: mode: 0o600 does not tighten permissions on a pre-existing config file

fs.writeFile's mode option only applies when the file is created. If config.json already exists with loose permissions (hand-created per the docs, or written by another tool), agent-render config set TOKEN … writes the bearer token into it and the file stays e.g. 0644 — silently defeating the hardening this line intends. Add an explicit chmod(configPath, 0o600) after the write.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// writeFile's mode only applies when it creates the file, so an existing world-readable config
// would keep its permissions while now holding a bearer token.
await chmod(configPath, 0o600);
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;
}
Loading
Loading