@agentplate/cli 1.0.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 (139) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +206 -0
  4. package/agents/architect.md +108 -0
  5. package/agents/builder.md +97 -0
  6. package/agents/coordinator.md +113 -0
  7. package/agents/deployer.md +117 -0
  8. package/agents/devops.md +114 -0
  9. package/agents/lead.md +107 -0
  10. package/agents/merger.md +103 -0
  11. package/agents/reviewer.md +90 -0
  12. package/agents/scout.md +95 -0
  13. package/agents/verifier.md +106 -0
  14. package/package.json +64 -0
  15. package/src/agents/guard-rules.ts +55 -0
  16. package/src/agents/identity.test.ts +161 -0
  17. package/src/agents/identity.ts +229 -0
  18. package/src/agents/manifest.test.ts +260 -0
  19. package/src/agents/manifest.ts +286 -0
  20. package/src/agents/overlay.test.ts +190 -0
  21. package/src/agents/overlay.ts +212 -0
  22. package/src/agents/system-prompt.test.ts +53 -0
  23. package/src/agents/system-prompt.ts +95 -0
  24. package/src/agents/turn-runner.ts +79 -0
  25. package/src/commands/coordinator.test.ts +75 -0
  26. package/src/commands/coordinator.ts +259 -0
  27. package/src/commands/deploy.test.ts +504 -0
  28. package/src/commands/deploy.ts +874 -0
  29. package/src/commands/doctor.test.ts +106 -0
  30. package/src/commands/doctor.ts +208 -0
  31. package/src/commands/init.ts +71 -0
  32. package/src/commands/log.ts +51 -0
  33. package/src/commands/mail.ts +197 -0
  34. package/src/commands/merge.ts +127 -0
  35. package/src/commands/model.ts +58 -0
  36. package/src/commands/prime.ts +61 -0
  37. package/src/commands/reap.ts +87 -0
  38. package/src/commands/serve.ts +61 -0
  39. package/src/commands/setup.ts +48 -0
  40. package/src/commands/ship.test.ts +106 -0
  41. package/src/commands/ship.ts +202 -0
  42. package/src/commands/skill.test.ts +458 -0
  43. package/src/commands/skill.ts +730 -0
  44. package/src/commands/sling.ts +365 -0
  45. package/src/commands/status.ts +60 -0
  46. package/src/commands/stop.ts +56 -0
  47. package/src/commands/tui.ts +199 -0
  48. package/src/commands/worktree.ts +77 -0
  49. package/src/config.test.ts +92 -0
  50. package/src/config.ts +202 -0
  51. package/src/db/sqlite.test.ts +77 -0
  52. package/src/db/sqlite.ts +102 -0
  53. package/src/deploy/audit.test.ts +233 -0
  54. package/src/deploy/audit.ts +245 -0
  55. package/src/deploy/context.test.ts +243 -0
  56. package/src/deploy/context.ts +72 -0
  57. package/src/deploy/registry.test.ts +101 -0
  58. package/src/deploy/registry.ts +86 -0
  59. package/src/deploy/secrets.test.ts +129 -0
  60. package/src/deploy/secrets.ts +69 -0
  61. package/src/deploy/targets/docker-gha.test.ts +323 -0
  62. package/src/deploy/targets/docker-gha.ts +841 -0
  63. package/src/deploy/types.ts +153 -0
  64. package/src/errors.test.ts +42 -0
  65. package/src/errors.ts +69 -0
  66. package/src/events/store.test.ts +183 -0
  67. package/src/events/store.ts +201 -0
  68. package/src/index.ts +137 -0
  69. package/src/insights/quality-gates.ts +73 -0
  70. package/src/json.test.ts +28 -0
  71. package/src/json.ts +50 -0
  72. package/src/logging/color.ts +62 -0
  73. package/src/logging/logger.ts +60 -0
  74. package/src/logging/sanitizer.test.ts +36 -0
  75. package/src/logging/sanitizer.ts +57 -0
  76. package/src/mail/client.test.ts +192 -0
  77. package/src/mail/client.ts +188 -0
  78. package/src/mail/store.test.ts +279 -0
  79. package/src/mail/store.ts +311 -0
  80. package/src/merge/lock.test.ts +88 -0
  81. package/src/merge/lock.ts +84 -0
  82. package/src/merge/queue.test.ts +136 -0
  83. package/src/merge/queue.ts +177 -0
  84. package/src/merge/resolver.test.ts +219 -0
  85. package/src/merge/resolver.ts +274 -0
  86. package/src/paths.ts +36 -0
  87. package/src/providers/apply.test.ts +90 -0
  88. package/src/providers/apply.ts +66 -0
  89. package/src/providers/registry.test.ts +74 -0
  90. package/src/providers/registry.ts +254 -0
  91. package/src/runtimes/claude.ts +313 -0
  92. package/src/runtimes/codex.ts +280 -0
  93. package/src/runtimes/cursor.ts +247 -0
  94. package/src/runtimes/gemini.ts +173 -0
  95. package/src/runtimes/mock.ts +71 -0
  96. package/src/runtimes/opencode.ts +259 -0
  97. package/src/runtimes/registry.test.ts +924 -0
  98. package/src/runtimes/registry.ts +63 -0
  99. package/src/runtimes/resolve.ts +45 -0
  100. package/src/runtimes/types.ts +97 -0
  101. package/src/scaffold.ts +68 -0
  102. package/src/secrets.test.ts +51 -0
  103. package/src/secrets.ts +78 -0
  104. package/src/serve/api.ts +667 -0
  105. package/src/serve/server.test.ts +433 -0
  106. package/src/serve/server.ts +271 -0
  107. package/src/serve/system.ts +90 -0
  108. package/src/serve/weather.ts +140 -0
  109. package/src/sessions/reaper.test.ts +162 -0
  110. package/src/sessions/reaper.ts +149 -0
  111. package/src/sessions/store.test.ts +351 -0
  112. package/src/sessions/store.ts +350 -0
  113. package/src/skills/distiller.test.ts +498 -0
  114. package/src/skills/distiller.ts +426 -0
  115. package/src/skills/feedback.test.ts +300 -0
  116. package/src/skills/feedback.ts +168 -0
  117. package/src/skills/lifecycle.ts +169 -0
  118. package/src/skills/retrieval.test.ts +421 -0
  119. package/src/skills/retrieval.ts +365 -0
  120. package/src/skills/safety.test.ts +335 -0
  121. package/src/skills/safety.ts +216 -0
  122. package/src/skills/store.test.ts +425 -0
  123. package/src/skills/store.ts +684 -0
  124. package/src/skills/types.ts +107 -0
  125. package/src/types.ts +442 -0
  126. package/src/utils/detect.test.ts +35 -0
  127. package/src/utils/detect.ts +82 -0
  128. package/src/version.test.ts +19 -0
  129. package/src/version.ts +7 -0
  130. package/src/wizard/setup.ts +254 -0
  131. package/src/worktree/manager.test.ts +181 -0
  132. package/src/worktree/manager.ts +229 -0
  133. package/templates/overlay.md.tmpl +102 -0
  134. package/ui/dist/assets/index-C7rXIMER.css +1 -0
  135. package/ui/dist/assets/index-W4kbr4by.js +4526 -0
  136. package/ui/dist/favicon.svg +21 -0
  137. package/ui/dist/index.html +16 -0
  138. package/ui/dist/logo-clay.svg +21 -0
  139. package/ui/dist/logo.svg +18 -0
@@ -0,0 +1,259 @@
1
+ /**
2
+ * OpenCode runtime adapter (SST OpenCode).
3
+ *
4
+ * Drives the `opencode` CLI. OpenCode reads an `AGENTS.md` instruction file from
5
+ * the working directory at startup (so the overlay is written there, not passed
6
+ * as a flag), and selects a model with `--model provider/model`. Provider auth is
7
+ * OpenCode's own (`opencode auth login`), so Agentplate injects no key — like a
8
+ * subscription runtime.
9
+ *
10
+ * The message is a POSITIONAL argument (`opencode run [message..]`), NOT a flag —
11
+ * a leading `--prompt` makes OpenCode print usage and do nothing. Headless turns
12
+ * use `opencode run --format json`, whose stdout is a stream of JSON events
13
+ * (`{ type, sessionID, … }`); {@link OpenCodeRuntime.parseEvents} normalizes them.
14
+ * Session continuity uses `--session <id>` captured from those events.
15
+ *
16
+ * Validated against opencode 1.15.x flag + JSON-event shapes.
17
+ */
18
+
19
+ import type { ResolvedModel } from "../types.ts";
20
+ import type { AgentEvent, AgentRuntime, DirectSpawnOpts, InteractiveSpawnOpts } from "./types.ts";
21
+
22
+ /**
23
+ * Auto-approve policy for unattended OpenCode turns, set via `OPENCODE_PERMISSION`
24
+ * (same JSON shape as the `permission` block in `opencode.json`). OpenCode has no
25
+ * stable first-class "skip permissions" CLI flag — the `run`-only
26
+ * `--dangerously-skip-permissions` is fragile and does NOT cover the hardcoded
27
+ * `external_directory → ask` default, which would still block a headless agent.
28
+ * This env var is the robust, version-stable mechanism. We allow the everyday
29
+ * tools (and `external_directory`, the headless gotcha) while DENYING the most
30
+ * destructive shell commands — using `deny` (not `ask`, which would hang a turn
31
+ * that has no TTY to answer it). Last-match-wins, so the catch-all comes first.
32
+ */
33
+ /**
34
+ * OpenCode addresses models as `provider/model` and rejects a bare id with
35
+ * "Invalid model format". A bare id almost always means an OpenCode Zen model
36
+ * (opencode's own `opencode` provider), so default that prefix. Ids that already
37
+ * carry a provider (`openrouter/…`, `opencode/…`, `anthropic/…`) pass through
38
+ * unchanged.
39
+ */
40
+ export function opencodeModel(model: string): string {
41
+ return model.includes("/") ? model : `opencode/${model}`;
42
+ }
43
+
44
+ const OPENCODE_BYPASS_PERMISSION = JSON.stringify({
45
+ bash: {
46
+ "*": "allow",
47
+ "rm -rf *": "deny",
48
+ "sudo *": "deny",
49
+ "mkfs *": "deny",
50
+ "dd *": "deny",
51
+ },
52
+ edit: "allow",
53
+ webfetch: "allow",
54
+ external_directory: "allow",
55
+ });
56
+
57
+ export class OpenCodeRuntime implements AgentRuntime {
58
+ /** Registry id; the value users pass to `--runtime opencode`. */
59
+ readonly id = "opencode";
60
+
61
+ /** Beta: validated against opencode 1.15.x flag shapes. */
62
+ readonly stability = "beta" as const;
63
+
64
+ /** OpenCode reads `AGENTS.md` from the cwd at startup. */
65
+ readonly instructionPath = "AGENTS.md";
66
+
67
+ /**
68
+ * Build argv for a single headless turn (`opencode run`).
69
+ *
70
+ * `run <message>` executes the prompt non-interactively — the message is the
71
+ * trailing POSITIONAL (the `run` subcommand has no `--prompt` flag and prints
72
+ * usage if given one). `--model provider/model` pins the model, `--format json`
73
+ * emits the per-event JSON that {@link parseEvents} consumes, and `--session
74
+ * <id>` resumes a prior turn (omitted on the first turn).
75
+ *
76
+ * Permission auto-approval is handled by the `OPENCODE_PERMISSION` env var (see
77
+ * {@link buildEnv}), NOT a CLI flag: the `--dangerously-skip-permissions` flag
78
+ * is `run`-only, fragile across versions, and misses the `external_directory`
79
+ * default that blocks headless agents.
80
+ */
81
+ buildDirectSpawn(opts: DirectSpawnOpts): string[] {
82
+ return [
83
+ "opencode",
84
+ "run",
85
+ "--model",
86
+ opencodeModel(opts.model),
87
+ "--format",
88
+ "json",
89
+ ...(opts.resumeSessionId ? ["--session", opts.resumeSessionId] : []),
90
+ // Message is the trailing positional argument.
91
+ opts.prompt ?? "",
92
+ ];
93
+ }
94
+
95
+ /**
96
+ * Build argv for an ATTENDED interactive OpenCode session.
97
+ *
98
+ * Run in the foreground with inherited stdio so the operator chats directly
99
+ * (`coordinator start`). OpenCode has no system-prompt flag, so the agent's
100
+ * role is supplied via the `AGENTS.md` overlay. The seed message uses the
101
+ * top-level `--prompt` flag (the interactive entrypoint, unlike `run`, has no
102
+ * positional message).
103
+ *
104
+ * NOTE: `--dangerously-skip-permissions` is a `run`-only flag — passing it to
105
+ * the interactive entrypoint makes OpenCode error out (exit 1). The attended
106
+ * coordinator approves actions in the TUI instead, so `permissionMode` is
107
+ * accepted for interface parity but not translated to a flag here.
108
+ */
109
+ buildInteractiveSpawn(opts: InteractiveSpawnOpts): string[] {
110
+ const argv = ["opencode", "--model", opencodeModel(opts.model)];
111
+ if (opts.initialMessage && opts.initialMessage.length > 0) {
112
+ argv.push("--prompt", opts.initialMessage);
113
+ }
114
+ return argv;
115
+ }
116
+
117
+ /**
118
+ * Provider env vars for the resolved model, plus `OPENCODE_PERMISSION` so an
119
+ * unattended turn auto-approves tool actions instead of deadlocking on a
120
+ * permission prompt (OpenCode's robust, version-stable bypass mechanism). The
121
+ * caller decides whether to apply this: the headless turn-runner always does
122
+ * (workers are unattended); the interactive coordinator applies it only in
123
+ * auto/bypass mode so `--safe` keeps OpenCode's in-TUI approval prompts.
124
+ */
125
+ buildEnv(model: ResolvedModel): Record<string, string> {
126
+ return { ...(model.env ?? {}), OPENCODE_PERMISSION: OPENCODE_BYPASS_PERMISSION };
127
+ }
128
+
129
+ /**
130
+ * Build argv for a one-shot, non-streaming call (`opencode run --format text`).
131
+ * The model is appended only when provided; the prompt is the trailing
132
+ * positional.
133
+ */
134
+ buildPrintCommand(prompt: string, model?: string): string[] {
135
+ const argv = ["opencode", "run", "--format", "text"];
136
+ if (model !== undefined) {
137
+ argv.push("--model", opencodeModel(model));
138
+ }
139
+ argv.push(prompt);
140
+ return argv;
141
+ }
142
+
143
+ /**
144
+ * Parse OpenCode's `--format json` stdout into normalized {@link AgentEvent}s.
145
+ *
146
+ * The stream is JSONL: one JSON object per line (`{ type, sessionID, … }`), but
147
+ * pipe chunk boundaries do NOT align to newlines, so we keep a `buffer` of the
148
+ * trailing partial line across reads and only parse once a `\n` completes it.
149
+ * Malformed lines are skipped so a partial flush never aborts the turn.
150
+ */
151
+ async *parseEvents(stream: ReadableStream<Uint8Array>): AsyncIterable<AgentEvent> {
152
+ const reader = stream.getReader();
153
+ const decoder = new TextDecoder();
154
+ let buffer = "";
155
+
156
+ try {
157
+ while (true) {
158
+ const { done, value } = await reader.read();
159
+ if (done) break;
160
+ buffer += decoder.decode(value, { stream: true });
161
+
162
+ const lines = buffer.split("\n");
163
+ buffer = lines.pop() ?? "";
164
+
165
+ for (const line of lines) {
166
+ const event = parseOpenCodeLine(line);
167
+ if (event) yield event;
168
+ }
169
+ }
170
+
171
+ const tail = parseOpenCodeLine(buffer);
172
+ if (tail) yield tail;
173
+ } finally {
174
+ reader.releaseLock();
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Parse a single `opencode run --format json` line into an {@link AgentEvent}, or
181
+ * `null` for a blank/unparseable line. OpenCode events are `{ type, timestamp,
182
+ * sessionID, … }`; we pass the `type` through (the feed labels any type), capture
183
+ * the session id for `--session`, and best-effort lift a tool name.
184
+ */
185
+ function parseOpenCodeLine(line: string): AgentEvent | null {
186
+ const trimmed = line.trim();
187
+ if (!trimmed) return null;
188
+
189
+ let raw: unknown;
190
+ try {
191
+ raw = JSON.parse(trimmed);
192
+ } catch {
193
+ return null;
194
+ }
195
+
196
+ if (typeof raw !== "object" || raw === null) return null;
197
+ const msg = raw as Record<string, unknown>;
198
+
199
+ const event: AgentEvent = {
200
+ type: typeof msg.type === "string" ? msg.type : "unknown",
201
+ raw,
202
+ };
203
+
204
+ for (const candidate of [msg.sessionID, msg.sessionId, msg.session_id]) {
205
+ if (typeof candidate === "string" && candidate.length > 0) {
206
+ event.sessionId = candidate;
207
+ break;
208
+ }
209
+ }
210
+
211
+ const tool = extractOpenCodeTool(msg);
212
+ if (tool !== undefined) event.tool = tool;
213
+
214
+ // Surface the failure reason on error events (e.g. "Model not found: …") so a
215
+ // failed agent shows WHY in the feed/logs instead of a blank "error".
216
+ if (typeof msg.error === "object" && msg.error !== null) {
217
+ const err = msg.error as Record<string, unknown>;
218
+ const data = (typeof err.data === "object" && err.data !== null ? err.data : {}) as Record<
219
+ string,
220
+ unknown
221
+ >;
222
+ const message =
223
+ (typeof data.message === "string" && data.message) ||
224
+ (typeof err.message === "string" && err.message) ||
225
+ (typeof err.name === "string" && err.name) ||
226
+ undefined;
227
+ if (message) event.error = message;
228
+ }
229
+
230
+ return event;
231
+ }
232
+
233
+ /**
234
+ * Best-effort tool name from an OpenCode event. Tool activity surfaces either as a
235
+ * top-level `tool` field or nested in a `part`/`properties.part` of type "tool".
236
+ * Returns `undefined` when no tool is named.
237
+ */
238
+ function extractOpenCodeTool(msg: Record<string, unknown>): string | undefined {
239
+ if (typeof msg.tool === "string") return msg.tool;
240
+
241
+ const part = (() => {
242
+ if (typeof msg.part === "object" && msg.part !== null)
243
+ return msg.part as Record<string, unknown>;
244
+ const props = msg.properties;
245
+ if (typeof props === "object" && props !== null) {
246
+ const p = (props as Record<string, unknown>).part;
247
+ if (typeof p === "object" && p !== null) return p as Record<string, unknown>;
248
+ }
249
+ return null;
250
+ })();
251
+ if (part && part.type === "tool") {
252
+ if (typeof part.tool === "string") return part.tool;
253
+ if (typeof part.name === "string") return part.name;
254
+ }
255
+ return undefined;
256
+ }
257
+
258
+ /** Singleton for callers that do not need dependency injection. */
259
+ export const openCodeRuntime = new OpenCodeRuntime();