@usezombie/zombiectl 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +76 -0
  2. package/bin/zombiectl.js +11 -0
  3. package/bun.lock +29 -0
  4. package/package.json +28 -0
  5. package/scripts/run-tests.mjs +38 -0
  6. package/src/cli.js +275 -0
  7. package/src/commands/admin.js +39 -0
  8. package/src/commands/agent.js +98 -0
  9. package/src/commands/agent_harness.js +43 -0
  10. package/src/commands/agent_improvement_report.js +42 -0
  11. package/src/commands/agent_profile.js +39 -0
  12. package/src/commands/agent_proposals.js +158 -0
  13. package/src/commands/agent_scores.js +44 -0
  14. package/src/commands/core-ops.js +108 -0
  15. package/src/commands/core.js +537 -0
  16. package/src/commands/harness.js +35 -0
  17. package/src/commands/harness_activate.js +53 -0
  18. package/src/commands/harness_active.js +32 -0
  19. package/src/commands/harness_compile.js +40 -0
  20. package/src/commands/harness_source.js +72 -0
  21. package/src/commands/run_preview.js +212 -0
  22. package/src/commands/run_preview_walk.js +1 -0
  23. package/src/commands/runs.js +35 -0
  24. package/src/commands/spec_init.js +287 -0
  25. package/src/commands/workspace_billing.js +26 -0
  26. package/src/constants/error-codes.js +1 -0
  27. package/src/lib/agent-loop.js +106 -0
  28. package/src/lib/analytics.js +114 -0
  29. package/src/lib/api-paths.js +2 -0
  30. package/src/lib/browser.js +96 -0
  31. package/src/lib/http.js +149 -0
  32. package/src/lib/sse-parser.js +50 -0
  33. package/src/lib/state.js +67 -0
  34. package/src/lib/tool-executors.js +110 -0
  35. package/src/lib/walk-dir.js +41 -0
  36. package/src/program/args.js +95 -0
  37. package/src/program/auth-guard.js +12 -0
  38. package/src/program/auth-token.js +44 -0
  39. package/src/program/banner.js +46 -0
  40. package/src/program/command-registry.js +17 -0
  41. package/src/program/http-client.js +38 -0
  42. package/src/program/io.js +83 -0
  43. package/src/program/routes.js +20 -0
  44. package/src/program/suggest.js +76 -0
  45. package/src/program/validate.js +24 -0
  46. package/src/ui-progress.js +59 -0
  47. package/src/ui-theme.js +62 -0
  48. package/test/admin_config.unit.test.js +25 -0
  49. package/test/agent-loop.unit.test.js +497 -0
  50. package/test/agent_harness.unit.test.js +52 -0
  51. package/test/agent_improvement_report.unit.test.js +74 -0
  52. package/test/agent_profile.unit.test.js +156 -0
  53. package/test/agent_proposals.unit.test.js +167 -0
  54. package/test/agent_scores.unit.test.js +220 -0
  55. package/test/analytics.unit.test.js +41 -0
  56. package/test/args.unit.test.js +69 -0
  57. package/test/auth-guard.test.js +33 -0
  58. package/test/auth-token.unit.test.js +112 -0
  59. package/test/banner.unit.test.js +442 -0
  60. package/test/browser.unit.test.js +16 -0
  61. package/test/cli-analytics.unit.test.js +296 -0
  62. package/test/did-you-mean.integration.test.js +76 -0
  63. package/test/doctor-json.test.js +81 -0
  64. package/test/error-codes.unit.test.js +7 -0
  65. package/test/harness-command.unit.test.js +180 -0
  66. package/test/harness-compile.test.js +81 -0
  67. package/test/harness-lifecycle.integration.test.js +339 -0
  68. package/test/harness-source-put.test.js +72 -0
  69. package/test/harness_activate.unit.test.js +48 -0
  70. package/test/harness_active.unit.test.js +53 -0
  71. package/test/harness_compile.unit.test.js +54 -0
  72. package/test/harness_source.unit.test.js +59 -0
  73. package/test/help.test.js +276 -0
  74. package/test/helpers-fs.js +32 -0
  75. package/test/helpers.js +31 -0
  76. package/test/io.unit.test.js +57 -0
  77. package/test/login.unit.test.js +115 -0
  78. package/test/logout.unit.test.js +65 -0
  79. package/test/parse.test.js +16 -0
  80. package/test/run-preview.edge.test.js +422 -0
  81. package/test/run-preview.integration.test.js +135 -0
  82. package/test/run-preview.security.test.js +246 -0
  83. package/test/run-preview.unit.test.js +131 -0
  84. package/test/run.unit.test.js +149 -0
  85. package/test/runs-cancel.unit.test.js +288 -0
  86. package/test/runs-list.unit.test.js +105 -0
  87. package/test/skill-secret.unit.test.js +94 -0
  88. package/test/spec-init.edge.test.js +232 -0
  89. package/test/spec-init.integration.test.js +128 -0
  90. package/test/spec-init.security.test.js +285 -0
  91. package/test/spec-init.unit.test.js +160 -0
  92. package/test/specs-sync.unit.test.js +164 -0
  93. package/test/sse-parser.unit.test.js +54 -0
  94. package/test/state.unit.test.js +34 -0
  95. package/test/streamfetch.unit.test.js +211 -0
  96. package/test/suggest.test.js +75 -0
  97. package/test/tool-executors.unit.test.js +165 -0
  98. package/test/validate.test.js +81 -0
  99. package/test/workspace-add.test.js +106 -0
  100. package/test/workspace.unit.test.js +230 -0
@@ -0,0 +1,41 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ // Only universally safe ignores — language-specific build dirs (.zig-cache,
5
+ // target, node_modules, vendor, etc.) are deferred to the agent walk milestone.
6
+ const IGNORED_DIRS = new Set([".git", ".worktrees"]);
7
+
8
+ /**
9
+ * Walk a directory (BFS, depth-limited) and collect file paths.
10
+ * @param {string} rootPath
11
+ * @param {number} maxDepth default 5 — preview needs one extra level vs spec scan
12
+ * @returns {string[]} absolute file paths
13
+ */
14
+ export function walkDir(rootPath, maxDepth = 5) {
15
+ const results = [];
16
+ const queue = [{ path: rootPath, depth: 0 }];
17
+
18
+ while (queue.length > 0) {
19
+ const { path: current, depth } = queue.shift();
20
+ if (depth > maxDepth) continue;
21
+
22
+ let entries;
23
+ try {
24
+ entries = readdirSync(current, { withFileTypes: true });
25
+ } catch {
26
+ continue;
27
+ }
28
+
29
+ for (const entry of entries) {
30
+ if (IGNORED_DIRS.has(entry.name)) continue;
31
+ const fullPath = join(current, entry.name);
32
+ if (entry.isDirectory()) {
33
+ queue.push({ path: fullPath, depth: depth + 1 });
34
+ } else if (entry.isFile()) {
35
+ results.push(fullPath);
36
+ }
37
+ }
38
+ }
39
+
40
+ return results;
41
+ }
@@ -0,0 +1,95 @@
1
+ const DEFAULT_API_URL = "http://localhost:3000";
2
+
3
+ function normalizeApiUrl(url) {
4
+ return String(url || DEFAULT_API_URL).replace(/\/+$/, "");
5
+ }
6
+
7
+ function splitOption(token) {
8
+ const idx = token.indexOf("=");
9
+ if (idx === -1) return { key: token, value: null };
10
+ return { key: token.slice(0, idx), value: token.slice(idx + 1) };
11
+ }
12
+
13
+ function parseFlags(tokens) {
14
+ const options = {};
15
+ const positionals = [];
16
+
17
+ for (let i = 0; i < tokens.length; i += 1) {
18
+ const token = tokens[i];
19
+ if (!token.startsWith("--")) {
20
+ positionals.push(token);
21
+ continue;
22
+ }
23
+
24
+ const { key, value } = splitOption(token);
25
+ const normalized = key.slice(2);
26
+
27
+ if (value !== null) {
28
+ options[normalized] = value;
29
+ continue;
30
+ }
31
+
32
+ const next = tokens[i + 1];
33
+ if (next && !next.startsWith("--")) {
34
+ options[normalized] = next;
35
+ i += 1;
36
+ continue;
37
+ }
38
+
39
+ options[normalized] = true;
40
+ }
41
+
42
+ return { options, positionals };
43
+ }
44
+
45
+ function parseGlobalArgs(argv, env = process.env) {
46
+ const options = {
47
+ json: false,
48
+ noInput: false,
49
+ noOpen: false,
50
+ help: false,
51
+ version: false,
52
+ api: null,
53
+ };
54
+
55
+ const rest = [];
56
+ for (let i = 0; i < argv.length; i += 1) {
57
+ const token = argv[i];
58
+ if (token === "--json") {
59
+ options.json = true;
60
+ } else if (token === "--no-input") {
61
+ options.noInput = true;
62
+ } else if (token === "--no-open") {
63
+ options.noOpen = true;
64
+ } else if (token === "--help" || token === "-h") {
65
+ options.help = true;
66
+ } else if (token === "--version") {
67
+ options.version = true;
68
+ } else if (token === "--api") {
69
+ options.api = argv[i + 1] || null;
70
+ i += 1;
71
+ } else if (token.startsWith("--api=")) {
72
+ options.api = token.slice("--api=".length);
73
+ } else {
74
+ rest.push(token);
75
+ }
76
+ }
77
+
78
+ const derived = {
79
+ apiUrl: normalizeApiUrl(options.api || env.ZOMBIE_API_URL || env.API_URL || DEFAULT_API_URL),
80
+ json: options.json,
81
+ noInput: options.noInput,
82
+ noOpen: options.noOpen,
83
+ help: options.help,
84
+ version: options.version,
85
+ };
86
+
87
+ return { global: derived, rest };
88
+ }
89
+
90
+ export {
91
+ DEFAULT_API_URL,
92
+ normalizeApiUrl,
93
+ parseFlags,
94
+ parseGlobalArgs,
95
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Authentication guard — blocks unauthenticated access to protected commands.
3
+ */
4
+
5
+ export function requireAuth(ctx) {
6
+ if (ctx.token || ctx.apiKey) {
7
+ return { ok: true };
8
+ }
9
+ return { ok: false };
10
+ }
11
+
12
+ export const AUTH_FAIL_MESSAGE = "not authenticated \u2014 run `zombiectl login` first";
@@ -0,0 +1,44 @@
1
+ function decodeTokenPayload(token) {
2
+ if (!token || typeof token !== "string") return null;
3
+ const parts = token.split(".");
4
+ if (parts.length < 2 || !parts[1]) return null;
5
+ try {
6
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
7
+ const padded = base64 + "===".slice((base64.length + 3) % 4);
8
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ function extractDistinctIdFromToken(token) {
15
+ const payload = decodeTokenPayload(token);
16
+ if (payload && typeof payload.sub === "string" && payload.sub.trim().length > 0) {
17
+ return payload.sub.trim();
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function extractRoleFromToken(token) {
23
+ const payload = decodeTokenPayload(token);
24
+ if (!payload) return null;
25
+
26
+ const candidates = [
27
+ payload.role,
28
+ payload.metadata?.role,
29
+ payload.custom_claims?.role,
30
+ payload.app_metadata?.role,
31
+ payload["https://usezombie.dev/role"],
32
+ payload["https://usezombie.com/role"],
33
+ payload.metadata?.["https://usezombie.dev/role"],
34
+ payload.metadata?.["https://usezombie.com/role"],
35
+ ];
36
+ for (const raw of candidates) {
37
+ if (typeof raw !== "string") continue;
38
+ const value = raw.toLowerCase();
39
+ if (value === "user" || value === "operator" || value === "admin") return value;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ export { decodeTokenPayload, extractDistinctIdFromToken, extractRoleFromToken };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CLI banner for --help and --version output.
3
+ */
4
+
5
+ export function printBanner(stream, version, opts = {}) {
6
+ const noColor = opts.noColor || false;
7
+ const jsonMode = opts.jsonMode || false;
8
+
9
+ if (jsonMode) return;
10
+
11
+ const label = ` zombiectl v${version} `;
12
+
13
+ if (noColor) {
14
+ stream.write(`zombiectl v${version}\n`);
15
+ return;
16
+ }
17
+
18
+ const c = (code, s) => `\u001b[${code}m${s}\u001b[0m`;
19
+ const bar = "\u2500".repeat(label.length);
20
+
21
+ stream.write(` ${c("1;36", `\u256D${bar}\u256E`)}\n`);
22
+ stream.write(` \u{1F9DF} ${c("1;36", "\u2502")}${c("1;37", label)}${c("1;36", "\u2502")}\n`);
23
+ stream.write(` ${c("1;36", `\u2570${bar}\u256F`)}\n`);
24
+ stream.write(` ${c("2", " autonomous agent cli")}\n`);
25
+ }
26
+
27
+ export function printPreReleaseWarning(stream, opts = {}) {
28
+ const noColor = opts.noColor || false;
29
+ const jsonMode = opts.jsonMode || false;
30
+ const ttyOnly = opts.ttyOnly || false;
31
+
32
+ if (jsonMode) return;
33
+ if (ttyOnly) return;
34
+
35
+ if (noColor) {
36
+ stream.write(`\n[PRE-RELEASE] This is a pre-release build for early access testing.\n`);
37
+ stream.write(`Contact nkishore@megam.io to get access.\n`);
38
+ stream.write(`General availability: April 5, 2026\n\n`);
39
+ return;
40
+ }
41
+
42
+ const c = (code, s) => `\u001b[${code}m${s}\u001b[0m`;
43
+ stream.write(`\n ${c("1;33", "⚠ Pre-release build")} — not for production use.\n`);
44
+ stream.write(` Early access testing only. Contact ${c("1;37", "nkishore@megam.io")} to get access.\n`);
45
+ stream.write(` General availability: ${c("1;37", "April 5, 2026")}\n\n`);
46
+ }
@@ -0,0 +1,17 @@
1
+ export function registerProgramCommands(handlers) {
2
+ return {
3
+ login: handlers.login,
4
+ logout: handlers.logout,
5
+ workspace: handlers.workspace,
6
+ "specs.sync": handlers.specsSync,
7
+ "spec.init": handlers.specInit,
8
+ run: handlers.run,
9
+ "runs.list": handlers.runsList,
10
+ "runs.cancel": handlers.runsCancel,
11
+ doctor: handlers.doctor,
12
+ harness: handlers.harness,
13
+ "skill-secret": handlers.skillSecret,
14
+ agent: handlers.agent,
15
+ admin: handlers.admin,
16
+ };
17
+ }
@@ -0,0 +1,38 @@
1
+ import { ApiError, apiRequest, authHeaders } from "../lib/http.js";
2
+
3
+ function apiHeaders(ctx) {
4
+ return authHeaders({ token: ctx.token, apiKey: ctx.apiKey });
5
+ }
6
+
7
+ async function request(ctx, reqPath, options = {}) {
8
+ const url = `${ctx.apiUrl}${reqPath}`;
9
+ return apiRequest(url, {
10
+ ...options,
11
+ fetchImpl: ctx.fetchImpl,
12
+ });
13
+ }
14
+
15
+ function printApiError(stderr, err, jsonMode, printJson, writeLine) {
16
+ if (!(err instanceof ApiError)) throw err;
17
+ const payload = {
18
+ error: {
19
+ code: err.code || "API_ERROR",
20
+ message: err.message,
21
+ status: err.status || null,
22
+ request_id: err.requestId || null,
23
+ },
24
+ };
25
+ if (jsonMode) {
26
+ printJson(stderr, payload);
27
+ } else {
28
+ writeLine(stderr, `error: ${payload.error.code} ${payload.error.message}`);
29
+ if (payload.error.request_id) writeLine(stderr, `request_id: ${payload.error.request_id}`);
30
+ }
31
+ }
32
+
33
+ export {
34
+ ApiError,
35
+ apiHeaders,
36
+ printApiError,
37
+ request,
38
+ };
@@ -0,0 +1,83 @@
1
+ import { printBanner } from "./banner.js";
2
+
3
+ function writeLine(stream, line = "") {
4
+ stream.write(`${line}\n`);
5
+ }
6
+
7
+ function printJson(stream, value) {
8
+ writeLine(stream, JSON.stringify(value, null, 2));
9
+ }
10
+
11
+ function printHelp(stdout, ui, opts = {}) {
12
+ const version = opts.version || "0.1.0";
13
+ const noColor = Boolean(opts.env?.NO_COLOR === "1" || opts.env?.NO_COLOR === "true");
14
+ const jsonMode = opts.jsonMode || false;
15
+ const showOperator = Boolean(opts.env?.ZOMBIE_OPERATOR === "1" || opts.authRole === "operator" || opts.authRole === "admin" || opts.operator);
16
+
17
+ printBanner(stdout, version, { noColor, jsonMode });
18
+ writeLine(stdout);
19
+ writeLine(stdout, " " + ui.head("UseZombie CLI") + ui.dim(" — autonomous agent platform"));
20
+ writeLine(stdout);
21
+ writeLine(stdout, ui.head("USAGE"));
22
+ writeLine(stdout, " zombiectl [--api URL] [--json] <command> [subcommand] [flags]");
23
+ writeLine(stdout);
24
+ writeLine(stdout, ui.head("USER COMMANDS"));
25
+ writeLine(stdout, " login [--timeout-sec N] [--poll-ms N] [--no-open]");
26
+ writeLine(stdout, " logout");
27
+ writeLine(stdout, " workspace add <repo_url> [--default-branch BRANCH]");
28
+ writeLine(stdout, " workspace list");
29
+ writeLine(stdout, " workspace remove <workspace_id>");
30
+ writeLine(stdout, " spec init [--path DIR] [--output PATH]");
31
+ writeLine(stdout, " specs sync [--workspace-id ID]");
32
+ writeLine(stdout, " run [--workspace-id ID] [--spec-id ID] [--mode MODE] [--requested-by USER] [--idempotency-key KEY]");
33
+ writeLine(stdout, " run [--spec FILE] [--preview] [--preview-only] [--path DIR]");
34
+ writeLine(stdout, " run status <run_id>");
35
+ writeLine(stdout, " runs list [--workspace-id ID]");
36
+ writeLine(stdout, " doctor");
37
+
38
+ if (showOperator) {
39
+ writeLine(stdout);
40
+ writeLine(stdout, ui.head("OPERATOR COMMANDS"));
41
+ writeLine(stdout, " harness source put --workspace-id ID --file PATH [--agent-id ID] [--name NAME]");
42
+ writeLine(stdout, " harness compile --workspace-id ID [--agent-id ID] [--config-version-id ID]");
43
+ writeLine(stdout, " harness activate --workspace-id ID --config-version-id ID [--activated-by USER]");
44
+ writeLine(stdout, " harness active --workspace-id ID");
45
+ writeLine(stdout, " workspace upgrade-scale --workspace-id ID --subscription-id SUBSCRIPTION_ID");
46
+ writeLine(stdout, " skill-secret put --workspace-id ID --skill-ref REF --key KEY --value VALUE [--scope host|sandbox]");
47
+ writeLine(stdout, " skill-secret delete --workspace-id ID --skill-ref REF --key KEY");
48
+ writeLine(stdout, " agent scores <agent-id> [--limit N] [--starting-after ID]");
49
+ writeLine(stdout, " agent profile <agent-id>");
50
+ writeLine(stdout, " agent improvement-report <agent-id>");
51
+ writeLine(stdout, " agent proposals <agent-id>");
52
+ writeLine(stdout, " agent proposals <agent-id> approve <proposal-id>");
53
+ writeLine(stdout, " agent proposals <agent-id> reject <proposal-id> [--reason TEXT]");
54
+ writeLine(stdout, " agent proposals <agent-id> veto <proposal-id> [--reason TEXT]");
55
+ writeLine(stdout, " agent harness revert <agent-id> --to-change <change-id>");
56
+ writeLine(stdout, " admin config set scoring_context_max_tokens <value> --workspace-id ID");
57
+ }
58
+
59
+ writeLine(stdout);
60
+ writeLine(stdout, ui.head("GLOBAL FLAGS"));
61
+ writeLine(stdout, " --api URL API base URL");
62
+ writeLine(stdout, " --json Machine-readable JSON output");
63
+ writeLine(stdout, " --no-input Disable interactive prompts");
64
+ writeLine(stdout, " --no-open Skip auto-opening browser");
65
+ writeLine(stdout, " --version Show version");
66
+ writeLine(stdout, " --help, -h Show this help");
67
+
68
+ writeLine(stdout);
69
+ writeLine(stdout, ui.head("ENVIRONMENT VARIABLES"));
70
+ writeLine(stdout, " ZOMBIE_API_URL API base URL (overridden by --api)");
71
+ writeLine(stdout, " ZOMBIE_TOKEN Auth token (overridden by login)");
72
+ writeLine(stdout, " ZOMBIE_API_KEY API key for service auth");
73
+ writeLine(stdout, " ZOMBIE_OPERATOR Set to 1 to force-show operator commands in help");
74
+ writeLine(stdout, " NO_COLOR Set to 1 to disable color output");
75
+ writeLine(stdout);
76
+ writeLine(stdout, ui.dim("workspace add opens UseZombie GitHub App install and binds via callback."));
77
+ }
78
+
79
+ export {
80
+ printHelp,
81
+ printJson,
82
+ writeLine,
83
+ };
@@ -0,0 +1,20 @@
1
+ const routes = [
2
+ { key: "login", match: (cmd) => cmd === "login" },
3
+ { key: "logout", match: (cmd) => cmd === "logout" },
4
+ { key: "workspace", match: (cmd) => cmd === "workspace" },
5
+ { key: "specs.sync", match: (cmd, args) => cmd === "specs" && args[0] === "sync" },
6
+ { key: "spec.init", match: (cmd, args) => cmd === "spec" && args[0] === "init" },
7
+ { key: "run", match: (cmd) => cmd === "run" },
8
+ { key: "runs.list", match: (cmd, args) => cmd === "runs" && args[0] === "list" },
9
+ // M17_001 §3: runs cancel
10
+ { key: "runs.cancel", match: (cmd, args) => cmd === "runs" && args[0] === "cancel" },
11
+ { key: "doctor", match: (cmd) => cmd === "doctor" },
12
+ { key: "harness", match: (cmd) => cmd === "harness" },
13
+ { key: "skill-secret", match: (cmd) => cmd === "skill-secret" },
14
+ { key: "agent", match: (cmd) => cmd === "agent" },
15
+ { key: "admin", match: (cmd) => cmd === "admin" },
16
+ ];
17
+
18
+ export function findRoute(command, args) {
19
+ return routes.find((r) => r.match(command, args)) || null;
20
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Levenshtein distance and command suggestion utilities.
3
+ */
4
+
5
+ const KNOWN_COMMANDS = [
6
+ "login",
7
+ "logout",
8
+ "workspace",
9
+ "spec",
10
+ "specs",
11
+ "run",
12
+ "runs",
13
+ "doctor",
14
+ "harness",
15
+ "skill-secret",
16
+ "agent",
17
+ ];
18
+
19
+ const KNOWN_SUBCOMMANDS = {
20
+ workspace: ["add", "list", "remove"],
21
+ spec: ["init"],
22
+ specs: ["sync"],
23
+ runs: ["list"],
24
+ run: ["status"],
25
+ harness: ["source", "compile", "activate", "active"],
26
+ "skill-secret": ["put", "delete"],
27
+ agent: ["scores", "profile"],
28
+ };
29
+
30
+ export function levenshteinDistance(a, b) {
31
+ const m = a.length;
32
+ const n = b.length;
33
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
34
+
35
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
36
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
37
+
38
+ for (let i = 1; i <= m; i++) {
39
+ for (let j = 1; j <= n; j++) {
40
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
41
+ dp[i][j] = Math.min(
42
+ dp[i - 1][j] + 1,
43
+ dp[i][j - 1] + 1,
44
+ dp[i - 1][j - 1] + cost,
45
+ );
46
+ }
47
+ }
48
+ return dp[m][n];
49
+ }
50
+
51
+ export function suggestCommand(input, knownCommands = KNOWN_COMMANDS, subcommands = KNOWN_SUBCOMMANDS) {
52
+ const maxDistance = 3;
53
+ const candidates = [];
54
+
55
+ // Match against top-level commands
56
+ for (const cmd of knownCommands) {
57
+ const d = levenshteinDistance(input.toLowerCase(), cmd.toLowerCase());
58
+ if (d > 0 && d <= maxDistance) {
59
+ candidates.push({ label: cmd, distance: d });
60
+ }
61
+ }
62
+
63
+ // Match against "command subcommand" combos
64
+ for (const [cmd, subs] of Object.entries(subcommands)) {
65
+ for (const sub of subs) {
66
+ const full = `${cmd} ${sub}`;
67
+ const d = levenshteinDistance(input.toLowerCase(), full.toLowerCase());
68
+ if (d > 0 && d <= maxDistance) {
69
+ candidates.push({ label: full, distance: d });
70
+ }
71
+ }
72
+ }
73
+
74
+ candidates.sort((a, b) => a.distance - b.distance);
75
+ return candidates.map((c) => c.label);
76
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ID format validation utilities.
3
+ */
4
+
5
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+ const SAFE_ID_RE = /^[a-zA-Z0-9_-]{4,128}$/;
7
+
8
+ export function isValidId(value) {
9
+ if (!value || typeof value !== "string") return false;
10
+ return UUID_RE.test(value) || SAFE_ID_RE.test(value);
11
+ }
12
+
13
+ export function validateRequiredId(value, name) {
14
+ if (!value || typeof value !== "string" || value.trim().length === 0) {
15
+ return { ok: false, message: `${name} is required` };
16
+ }
17
+ if (!isValidId(value)) {
18
+ return {
19
+ ok: false,
20
+ message: `invalid ${name}: expected UUID format (e.g. 550e8400-e29b-41d4-a716-446655440000) or alphanumeric identifier (4-128 chars)`,
21
+ };
22
+ }
23
+ return { ok: true };
24
+ }
@@ -0,0 +1,59 @@
1
+ function spinnerFrames(style) {
2
+ if (style === "dotmatrix" || style === "matrix") {
3
+ return ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
4
+ }
5
+
6
+ return ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
7
+ }
8
+
9
+ export async function withSpinner(opts, work) {
10
+ const spin = createSpinner(opts);
11
+ spin.start();
12
+
13
+ try {
14
+ const out = await work();
15
+ spin.succeed();
16
+ return out;
17
+ } catch (err) {
18
+ spin.fail();
19
+ throw err;
20
+ }
21
+ }
22
+
23
+ export function createSpinner(opts = {}) {
24
+ const enabled = opts.enabled === true;
25
+ const stream = opts.stream || process.stderr;
26
+ const label = opts.label || "working";
27
+ const style = opts.style || process.env.ZOMBIE_PROGRESS_STYLE || "spinner";
28
+ const frames = spinnerFrames(style);
29
+ let i = 0;
30
+ let timer = null;
31
+
32
+ return {
33
+ start() {
34
+ if (!enabled || timer) return;
35
+ timer = setInterval(() => {
36
+ stream.write(`\r${frames[i % frames.length]} ${label}`);
37
+ i += 1;
38
+ }, 80);
39
+ },
40
+ succeed(message) {
41
+ if (!enabled) return;
42
+ if (timer) clearInterval(timer);
43
+ timer = null;
44
+ stream.write(`\r✔ ${message || label}\n`);
45
+ },
46
+ fail(message) {
47
+ if (!enabled) return;
48
+ if (timer) clearInterval(timer);
49
+ timer = null;
50
+ stream.write(`\r✖ ${message || label}\n`);
51
+ },
52
+ stop() {
53
+ if (!enabled) return;
54
+ if (timer) clearInterval(timer);
55
+ timer = null;
56
+ stream.write("\r");
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,62 @@
1
+ // NO_COLOR spec: any non-empty value disables color
2
+ const useColor = !process.env.NO_COLOR && process.stdout.isTTY === true;
3
+
4
+ function color(code, text) {
5
+ return useColor ? `\u001b[${code}m${text}\u001b[0m` : text;
6
+ }
7
+
8
+ export const ui = {
9
+ ok: (s) => color("32", `✔ ${s}`),
10
+ info: (s) => color("36", `ℹ ${s}`),
11
+ warn: (s) => color("33", `▲ ${s}`),
12
+ err: (s) => color("31", `✖ ${s}`),
13
+ head: (s) => color("1;36", s),
14
+ dim: (s) => color("2", s),
15
+ label: (s) => color("2", s), // dim alias for KV labels
16
+ run: (s) => color("1;35", `◉ ${s}`), // bold magenta — active/running state
17
+ step: (s) => color("1;37", s), // bold white — numbered steps
18
+ };
19
+
20
+ export function printSection(stream, title, theme = ui) {
21
+ const heading = theme.head ? theme.head(title) : title;
22
+ const rule = "─".repeat(title.length);
23
+ stream.write(`\n${heading}\n`);
24
+ stream.write(`${theme.dim ? theme.dim(rule) : rule}\n`);
25
+ }
26
+
27
+ export function printKeyValue(stream, rows, theme = ui) {
28
+ const entries = Object.entries(rows);
29
+ if (entries.length === 0) return;
30
+ const width = Math.max(...entries.map(([k]) => k.length), 0);
31
+ const sep = theme.dim ? theme.dim(" · ") : " · ";
32
+ for (const [key, value] of entries) {
33
+ const label = theme.dim ? theme.dim(key.padEnd(width)) : key.padEnd(width);
34
+ stream.write(` ${label}${sep}${value}\n`);
35
+ }
36
+ }
37
+
38
+ export function printTable(stream, columns, rows, theme = ui) {
39
+ if (rows.length === 0) {
40
+ const none = theme.dim ? theme.dim("(none)") : "(none)";
41
+ stream.write(`${none}\n`);
42
+ return;
43
+ }
44
+ const widths = columns.map((c) =>
45
+ Math.max(
46
+ c.label.length,
47
+ ...rows.map((r) => String(r[c.key] ?? "").length),
48
+ ),
49
+ );
50
+
51
+ const headerStr = columns.map((c, i) => c.label.padEnd(widths[i])).join(" ");
52
+ stream.write(`${theme.head ? theme.head(headerStr) : headerStr}\n`);
53
+
54
+ const sepStr = widths.map((w) => "\u2500".repeat(w)).join(" ");
55
+ stream.write(`${theme.dim ? theme.dim(sepStr) : sepStr}\n`);
56
+
57
+ for (const row of rows) {
58
+ stream.write(
59
+ `${columns.map((c, i) => String(row[c.key] ?? "").padEnd(widths[i])).join(" ")}\n`,
60
+ );
61
+ }
62
+ }
@@ -0,0 +1,25 @@
1
+ import { test } from "bun:test";
2
+ import assert from "node:assert/strict";
3
+ import { commandAdmin } from "../src/commands/admin.js";
4
+ import { makeNoop, ui, WS_ID } from "./helpers.js";
5
+
6
+ test("admin config set scoring_context_max_tokens posts workspace config update", async () => {
7
+ let called = null;
8
+ const deps = {
9
+ parseFlags: () => ({ options: { "workspace-id": WS_ID }, positionals: ["1024"] }),
10
+ request: async (_ctx, url, opts) => {
11
+ called = { url, opts };
12
+ return { workspace_id: WS_ID, scoring_context_max_tokens: 1024 };
13
+ },
14
+ apiHeaders: () => ({ Authorization: "Bearer token" }),
15
+ ui,
16
+ printJson: () => {},
17
+ writeLine: () => {},
18
+ };
19
+
20
+ const code = await commandAdmin({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, ["config", "set", "scoring_context_max_tokens"], null, deps);
21
+ assert.equal(code, 0);
22
+ assert.equal(called.url, `/v1/workspaces/${encodeURIComponent(WS_ID)}/scoring/config`);
23
+ assert.equal(called.opts.method, "POST");
24
+ assert.deepEqual(JSON.parse(called.opts.body), { scoring_context_max_tokens: 1024 });
25
+ });