@geteigen/cli 0.0.0-stage → 3.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 (168) hide show
  1. package/README.md +154 -2
  2. package/dist/agent.d.ts +32 -0
  3. package/dist/agent.js +246 -0
  4. package/dist/auth.d.ts +5 -0
  5. package/dist/auth.js +33 -0
  6. package/dist/cli/src/agent.d.ts +24 -0
  7. package/dist/cli/src/agent.js +260 -0
  8. package/dist/cli/src/agentTools.d.ts +10 -0
  9. package/dist/cli/src/agentTools.js +70 -0
  10. package/dist/cli/src/auth.d.ts +5 -0
  11. package/dist/cli/src/auth.js +33 -0
  12. package/dist/cli/src/config.d.ts +11 -0
  13. package/dist/cli/src/config.js +51 -0
  14. package/dist/cli/src/cursorAuth.d.ts +10 -0
  15. package/dist/cli/src/cursorAuth.js +92 -0
  16. package/dist/cli/src/index.d.ts +2 -0
  17. package/dist/cli/src/index.js +244 -0
  18. package/dist/cli/src/listModels.d.ts +14 -0
  19. package/dist/cli/src/listModels.js +196 -0
  20. package/dist/cli/src/listSessions.d.ts +9 -0
  21. package/dist/cli/src/listSessions.js +37 -0
  22. package/dist/cli/src/machineConfig.d.ts +20 -0
  23. package/dist/cli/src/machineConfig.js +66 -0
  24. package/dist/cli/src/pickFolder.d.ts +5 -0
  25. package/dist/cli/src/pickFolder.js +65 -0
  26. package/dist/cli/src/proc.d.ts +23 -0
  27. package/dist/cli/src/proc.js +77 -0
  28. package/dist/cli/src/service.d.ts +17 -0
  29. package/dist/cli/src/service.js +246 -0
  30. package/dist/cli/src/workRoots.d.ts +30 -0
  31. package/dist/cli/src/workRoots.js +221 -0
  32. package/dist/cli/src/worker.d.ts +8 -0
  33. package/dist/cli/src/worker.js +51 -0
  34. package/dist/cli/src/workerCore.d.ts +47 -0
  35. package/dist/cli/src/workerCore.js +702 -0
  36. package/dist/cli/src/workspace.d.ts +8 -0
  37. package/dist/cli/src/workspace.js +269 -0
  38. package/dist/config.d.ts +11 -0
  39. package/dist/config.js +51 -0
  40. package/dist/cursorAuth.d.ts +10 -0
  41. package/dist/cursorAuth.js +95 -0
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.js +108 -0
  44. package/dist/listModels.d.ts +6 -0
  45. package/dist/listModels.js +62 -0
  46. package/dist/listSessions.d.ts +9 -0
  47. package/dist/listSessions.js +37 -0
  48. package/dist/pickFolder.d.ts +5 -0
  49. package/dist/pickFolder.js +65 -0
  50. package/dist/shared/actionDrafts.d.ts +83 -0
  51. package/dist/shared/actionDrafts.js +169 -0
  52. package/dist/shared/agentArtifacts.d.ts +49 -0
  53. package/dist/shared/agentArtifacts.js +169 -0
  54. package/dist/shared/agentLabel.d.ts +18 -0
  55. package/dist/shared/agentLabel.js +22 -0
  56. package/dist/shared/agentMentions.d.ts +38 -0
  57. package/dist/shared/agentMentions.js +110 -0
  58. package/dist/shared/agentRepos.d.ts +28 -0
  59. package/dist/shared/agentRepos.js +60 -0
  60. package/dist/shared/agentRunStatus.d.ts +12 -0
  61. package/dist/shared/agentRunStatus.js +17 -0
  62. package/dist/shared/agentUsage.d.ts +24 -0
  63. package/dist/shared/agentUsage.js +139 -0
  64. package/dist/shared/approvals.d.ts +44 -0
  65. package/dist/shared/approvals.js +97 -0
  66. package/dist/shared/askEigen.d.ts +190 -0
  67. package/dist/shared/askEigen.js +76 -0
  68. package/dist/shared/attribution.d.ts +25 -0
  69. package/dist/shared/attribution.js +63 -0
  70. package/dist/shared/automations.d.ts +136 -0
  71. package/dist/shared/automations.js +546 -0
  72. package/dist/shared/backends/claudeCode.d.ts +42 -0
  73. package/dist/shared/backends/claudeCode.js +540 -0
  74. package/dist/shared/backends/codex.d.ts +15 -0
  75. package/dist/shared/backends/codex.js +287 -0
  76. package/dist/shared/backends/cursor.d.ts +102 -0
  77. package/dist/shared/backends/cursor.js +1197 -0
  78. package/dist/shared/backends/index.d.ts +18 -0
  79. package/dist/shared/backends/index.js +58 -0
  80. package/dist/shared/backends/opencode.d.ts +40 -0
  81. package/dist/shared/backends/opencode.js +397 -0
  82. package/dist/shared/backends/types.d.ts +171 -0
  83. package/dist/shared/backends/types.js +19 -0
  84. package/dist/shared/backends/usage.d.ts +8 -0
  85. package/dist/shared/backends/usage.js +71 -0
  86. package/dist/shared/billingPlans.d.ts +60 -0
  87. package/dist/shared/billingPlans.js +262 -0
  88. package/dist/shared/brain.d.ts +353 -0
  89. package/dist/shared/brain.js +487 -0
  90. package/dist/shared/clarifyingQuestions.d.ts +13 -0
  91. package/dist/shared/clarifyingQuestions.js +207 -0
  92. package/dist/shared/claudeModels.d.ts +10 -0
  93. package/dist/shared/claudeModels.js +52 -0
  94. package/dist/shared/cloudBackends.d.ts +21 -0
  95. package/dist/shared/cloudBackends.js +29 -0
  96. package/dist/shared/codexModels.d.ts +10 -0
  97. package/dist/shared/codexModels.js +28 -0
  98. package/dist/shared/contextWindows.d.ts +2 -0
  99. package/dist/shared/contextWindows.js +30 -0
  100. package/dist/shared/cursorMcpConfig.d.ts +14 -0
  101. package/dist/shared/cursorMcpConfig.js +43 -0
  102. package/dist/shared/cursorProjectMcp.d.ts +10 -0
  103. package/dist/shared/cursorProjectMcp.js +42 -0
  104. package/dist/shared/docs.d.ts +59 -0
  105. package/dist/shared/docs.js +87 -0
  106. package/dist/shared/events.d.ts +929 -0
  107. package/dist/shared/events.js +59 -0
  108. package/dist/shared/integrations.d.ts +266 -0
  109. package/dist/shared/integrations.js +449 -0
  110. package/dist/shared/issueTranscript.d.ts +64 -0
  111. package/dist/shared/issueTranscript.js +387 -0
  112. package/dist/shared/issues.d.ts +139 -0
  113. package/dist/shared/issues.js +217 -0
  114. package/dist/shared/machines.d.ts +115 -0
  115. package/dist/shared/machines.js +58 -0
  116. package/dist/shared/markdownMedia.d.ts +13 -0
  117. package/dist/shared/markdownMedia.js +41 -0
  118. package/dist/shared/notificationText.d.ts +6 -0
  119. package/dist/shared/notificationText.js +38 -0
  120. package/dist/shared/opencodeModels.d.ts +19 -0
  121. package/dist/shared/opencodeModels.js +62 -0
  122. package/dist/shared/opencodeSession.d.ts +7 -0
  123. package/dist/shared/opencodeSession.js +30 -0
  124. package/dist/shared/orchestration.d.ts +58 -0
  125. package/dist/shared/orchestration.js +12 -0
  126. package/dist/shared/orgs.d.ts +47 -0
  127. package/dist/shared/orgs.js +49 -0
  128. package/dist/shared/personalOrg.d.ts +5 -0
  129. package/dist/shared/personalOrg.js +12 -0
  130. package/dist/shared/plans.d.ts +3 -0
  131. package/dist/shared/plans.js +19 -0
  132. package/dist/shared/providerModels.d.ts +40 -0
  133. package/dist/shared/providerModels.js +503 -0
  134. package/dist/shared/roomAttention.d.ts +17 -0
  135. package/dist/shared/roomAttention.js +36 -0
  136. package/dist/shared/roomContext.d.ts +120 -0
  137. package/dist/shared/roomContext.js +133 -0
  138. package/dist/shared/roomPermissions.d.ts +56 -0
  139. package/dist/shared/roomPermissions.js +133 -0
  140. package/dist/shared/sessionSeats.d.ts +29 -0
  141. package/dist/shared/sessionSeats.js +57 -0
  142. package/dist/shared/slashCommands.d.ts +39 -0
  143. package/dist/shared/slashCommands.js +166 -0
  144. package/dist/shared/space.d.ts +33 -0
  145. package/dist/shared/space.js +54 -0
  146. package/dist/shared/spaceFeed.d.ts +78 -0
  147. package/dist/shared/spaceFeed.js +194 -0
  148. package/dist/shared/stringifyUnknown.d.ts +1 -0
  149. package/dist/shared/stringifyUnknown.js +1 -0
  150. package/dist/shared/swarm.d.ts +320 -0
  151. package/dist/shared/swarm.js +328 -0
  152. package/dist/shared/swarmVisual.d.ts +79 -0
  153. package/dist/shared/swarmVisual.js +212 -0
  154. package/dist/shared/terminalFilter.d.ts +13 -0
  155. package/dist/shared/terminalFilter.js +46 -0
  156. package/dist/shared/toolIntegration.d.ts +56 -0
  157. package/dist/shared/toolIntegration.js +180 -0
  158. package/dist/shared/typing.d.ts +9 -0
  159. package/dist/shared/typing.js +37 -0
  160. package/dist/shared/uploads.d.ts +31 -0
  161. package/dist/shared/uploads.js +65 -0
  162. package/dist/shared/voiceApprovals.d.ts +36 -0
  163. package/dist/shared/voiceApprovals.js +229 -0
  164. package/dist/shared/workspace.d.ts +47 -0
  165. package/dist/shared/workspace.js +40 -0
  166. package/dist/worker.d.ts +1 -0
  167. package/dist/worker.js +266 -0
  168. package/package.json +51 -4
@@ -0,0 +1,18 @@
1
+ import type { AgentBackendKind, WorkerBackend } from "./types.js";
2
+ import { ClaudeCodeBackend, claudeCodeBackend } from "./claudeCode.js";
3
+ import { CodexBackend, codexBackend } from "./codex.js";
4
+ export type { AgentBackendKind, BuildArgsOptions, CliSandboxBackendKind, NormalizedAgentEvent, ParseLineContext, RunGitInfo, RunUsage, WorkerBackend, } from "./types.js";
5
+ export { EMPTY_RUN_USAGE, addRunUsage, runUsageCostUsd } from "./usage.js";
6
+ export { CLI_SANDBOX_BACKENDS, backendShortLabel, isCliSandboxBackend, parseAgentBackendKind, } from "./types.js";
7
+ export { CursorAgentBackend, cursorAgentBackend, extractThinkingText, isEditTool, mergeAssistantText, mergeThinkingText, normalizeThinkingText, diffFromToolArgs, diffFromToolResult, diffFromToolEvent, formatToolResultDetail, formatToolTranscript, mergeToolStartAndResult, unwrapToolResultPayload, } from "./cursor.js";
8
+ export { ClaudeCodeBackend, claudeCodeBackend };
9
+ export { CodexBackend, codexBackend };
10
+ export { OpencodeBackend, opencodeBackend, opencodeToolName } from "./opencode.js";
11
+ /**
12
+ * Return a backend suitable for one agent run.
13
+ * Claude Code parsers are stateful (pending tool_use ids), so each call
14
+ * for `claude-code` returns a fresh instance.
15
+ */
16
+ export declare function getBackend(kind: AgentBackendKind): WorkerBackend;
17
+ export declare function listBackends(): WorkerBackend[];
18
+ export declare function isBackendAvailable(kind: AgentBackendKind): boolean;
@@ -0,0 +1,58 @@
1
+ import { cursorAgentBackend } from "./cursor.js";
2
+ import { ClaudeCodeBackend, claudeCodeBackend } from "./claudeCode.js";
3
+ import { CodexBackend, codexBackend } from "./codex.js";
4
+ import { OpencodeBackend, opencodeBackend } from "./opencode.js";
5
+ import { cursorStreamLineIntegration } from "../toolIntegration.js";
6
+ export { EMPTY_RUN_USAGE, addRunUsage, runUsageCostUsd } from "./usage.js";
7
+ export { CLI_SANDBOX_BACKENDS, backendShortLabel, isCliSandboxBackend, parseAgentBackendKind, } from "./types.js";
8
+ export { CursorAgentBackend, cursorAgentBackend, extractThinkingText, isEditTool, mergeAssistantText, mergeThinkingText, normalizeThinkingText, diffFromToolArgs, diffFromToolResult, diffFromToolEvent, formatToolResultDetail, formatToolTranscript, mergeToolStartAndResult, unwrapToolResultPayload, } from "./cursor.js";
9
+ export { ClaudeCodeBackend, claudeCodeBackend };
10
+ export { CodexBackend, codexBackend };
11
+ export { OpencodeBackend, opencodeBackend, opencodeToolName } from "./opencode.js";
12
+ /**
13
+ * Cursor parser + MCP integration tags. Tagging lives here, not in cursor.ts,
14
+ * because the web bundle imports cursor.ts and can't follow its runtime
15
+ * `.js` imports.
16
+ */
17
+ const taggedCursorBackend = {
18
+ kind: cursorAgentBackend.kind,
19
+ available: cursorAgentBackend.available,
20
+ command: cursorAgentBackend.command,
21
+ buildArgs: (opts) => cursorAgentBackend.buildArgs(opts),
22
+ parseLine(json, ctx) {
23
+ const events = cursorAgentBackend.parseLine(json, ctx);
24
+ const integration = cursorStreamLineIntegration(json);
25
+ if (!integration)
26
+ return events;
27
+ return events.map((event) => event.kind === "tool_start" || event.kind === "tool_done"
28
+ ? { ...event, integration }
29
+ : event);
30
+ },
31
+ };
32
+ /** Singletons for availability checks / UI. Cursor is stateless; Claude is not. */
33
+ const prototypes = {
34
+ cursor: taggedCursorBackend,
35
+ "claude-code": claudeCodeBackend,
36
+ codex: codexBackend,
37
+ opencode: opencodeBackend,
38
+ };
39
+ /**
40
+ * Return a backend suitable for one agent run.
41
+ * Claude Code parsers are stateful (pending tool_use ids), so each call
42
+ * for `claude-code` returns a fresh instance.
43
+ */
44
+ export function getBackend(kind) {
45
+ if (kind === "claude-code")
46
+ return new ClaudeCodeBackend();
47
+ if (kind === "codex")
48
+ return new CodexBackend();
49
+ if (kind === "opencode")
50
+ return new OpencodeBackend();
51
+ return prototypes[kind] ?? taggedCursorBackend;
52
+ }
53
+ export function listBackends() {
54
+ return Object.values(prototypes);
55
+ }
56
+ export function isBackendAvailable(kind) {
57
+ return Boolean(prototypes[kind]?.available);
58
+ }
@@ -0,0 +1,40 @@
1
+ import type { BuildArgsOptions, NormalizedAgentEvent, ParseLineContext, WorkerBackend } from "./types.js";
2
+ /** Map OpenCode's lowercase built-ins to the names the chat UI renders. */
3
+ export declare function opencodeToolName(raw: string): string;
4
+ /**
5
+ * OpenCode CLI headless backend.
6
+ * Spawns: `opencode run --format json --thinking …`
7
+ *
8
+ * Emits one NDJSON object per line with a `part` payload. Text/reasoning can
9
+ * arrive as either full part snapshots (`text`, `reasoning`) or incremental
10
+ * `part_delta` events depending on the CLI version, so both are handled and
11
+ * de-duplicated per part id. Tool calls arrive once, already completed.
12
+ */
13
+ export declare class OpencodeBackend implements WorkerBackend {
14
+ readonly kind: "opencode";
15
+ readonly available = true;
16
+ readonly command = "opencode";
17
+ private sessionEmitted;
18
+ private usage;
19
+ private textSent;
20
+ private thinkingSent;
21
+ private startedTools;
22
+ private doneTools;
23
+ private anonSeq;
24
+ /**
25
+ * A `stop` step-finish can belong to the compaction summary, after which the
26
+ * same turn continues. Hold it until the stream goes quiet (flush) instead of
27
+ * ending the reply as soon as it arrives.
28
+ */
29
+ private pendingDone;
30
+ /** A held `done` was flushed; clear the terminal flag if the turn resumes. */
31
+ private deferredTerminal;
32
+ buildArgs(opts: BuildArgsOptions): string[];
33
+ /** OpenCode takes MCP servers through its merged config, not a CLI flag. */
34
+ buildEnv(opts: BuildArgsOptions): Record<string, string>;
35
+ private appendText;
36
+ private parseTool;
37
+ parseLine(json: unknown, ctx?: ParseLineContext): NormalizedAgentEvent[];
38
+ flush(ctx?: ParseLineContext): NormalizedAgentEvent[];
39
+ }
40
+ export declare const opencodeBackend: OpencodeBackend;
@@ -0,0 +1,397 @@
1
+ import { opencodeToolIntegration } from "../toolIntegration.js";
2
+ import { formatToolResultDetail, todosFromToolArgs, todoStatusSummary, } from "./cursor.js";
3
+ import { stringifyUnknown } from "../stringifyUnknown.js";
4
+ const TOOL_NAMES = {
5
+ bash: "Bash",
6
+ shell: "Bash",
7
+ read: "Read",
8
+ write: "Write",
9
+ edit: "Edit",
10
+ multiedit: "Edit",
11
+ patch: "Edit",
12
+ apply_patch: "Edit",
13
+ list: "List",
14
+ ls: "List",
15
+ glob: "Glob",
16
+ grep: "Grep",
17
+ webfetch: "WebFetch",
18
+ websearch: "WebSearch",
19
+ task: "Task",
20
+ todowrite: "todo",
21
+ todoread: "todo",
22
+ skill: "Skill",
23
+ };
24
+ /** Map OpenCode's lowercase built-ins to the names the chat UI renders. */
25
+ export function opencodeToolName(raw) {
26
+ const name = raw.trim();
27
+ if (!name)
28
+ return "Tool";
29
+ return TOOL_NAMES[name.toLowerCase()] || name;
30
+ }
31
+ function asRecord(value) {
32
+ return value && typeof value === "object" && !Array.isArray(value)
33
+ ? value
34
+ : undefined;
35
+ }
36
+ function textField(value) {
37
+ return typeof value === "string" ? value : "";
38
+ }
39
+ function toolPath(args) {
40
+ if (!args)
41
+ return undefined;
42
+ for (const key of ["filePath", "file_path", "path", "targetFile", "target_file"]) {
43
+ const value = args[key];
44
+ if (typeof value === "string" && value.trim())
45
+ return value.trim();
46
+ }
47
+ return undefined;
48
+ }
49
+ function toolDetail(name, args, path, title) {
50
+ if (title.trim())
51
+ return title.trim();
52
+ if (args) {
53
+ const command = typeof args.command === "string"
54
+ ? args.command
55
+ : typeof args.pattern === "string"
56
+ ? args.pattern
57
+ : typeof args.glob === "string"
58
+ ? args.glob
59
+ : typeof args.query === "string"
60
+ ? args.query
61
+ : typeof args.url === "string"
62
+ ? args.url
63
+ : undefined;
64
+ if (command)
65
+ return command;
66
+ }
67
+ if (path)
68
+ return path;
69
+ if (args) {
70
+ try {
71
+ return JSON.stringify(args).slice(0, 120);
72
+ }
73
+ catch {
74
+ // fall through
75
+ }
76
+ }
77
+ return name;
78
+ }
79
+ function toolError(state) {
80
+ const error = state.error;
81
+ if (typeof error === "string" && error.trim())
82
+ return error.trim();
83
+ const rec = asRecord(error);
84
+ if (rec) {
85
+ const data = asRecord(rec.data);
86
+ const message = textField(rec.message) ||
87
+ textField(data?.message) ||
88
+ textField(rec.data);
89
+ if (message.trim())
90
+ return message.trim();
91
+ }
92
+ return "";
93
+ }
94
+ function resultText(state) {
95
+ const error = toolError(state);
96
+ if (error)
97
+ return error;
98
+ const output = state.output;
99
+ if (typeof output === "string")
100
+ return output;
101
+ if (output !== undefined)
102
+ return stringifyUnknown(output);
103
+ return "";
104
+ }
105
+ function stepUsage(part) {
106
+ const tokens = asRecord(part.tokens);
107
+ const cost = Number(part.cost);
108
+ if (!tokens && !Number.isFinite(cost))
109
+ return {};
110
+ const cache = asRecord(tokens?.cache);
111
+ const n = (value) => {
112
+ const num = typeof value === "number" ? value : Number(value);
113
+ return Number.isFinite(num) && num > 0 ? num : 0;
114
+ };
115
+ return {
116
+ usage: {
117
+ inputTokens: n(tokens?.input),
118
+ outputTokens: n(tokens?.output),
119
+ cacheReadTokens: n(cache?.read),
120
+ cacheWriteTokens: n(cache?.write),
121
+ ...(Number.isFinite(cost) && cost > 0 ? { costUsd: cost } : {}),
122
+ },
123
+ };
124
+ }
125
+ /**
126
+ * OpenCode CLI headless backend.
127
+ * Spawns: `opencode run --format json --thinking …`
128
+ *
129
+ * Emits one NDJSON object per line with a `part` payload. Text/reasoning can
130
+ * arrive as either full part snapshots (`text`, `reasoning`) or incremental
131
+ * `part_delta` events depending on the CLI version, so both are handled and
132
+ * de-duplicated per part id. Tool calls arrive once, already completed.
133
+ */
134
+ export class OpencodeBackend {
135
+ kind = "opencode";
136
+ available = true;
137
+ command = "opencode";
138
+ sessionEmitted = false;
139
+ usage = {
140
+ inputTokens: 0,
141
+ outputTokens: 0,
142
+ cacheReadTokens: 0,
143
+ cacheWriteTokens: 0,
144
+ costUsd: 0,
145
+ };
146
+ textSent = new Map();
147
+ thinkingSent = new Map();
148
+ startedTools = new Set();
149
+ doneTools = new Set();
150
+ anonSeq = 0;
151
+ /**
152
+ * A `stop` step-finish can belong to the compaction summary, after which the
153
+ * same turn continues. Hold it until the stream goes quiet (flush) instead of
154
+ * ending the reply as soon as it arrives.
155
+ */
156
+ pendingDone = null;
157
+ /** A held `done` was flushed; clear the terminal flag if the turn resumes. */
158
+ deferredTerminal = false;
159
+ buildArgs(opts) {
160
+ const args = ["run", "--format", "json", "--thinking"];
161
+ if (opts.mode === "plan") {
162
+ args.push("--agent", "plan");
163
+ }
164
+ else {
165
+ args.push("--auto");
166
+ }
167
+ if (opts.modelId && opts.modelId !== "auto") {
168
+ args.push("--model", opts.modelId);
169
+ }
170
+ if (opts.sessionId) {
171
+ args.push("--session", opts.sessionId);
172
+ }
173
+ args.push("--", opts.prompt);
174
+ return args;
175
+ }
176
+ /** OpenCode takes MCP servers through its merged config, not a CLI flag. */
177
+ buildEnv(opts) {
178
+ const servers = opts.mcpServers;
179
+ if (!servers || !Object.keys(servers).length)
180
+ return {};
181
+ const mcp = {};
182
+ for (const [name, server] of Object.entries(servers)) {
183
+ mcp[name] = {
184
+ type: "remote",
185
+ url: server.url,
186
+ headers: server.headers,
187
+ enabled: true,
188
+ };
189
+ }
190
+ return {
191
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
192
+ $schema: "https://opencode.ai/config.json",
193
+ mcp,
194
+ }),
195
+ };
196
+ }
197
+ appendText(ctx, partId, incoming, kind, full) {
198
+ const sentMap = kind === "text" ? this.textSent : this.thinkingSent;
199
+ const buffer = kind === "text" ? ctx.assistantBuf : (ctx.thinkingBuf ??= { value: "" });
200
+ const already = sentMap.get(partId) || "";
201
+ let chunk = "";
202
+ if (!incoming)
203
+ return "";
204
+ if (full) {
205
+ if (incoming.startsWith(already))
206
+ chunk = incoming.slice(already.length);
207
+ else if (already && already.startsWith(incoming))
208
+ chunk = "";
209
+ else
210
+ chunk = incoming;
211
+ sentMap.set(partId, incoming);
212
+ }
213
+ else {
214
+ chunk = incoming;
215
+ sentMap.set(partId, already + incoming);
216
+ }
217
+ if (!chunk)
218
+ return "";
219
+ // A new part (e.g. text after a tool step) starts its own paragraph.
220
+ if (!already && buffer.value && !/\s$/.test(buffer.value) && !/^\s/.test(chunk)) {
221
+ buffer.value += "\n\n";
222
+ }
223
+ buffer.value += chunk;
224
+ return buffer.value;
225
+ }
226
+ parseTool(ev, part, ctx) {
227
+ const out = [];
228
+ const rawName = textField(part.tool) || "tool";
229
+ const name = opencodeToolName(rawName);
230
+ const callId = textField(part.callID) ||
231
+ textField(part.id) ||
232
+ `opencode-tool-${(this.anonSeq += 1)}`;
233
+ const state = asRecord(part.state) || {};
234
+ const args = asRecord(state.input);
235
+ const path = toolPath(args);
236
+ const status = textField(state.status).toLowerCase();
237
+ const title = textField(state.title);
238
+ const detail = toolDetail(name, args, path, title);
239
+ const todos = todosFromToolArgs(args);
240
+ const integration = opencodeToolIntegration(rawName, args);
241
+ const error = toolError(state);
242
+ const done = status === "completed" || status === "error" || status === "failed";
243
+ if (!this.startedTools.has(callId)) {
244
+ this.startedTools.add(callId);
245
+ out.push({
246
+ kind: "tool_start",
247
+ callId,
248
+ name,
249
+ detail: todos.length > 0
250
+ ? `${todos.length} todo${todos.length === 1 ? "" : "s"} · ${todoStatusSummary(todos)}`
251
+ : detail,
252
+ ...(path ? { path } : {}),
253
+ ...(todos.length ? { todos } : {}),
254
+ ...(integration ? { integration } : {}),
255
+ });
256
+ }
257
+ if (done && !this.doneTools.has(callId)) {
258
+ this.doneTools.add(callId);
259
+ const result = resultText(state);
260
+ out.push({
261
+ kind: "tool_done",
262
+ callId,
263
+ name,
264
+ detail: todos.length > 0
265
+ ? `${todos.length} todo${todos.length === 1 ? "" : "s"} · ${todoStatusSummary(todos)}`
266
+ : formatToolResultDetail(name, args, result || (error ? "Failed" : "Done"), error ? "Failed" : "Done"),
267
+ ...(path ? { path } : {}),
268
+ ...(status !== "completed" ? { isError: true } : {}),
269
+ ...(todos.length ? { todos } : {}),
270
+ ...(integration ? { integration } : {}),
271
+ });
272
+ }
273
+ return out;
274
+ }
275
+ parseLine(json, ctx = {
276
+ assistantBuf: { value: "" },
277
+ gotTerminalEvent: { value: false },
278
+ }) {
279
+ if (!json || typeof json !== "object")
280
+ return [];
281
+ const ev = json;
282
+ const type = textField(ev.type);
283
+ const part = asRecord(ev.part);
284
+ const out = [];
285
+ const sessionId = textField(ev.sessionID) || textField(part?.sessionID);
286
+ if (sessionId && !this.sessionEmitted) {
287
+ this.sessionEmitted = true;
288
+ out.push({ kind: "session", sessionId });
289
+ }
290
+ // Any new step/output after a held `stop` means the turn continued (this is
291
+ // how OpenCode's compaction summary looks on the wire).
292
+ if (type === "step_start" ||
293
+ type === "part_delta" ||
294
+ type === "text" ||
295
+ type === "reasoning" ||
296
+ type === "tool_use") {
297
+ this.pendingDone = null;
298
+ if (this.deferredTerminal) {
299
+ this.deferredTerminal = false;
300
+ ctx.gotTerminalEvent.value = false;
301
+ }
302
+ }
303
+ if (type === "error") {
304
+ this.pendingDone = null;
305
+ this.deferredTerminal = false;
306
+ ctx.gotTerminalEvent.value = true;
307
+ const error = asRecord(ev.error);
308
+ const data = asRecord(error?.data);
309
+ const message = textField(data?.message) ||
310
+ textField(error?.message) ||
311
+ stringifyUnknown(error?.data ?? error) ||
312
+ textField(ev.message) ||
313
+ ctx.stderr ||
314
+ "OpenCode error";
315
+ out.push({ kind: "error", message });
316
+ return out;
317
+ }
318
+ if (type === "step_finish") {
319
+ if (part) {
320
+ const { usage } = stepUsage(part);
321
+ if (usage) {
322
+ this.usage.inputTokens += usage.inputTokens;
323
+ this.usage.outputTokens += usage.outputTokens;
324
+ this.usage.cacheReadTokens += usage.cacheReadTokens;
325
+ this.usage.cacheWriteTokens += usage.cacheWriteTokens;
326
+ this.usage.costUsd += usage.costUsd ?? 0;
327
+ }
328
+ if (textField(part.reason) === "stop") {
329
+ this.pendingDone = {
330
+ kind: "done",
331
+ result: ctx.assistantBuf.value,
332
+ usage: {
333
+ ...this.usage,
334
+ ...(this.usage.costUsd > 0 ? { costUsd: this.usage.costUsd } : {}),
335
+ },
336
+ };
337
+ }
338
+ else {
339
+ this.pendingDone = null;
340
+ }
341
+ }
342
+ return out;
343
+ }
344
+ if (type === "part_delta") {
345
+ const partId = textField(ev.partID) || textField(part?.id) || `delta-${(this.anonSeq += 1)}`;
346
+ const partType = textField(ev.partType) || textField(part?.type);
347
+ const delta = textField(ev.delta);
348
+ if (!delta || ctx.toolsOnly)
349
+ return out;
350
+ if (partType === "reasoning" || partType === "thinking") {
351
+ const full = this.appendText(ctx, partId, delta, "thinking", false);
352
+ if (full)
353
+ out.push({ kind: "thinking_delta", text: full });
354
+ }
355
+ else {
356
+ const full = this.appendText(ctx, partId, delta, "text", false);
357
+ if (full)
358
+ out.push({ kind: "assistant_delta", text: full });
359
+ }
360
+ return out;
361
+ }
362
+ if (type === "text" || type === "reasoning") {
363
+ if (!part || ctx.toolsOnly)
364
+ return out;
365
+ const partId = textField(part.id) || `part-${(this.anonSeq += 1)}`;
366
+ const text = textField(part.text);
367
+ if (!text)
368
+ return out;
369
+ if (type === "reasoning") {
370
+ const full = this.appendText(ctx, partId, text, "thinking", true);
371
+ if (full)
372
+ out.push({ kind: "thinking_delta", text: full });
373
+ }
374
+ else {
375
+ const full = this.appendText(ctx, partId, text, "text", true);
376
+ if (full)
377
+ out.push({ kind: "assistant_delta", text: full });
378
+ }
379
+ return out;
380
+ }
381
+ if (type === "tool_use" && part) {
382
+ return out.concat(this.parseTool(ev, part, ctx));
383
+ }
384
+ return out;
385
+ }
386
+ flush(ctx) {
387
+ const done = this.pendingDone;
388
+ if (!done)
389
+ return [];
390
+ this.pendingDone = null;
391
+ this.deferredTerminal = true;
392
+ if (ctx)
393
+ ctx.gotTerminalEvent.value = true;
394
+ return [done];
395
+ }
396
+ }
397
+ export const opencodeBackend = new OpencodeBackend();
@@ -0,0 +1,171 @@
1
+ import type { AgentTodoItem, ClarifyingQuestion } from "../events.js";
2
+ import type { ToolIntegrationRef } from "../toolIntegration.js";
3
+ import type { CliCommandCatalog } from "../slashCommands.js";
4
+ /** Backend kind for a room-level agent process. */
5
+ export type AgentBackendKind = "cursor" | "claude-code" | "codex" | "opencode";
6
+ /** Claude Code, Codex, and OpenCode share the local-CLI + Blaxel-sandbox path. */
7
+ export declare const CLI_SANDBOX_BACKENDS: readonly ["claude-code", "codex", "opencode"];
8
+ export type CliSandboxBackendKind = (typeof CLI_SANDBOX_BACKENDS)[number];
9
+ export declare function isCliSandboxBackend(kind: string | null | undefined): kind is CliSandboxBackendKind;
10
+ export declare function parseAgentBackendKind(raw: unknown): AgentBackendKind;
11
+ export declare function backendShortLabel(kind: AgentBackendKind): string;
12
+ /** Git / PR metadata attached to a completed cloud agent run. */
13
+ export interface RunGitInfo {
14
+ branches: Array<{
15
+ repoUrl: string;
16
+ branch?: string;
17
+ prUrl?: string;
18
+ }>;
19
+ }
20
+ /** Token use (and cost, when the CLI reports it) for one finished turn. */
21
+ export interface RunUsage {
22
+ inputTokens: number;
23
+ outputTokens: number;
24
+ cacheReadTokens: number;
25
+ cacheWriteTokens: number;
26
+ /** Reported by the CLI (Claude Code); absent when it has to be estimated. */
27
+ costUsd?: number;
28
+ }
29
+ export type { AgentTodoItem, ClarifyingQuestion };
30
+ /**
31
+ * Normalized agent stream events used internally by Steer.
32
+ * Subagent events from within a single agent carry `subagent_nested`
33
+ * and a parent tool-call id — they do NOT create a new Agent row.
34
+ */
35
+ export type NormalizedAgentEvent = {
36
+ kind: "session";
37
+ sessionId: string;
38
+ } | {
39
+ kind: "assistant_delta";
40
+ text: string;
41
+ } | {
42
+ kind: "assistant_final";
43
+ text: string;
44
+ } | {
45
+ kind: "thinking_delta";
46
+ text: string;
47
+ } | {
48
+ kind: "thinking_final";
49
+ text: string;
50
+ } | {
51
+ kind: "tool_start";
52
+ callId: string;
53
+ name: string;
54
+ detail: string;
55
+ path?: string;
56
+ todos?: AgentTodoItem[];
57
+ questions?: ClarifyingQuestion[];
58
+ /** Set for MCP calls: which server / integration / tool ran. */
59
+ integration?: ToolIntegrationRef;
60
+ } | {
61
+ kind: "tool_done";
62
+ callId: string;
63
+ name: string;
64
+ detail: string;
65
+ path?: string;
66
+ /** The tool failed / was rejected; `detail` carries the error. */
67
+ isError?: boolean;
68
+ diffPatch?: string;
69
+ todos?: AgentTodoItem[];
70
+ questions?: ClarifyingQuestion[];
71
+ /** Set for MCP calls: which server / integration / tool ran. */
72
+ integration?: ToolIntegrationRef;
73
+ } | {
74
+ kind: "subagent_nested";
75
+ parentCallId: string;
76
+ callId: string;
77
+ name: string;
78
+ detail: string;
79
+ path?: string;
80
+ status: "started" | "completed";
81
+ } | {
82
+ kind: "error";
83
+ message: string;
84
+ }
85
+ /**
86
+ * The agent's reply and tools are finished; the runner may still be doing
87
+ * post-run work (e.g. pushing the branch) before `done` arrives.
88
+ */
89
+ | {
90
+ kind: "turn_complete";
91
+ detail?: string;
92
+ }
93
+ /** Runner progress before/around the agent (e.g. "Starting sandbox…"); empty clears it. */
94
+ | {
95
+ kind: "status";
96
+ detail?: string;
97
+ }
98
+ /** Slash commands the CLI reported at startup (drives the composer menu). */
99
+ | {
100
+ kind: "cli_commands";
101
+ catalog: CliCommandCatalog;
102
+ } | {
103
+ kind: "done";
104
+ result: string;
105
+ git?: RunGitInfo;
106
+ usage?: RunUsage;
107
+ };
108
+ export interface BuildArgsOptions {
109
+ prompt: string;
110
+ modelId: string;
111
+ sessionId?: string | null;
112
+ /** Cursor `--mode plan` / Claude `--permission-mode plan`. */
113
+ mode?: "agent" | "plan";
114
+ /** Working directory for Codex `--cd` (repo root in Blaxel). */
115
+ cwd?: string | null;
116
+ /** Streamable-HTTP MCP servers to attach for this invocation. */
117
+ mcpServers?: Record<string, {
118
+ type: "http";
119
+ url: string;
120
+ headers: Record<string, string>;
121
+ }>;
122
+ }
123
+ /**
124
+ * Abstraction over headless coding-agent CLIs.
125
+ * Implementations spawn the process and parse NDJSON into NormalizedAgentEvent.
126
+ */
127
+ export interface WorkerBackend {
128
+ kind: AgentBackendKind;
129
+ /** False until the backend is fully wired and tested. */
130
+ available: boolean;
131
+ /** Executable name (`cursor`, `claude`, …). */
132
+ command: string;
133
+ buildArgs(opts: BuildArgsOptions): string[];
134
+ /** Extra env for this invocation (e.g. OpenCode inline MCP config). */
135
+ buildEnv?(opts: BuildArgsOptions): Record<string, string>;
136
+ /**
137
+ * Parse one JSON object from a stream-json line into zero or more
138
+ * normalized events. Stateful parsers may hold buffers on the instance;
139
+ * callers that need isolation should create a fresh backend per run.
140
+ */
141
+ parseLine(json: unknown, ctx?: ParseLineContext): NormalizedAgentEvent[];
142
+ /**
143
+ * Emit a terminal event the parser is holding back. OpenCode reports a
144
+ * `stop` on the step that *triggers* compaction too, so its `done` is
145
+ * deferred until the stream stays quiet (a new step cancels it) or the
146
+ * process exits. Call periodically and once on process close.
147
+ */
148
+ flush?(ctx?: ParseLineContext): NormalizedAgentEvent[];
149
+ }
150
+ export interface ParseLineContext {
151
+ /** Accumulated assistant text so far (mutated by the parser). */
152
+ assistantBuf: {
153
+ value: string;
154
+ };
155
+ /** Accumulated model thinking / reasoning for the current turn. */
156
+ thinkingBuf?: {
157
+ value: string;
158
+ };
159
+ /** Set true when a terminal result/done/error event is emitted. */
160
+ gotTerminalEvent: {
161
+ value: boolean;
162
+ };
163
+ /** Optional stderr buffer used for error messages. */
164
+ stderr?: string;
165
+ /**
166
+ * When true, only emit tool/session events (no assistant text / done / error).
167
+ * Used to re-parse Blaxel process stdout after streamLogs may have missed
168
+ * mid-run tool lines while still delivering the terminal result.
169
+ */
170
+ toolsOnly?: boolean;
171
+ }