@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.
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/agents/architect.md +108 -0
- package/agents/builder.md +97 -0
- package/agents/coordinator.md +113 -0
- package/agents/deployer.md +117 -0
- package/agents/devops.md +114 -0
- package/agents/lead.md +107 -0
- package/agents/merger.md +103 -0
- package/agents/reviewer.md +90 -0
- package/agents/scout.md +95 -0
- package/agents/verifier.md +106 -0
- package/package.json +64 -0
- package/src/agents/guard-rules.ts +55 -0
- package/src/agents/identity.test.ts +161 -0
- package/src/agents/identity.ts +229 -0
- package/src/agents/manifest.test.ts +260 -0
- package/src/agents/manifest.ts +286 -0
- package/src/agents/overlay.test.ts +190 -0
- package/src/agents/overlay.ts +212 -0
- package/src/agents/system-prompt.test.ts +53 -0
- package/src/agents/system-prompt.ts +95 -0
- package/src/agents/turn-runner.ts +79 -0
- package/src/commands/coordinator.test.ts +75 -0
- package/src/commands/coordinator.ts +259 -0
- package/src/commands/deploy.test.ts +504 -0
- package/src/commands/deploy.ts +874 -0
- package/src/commands/doctor.test.ts +106 -0
- package/src/commands/doctor.ts +208 -0
- package/src/commands/init.ts +71 -0
- package/src/commands/log.ts +51 -0
- package/src/commands/mail.ts +197 -0
- package/src/commands/merge.ts +127 -0
- package/src/commands/model.ts +58 -0
- package/src/commands/prime.ts +61 -0
- package/src/commands/reap.ts +87 -0
- package/src/commands/serve.ts +61 -0
- package/src/commands/setup.ts +48 -0
- package/src/commands/ship.test.ts +106 -0
- package/src/commands/ship.ts +202 -0
- package/src/commands/skill.test.ts +458 -0
- package/src/commands/skill.ts +730 -0
- package/src/commands/sling.ts +365 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/stop.ts +56 -0
- package/src/commands/tui.ts +199 -0
- package/src/commands/worktree.ts +77 -0
- package/src/config.test.ts +92 -0
- package/src/config.ts +202 -0
- package/src/db/sqlite.test.ts +77 -0
- package/src/db/sqlite.ts +102 -0
- package/src/deploy/audit.test.ts +233 -0
- package/src/deploy/audit.ts +245 -0
- package/src/deploy/context.test.ts +243 -0
- package/src/deploy/context.ts +72 -0
- package/src/deploy/registry.test.ts +101 -0
- package/src/deploy/registry.ts +86 -0
- package/src/deploy/secrets.test.ts +129 -0
- package/src/deploy/secrets.ts +69 -0
- package/src/deploy/targets/docker-gha.test.ts +323 -0
- package/src/deploy/targets/docker-gha.ts +841 -0
- package/src/deploy/types.ts +153 -0
- package/src/errors.test.ts +42 -0
- package/src/errors.ts +69 -0
- package/src/events/store.test.ts +183 -0
- package/src/events/store.ts +201 -0
- package/src/index.ts +137 -0
- package/src/insights/quality-gates.ts +73 -0
- package/src/json.test.ts +28 -0
- package/src/json.ts +50 -0
- package/src/logging/color.ts +62 -0
- package/src/logging/logger.ts +60 -0
- package/src/logging/sanitizer.test.ts +36 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/client.test.ts +192 -0
- package/src/mail/client.ts +188 -0
- package/src/mail/store.test.ts +279 -0
- package/src/mail/store.ts +311 -0
- package/src/merge/lock.test.ts +88 -0
- package/src/merge/lock.ts +84 -0
- package/src/merge/queue.test.ts +136 -0
- package/src/merge/queue.ts +177 -0
- package/src/merge/resolver.test.ts +219 -0
- package/src/merge/resolver.ts +274 -0
- package/src/paths.ts +36 -0
- package/src/providers/apply.test.ts +90 -0
- package/src/providers/apply.ts +66 -0
- package/src/providers/registry.test.ts +74 -0
- package/src/providers/registry.ts +254 -0
- package/src/runtimes/claude.ts +313 -0
- package/src/runtimes/codex.ts +280 -0
- package/src/runtimes/cursor.ts +247 -0
- package/src/runtimes/gemini.ts +173 -0
- package/src/runtimes/mock.ts +71 -0
- package/src/runtimes/opencode.ts +259 -0
- package/src/runtimes/registry.test.ts +924 -0
- package/src/runtimes/registry.ts +63 -0
- package/src/runtimes/resolve.ts +45 -0
- package/src/runtimes/types.ts +97 -0
- package/src/scaffold.ts +68 -0
- package/src/secrets.test.ts +51 -0
- package/src/secrets.ts +78 -0
- package/src/serve/api.ts +667 -0
- package/src/serve/server.test.ts +433 -0
- package/src/serve/server.ts +271 -0
- package/src/serve/system.ts +90 -0
- package/src/serve/weather.ts +140 -0
- package/src/sessions/reaper.test.ts +162 -0
- package/src/sessions/reaper.ts +149 -0
- package/src/sessions/store.test.ts +351 -0
- package/src/sessions/store.ts +350 -0
- package/src/skills/distiller.test.ts +498 -0
- package/src/skills/distiller.ts +426 -0
- package/src/skills/feedback.test.ts +300 -0
- package/src/skills/feedback.ts +168 -0
- package/src/skills/lifecycle.ts +169 -0
- package/src/skills/retrieval.test.ts +421 -0
- package/src/skills/retrieval.ts +365 -0
- package/src/skills/safety.test.ts +335 -0
- package/src/skills/safety.ts +216 -0
- package/src/skills/store.test.ts +425 -0
- package/src/skills/store.ts +684 -0
- package/src/skills/types.ts +107 -0
- package/src/types.ts +442 -0
- package/src/utils/detect.test.ts +35 -0
- package/src/utils/detect.ts +82 -0
- package/src/version.test.ts +19 -0
- package/src/version.ts +7 -0
- package/src/wizard/setup.ts +254 -0
- package/src/worktree/manager.test.ts +181 -0
- package/src/worktree/manager.ts +229 -0
- package/templates/overlay.md.tmpl +102 -0
- package/ui/dist/assets/index-C7rXIMER.css +1 -0
- package/ui/dist/assets/index-W4kbr4by.js +4526 -0
- package/ui/dist/favicon.svg +21 -0
- package/ui/dist/index.html +16 -0
- package/ui/dist/logo-clay.svg +21 -0
- 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();
|