@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,39 @@
1
+ import { AGENTS_PATH } from "../lib/api-paths.js";
2
+ import { queueCliAnalyticsEvent, setCliAnalyticsContext } from "../lib/analytics.js";
3
+
4
+ export async function commandAgentProfile(ctx, parsed, agentId, deps) {
5
+ const { request, apiHeaders, printJson, printKeyValue, printSection = () => {} } = deps;
6
+
7
+ const res = await request(ctx, `${AGENTS_PATH}${encodeURIComponent(agentId)}`, {
8
+ method: "GET",
9
+ headers: apiHeaders(ctx),
10
+ });
11
+ setCliAnalyticsContext(ctx, {
12
+ agent_id: res.agent_id,
13
+ workspace_id: res.workspace_id,
14
+ trust_level: res.trust_level,
15
+ agent_status: res.status,
16
+ });
17
+ queueCliAnalyticsEvent(ctx, "agent_profile_viewed", {
18
+ agent_id: res.agent_id,
19
+ workspace_id: res.workspace_id,
20
+ });
21
+
22
+ if (ctx.jsonMode) {
23
+ printJson(ctx.stdout, res);
24
+ } else {
25
+ printSection(ctx.stdout, `Agent profile · ${res.agent_id}`);
26
+ printKeyValue(ctx.stdout, {
27
+ agent_id: res.agent_id,
28
+ name: res.name,
29
+ status: res.status,
30
+ workspace_id: res.workspace_id,
31
+ trust_level: res.trust_level,
32
+ trust_streak_runs: res.trust_streak_runs,
33
+ improvement_stalled_warning: res.improvement_stalled_warning,
34
+ created_at: res.created_at,
35
+ updated_at: res.updated_at,
36
+ });
37
+ }
38
+ return 0;
39
+ }
@@ -0,0 +1,158 @@
1
+ import { AGENTS_PATH } from "../lib/api-paths.js";
2
+ import { queueCliAnalyticsEvent, setCliAnalyticsContext } from "../lib/analytics.js";
3
+
4
+ export async function commandAgentProposals(ctx, parsed, agentId, deps) {
5
+ const { request, apiHeaders, printJson, printSection = () => {}, printTable, ui, writeLine } = deps;
6
+
7
+ const subaction = parsed.positionals[1] || "list";
8
+ const proposalId = parsed.positionals[2] || null;
9
+
10
+ if (subaction === "list") {
11
+ const res = await request(ctx, `${AGENTS_PATH}${encodeURIComponent(agentId)}/proposals`, {
12
+ method: "GET",
13
+ headers: apiHeaders(ctx),
14
+ });
15
+ const items = Array.isArray(res.data) ? res.data : [];
16
+ setCliAnalyticsContext(ctx, {
17
+ agent_id: agentId,
18
+ proposal_count: items.length,
19
+ });
20
+ queueCliAnalyticsEvent(ctx, "agent_proposals_viewed", {
21
+ agent_id: agentId,
22
+ proposal_count: items.length,
23
+ });
24
+
25
+ if (ctx.jsonMode) {
26
+ printJson(ctx.stdout, res);
27
+ } else {
28
+ if (items.length === 0) {
29
+ writeLine(ctx.stdout, ui.info("no open proposals"));
30
+ } else {
31
+ printSection(ctx.stdout, `Agent proposals · ${agentId}`);
32
+ printTable(ctx.stdout, [
33
+ { key: "proposal_id", label: "PROPOSAL_ID" },
34
+ { key: "status", label: "STATUS" },
35
+ { key: "trigger_reason", label: "TRIGGER" },
36
+ { key: "action", label: "ACTION" },
37
+ { key: "config_version_id", label: "CONFIG_VERSION_ID" },
38
+ { key: "created_at", label: "CREATED_AT" },
39
+ ], items.map((item) => ({
40
+ ...item,
41
+ action: describeProposalAction(item),
42
+ })));
43
+ }
44
+ }
45
+ return 0;
46
+ }
47
+
48
+ if (!proposalId) {
49
+ writeLine(ctx.stderr, ui.err(`agent proposals ${subaction} requires <proposal-id>`));
50
+ return 2;
51
+ }
52
+
53
+ if (subaction === "approve") {
54
+ const res = await request(
55
+ ctx,
56
+ `${AGENTS_PATH}${encodeURIComponent(agentId)}/proposals/${encodeURIComponent(proposalId)}:approve`,
57
+ {
58
+ method: "POST",
59
+ headers: apiHeaders(ctx),
60
+ },
61
+ );
62
+ setCliAnalyticsContext(ctx, {
63
+ agent_id: agentId,
64
+ proposal_id: res.proposal_id,
65
+ proposal_status: res.status,
66
+ });
67
+ queueCliAnalyticsEvent(ctx, "agent_proposal_approved", {
68
+ agent_id: agentId,
69
+ proposal_id: res.proposal_id,
70
+ });
71
+ if (ctx.jsonMode) {
72
+ printJson(ctx.stdout, res);
73
+ } else {
74
+ writeLine(ctx.stdout, ui.ok(`approved ${res.proposal_id} -> ${res.status}`));
75
+ }
76
+ return 0;
77
+ }
78
+
79
+ if (subaction === "reject") {
80
+ const reason = parsed.options.reason || null;
81
+ const res = await request(
82
+ ctx,
83
+ `${AGENTS_PATH}${encodeURIComponent(agentId)}/proposals/${encodeURIComponent(proposalId)}:reject`,
84
+ {
85
+ method: "POST",
86
+ headers: apiHeaders(ctx),
87
+ body: JSON.stringify(reason ? { reason } : {}),
88
+ },
89
+ );
90
+ setCliAnalyticsContext(ctx, {
91
+ agent_id: agentId,
92
+ proposal_id: res.proposal_id,
93
+ proposal_status: res.status,
94
+ rejection_reason: res.rejection_reason,
95
+ });
96
+ queueCliAnalyticsEvent(ctx, "agent_proposal_rejected", {
97
+ agent_id: agentId,
98
+ proposal_id: res.proposal_id,
99
+ });
100
+ if (ctx.jsonMode) {
101
+ printJson(ctx.stdout, res);
102
+ } else {
103
+ writeLine(ctx.stdout, ui.ok(`rejected ${res.proposal_id} (${res.rejection_reason})`));
104
+ }
105
+ return 0;
106
+ }
107
+
108
+ if (subaction === "veto") {
109
+ const reason = parsed.options.reason || null;
110
+ const res = await request(
111
+ ctx,
112
+ `${AGENTS_PATH}${encodeURIComponent(agentId)}/proposals/${encodeURIComponent(proposalId)}:veto`,
113
+ {
114
+ method: "POST",
115
+ headers: apiHeaders(ctx),
116
+ body: JSON.stringify(reason ? { reason } : {}),
117
+ },
118
+ );
119
+ setCliAnalyticsContext(ctx, {
120
+ agent_id: agentId,
121
+ proposal_id: res.proposal_id,
122
+ proposal_status: res.status,
123
+ rejection_reason: res.rejection_reason,
124
+ });
125
+ queueCliAnalyticsEvent(ctx, "agent_proposal_vetoed", {
126
+ agent_id: agentId,
127
+ proposal_id: res.proposal_id,
128
+ });
129
+ if (ctx.jsonMode) {
130
+ printJson(ctx.stdout, res);
131
+ } else {
132
+ writeLine(ctx.stdout, ui.ok(`vetoed ${res.proposal_id} (${res.rejection_reason})`));
133
+ }
134
+ return 0;
135
+ }
136
+
137
+ writeLine(ctx.stderr, ui.err("usage: agent proposals <agent-id>"));
138
+ writeLine(ctx.stderr, ui.err(" agent proposals <agent-id> approve <proposal-id>"));
139
+ writeLine(ctx.stderr, ui.err(" agent proposals <agent-id> reject <proposal-id> [--reason TEXT]"));
140
+ writeLine(ctx.stderr, ui.err(" agent proposals <agent-id> veto <proposal-id> [--reason TEXT]"));
141
+ return 2;
142
+ }
143
+
144
+ function describeProposalAction(item) {
145
+ if (item?.status === "VETO_WINDOW") {
146
+ return `${formatCountdown(item.auto_apply_at)} - zombiectl agent proposals veto ${item.proposal_id} to cancel`;
147
+ }
148
+ return "manual review required";
149
+ }
150
+
151
+ function formatCountdown(autoApplyAt) {
152
+ if (!Number.isFinite(autoApplyAt)) return "Auto-apply scheduled";
153
+ const diffMs = Math.max(0, autoApplyAt - Date.now());
154
+ const totalMinutes = Math.floor(diffMs / 60000);
155
+ const hours = Math.floor(totalMinutes / 60);
156
+ const minutes = totalMinutes % 60;
157
+ return `Auto-applies in ${hours}h ${minutes}m`;
158
+ }
@@ -0,0 +1,44 @@
1
+ import { AGENTS_PATH } from "../lib/api-paths.js";
2
+ import { queueCliAnalyticsEvent, setCliAnalyticsContext } from "../lib/analytics.js";
3
+
4
+ export async function commandAgentScores(ctx, parsed, agentId, deps) {
5
+ const { request, apiHeaders, ui, printJson, printSection = () => {}, printTable, writeLine } = deps;
6
+
7
+ const limit = parsed.options.limit || 20;
8
+ const startingAfter = parsed.options["starting-after"] || null;
9
+
10
+ let url = `${AGENTS_PATH}${encodeURIComponent(agentId)}/scores?limit=${limit}`;
11
+ if (startingAfter) url += `&starting_after=${encodeURIComponent(startingAfter)}`;
12
+
13
+ const res = await request(ctx, url, { method: "GET", headers: apiHeaders(ctx) });
14
+ const items = Array.isArray(res.data) ? res.data : [];
15
+ setCliAnalyticsContext(ctx, {
16
+ agent_id: agentId,
17
+ score_count: items.length,
18
+ next_cursor: res.next_cursor ?? "",
19
+ });
20
+ queueCliAnalyticsEvent(ctx, "agent_scores_viewed", {
21
+ agent_id: agentId,
22
+ score_count: items.length,
23
+ });
24
+
25
+ if (ctx.jsonMode) {
26
+ printJson(ctx.stdout, res);
27
+ } else {
28
+ if (items.length === 0) {
29
+ writeLine(ctx.stdout, ui.info("no scores"));
30
+ } else {
31
+ printSection(ctx.stdout, `Agent scores · ${agentId}`);
32
+ printTable(ctx.stdout, [
33
+ { key: "score_id", label: "SCORE_ID" },
34
+ { key: "run_id", label: "RUN_ID" },
35
+ { key: "score", label: "SCORE" },
36
+ { key: "scored_at", label: "SCORED_AT" },
37
+ ], items);
38
+ }
39
+ if (res.has_more && res.next_cursor) {
40
+ writeLine(ctx.stdout, ui.dim(`next: --starting-after ${res.next_cursor}`));
41
+ }
42
+ }
43
+ return 0;
44
+ }
@@ -0,0 +1,108 @@
1
+ function createCoreOpsHandlers(ctx, workspaces, deps) {
2
+ const {
3
+ apiHeaders,
4
+ parseFlags,
5
+ printJson,
6
+ request,
7
+ ui,
8
+ writeLine,
9
+ } = deps;
10
+
11
+ async function commandDoctor() {
12
+ const checks = [];
13
+
14
+ try {
15
+ const healthz = await request(ctx, "/healthz", { method: "GET" });
16
+ checks.push({ name: "healthz", ok: healthz.status === "ok", detail: healthz });
17
+ } catch (err) {
18
+ checks.push({ name: "healthz", ok: false, detail: String(err) });
19
+ }
20
+
21
+ try {
22
+ const readyz = await request(ctx, "/readyz", { method: "GET" });
23
+ checks.push({ name: "readyz", ok: readyz.ready === true, detail: readyz });
24
+ } catch (err) {
25
+ checks.push({ name: "readyz", ok: false, detail: String(err) });
26
+ }
27
+
28
+ checks.push({ name: "credentials", ok: Boolean(ctx.token || ctx.apiKey), detail: ctx.token ? "token" : ctx.apiKey ? "api_key" : "missing" });
29
+ checks.push({ name: "workspace", ok: Boolean(workspaces.current_workspace_id), detail: workspaces.current_workspace_id || "missing" });
30
+
31
+ const ok = checks.every((c) => c.ok);
32
+ const report = { ok, api_url: ctx.apiUrl, checks };
33
+
34
+ if (ctx.jsonMode) {
35
+ printJson(ctx.stdout, report);
36
+ } else {
37
+ writeLine(ctx.stdout, ui.head("zombiectl doctor"));
38
+ writeLine(ctx.stdout);
39
+ for (const c of checks) {
40
+ const tag = c.ok ? "[OK]" : "[FAIL]";
41
+ writeLine(ctx.stdout, c.ok ? ui.ok(`${tag} ${c.name}`) : ui.err(`${tag} ${c.name}`));
42
+ }
43
+ writeLine(ctx.stdout);
44
+ const passed = checks.filter((c) => c.ok).length;
45
+ if (ok) {
46
+ writeLine(ctx.stdout, ui.ok(`All checks passed.`));
47
+ } else {
48
+ writeLine(ctx.stdout, ui.err(`${passed}/${checks.length} checks passed`));
49
+ }
50
+ }
51
+ return ok ? 0 : 1;
52
+ }
53
+
54
+ async function commandSkillSecret(args) {
55
+ const action = args[0];
56
+ const parsed = parseFlags(args.slice(1));
57
+ const workspaceId = parsed.options["workspace-id"] || workspaces.current_workspace_id;
58
+ const skillRef = parsed.options["skill-ref"];
59
+ const key = parsed.options.key;
60
+
61
+ if (!workspaceId || !skillRef || !key) {
62
+ writeLine(ctx.stderr, ui.err("skill-secret requires --workspace-id --skill-ref --key"));
63
+ return 2;
64
+ }
65
+
66
+ const route = `/v1/workspaces/${encodeURIComponent(workspaceId)}/skills/${encodeURIComponent(skillRef)}/secrets/${encodeURIComponent(key)}`;
67
+
68
+ if (action === "put") {
69
+ if (!parsed.options.value) {
70
+ writeLine(ctx.stderr, ui.err("skill-secret put requires --value"));
71
+ return 2;
72
+ }
73
+ const body = {
74
+ value: String(parsed.options.value),
75
+ scope: parsed.options.scope || "sandbox",
76
+ meta: {},
77
+ };
78
+ const res = await request(ctx, route, {
79
+ method: "PUT",
80
+ headers: apiHeaders(ctx),
81
+ body: JSON.stringify(body),
82
+ });
83
+ if (ctx.jsonMode) printJson(ctx.stdout, res);
84
+ else writeLine(ctx.stdout, ui.ok("skill secret stored"));
85
+ return 0;
86
+ }
87
+
88
+ if (action === "delete") {
89
+ const res = await request(ctx, route, {
90
+ method: "DELETE",
91
+ headers: apiHeaders(ctx),
92
+ });
93
+ if (ctx.jsonMode) printJson(ctx.stdout, res);
94
+ else writeLine(ctx.stdout, ui.ok("skill secret deleted"));
95
+ return 0;
96
+ }
97
+
98
+ writeLine(ctx.stderr, ui.err("usage: skill-secret put|delete ..."));
99
+ return 2;
100
+ }
101
+
102
+ return {
103
+ commandDoctor,
104
+ commandSkillSecret,
105
+ };
106
+ }
107
+
108
+ export { createCoreOpsHandlers };