@geteigen/cli 0.0.0-stage → 3.1.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/README.md +154 -2
- package/dist/agent.d.ts +32 -0
- package/dist/agent.js +246 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +33 -0
- package/dist/cli/src/agent.d.ts +24 -0
- package/dist/cli/src/agent.js +263 -0
- package/dist/cli/src/agentTools.d.ts +6 -0
- package/dist/cli/src/agentTools.js +104 -0
- package/dist/cli/src/auth.d.ts +5 -0
- package/dist/cli/src/auth.js +33 -0
- package/dist/cli/src/cliEnv.d.ts +6 -0
- package/dist/cli/src/cliEnv.js +26 -0
- package/dist/cli/src/config.d.ts +11 -0
- package/dist/cli/src/config.js +51 -0
- package/dist/cli/src/cursorAuth.d.ts +10 -0
- package/dist/cli/src/cursorAuth.js +92 -0
- package/dist/cli/src/gitAuthEnv.d.ts +5 -0
- package/dist/cli/src/gitAuthEnv.js +35 -0
- package/dist/cli/src/index.d.ts +2 -0
- package/dist/cli/src/index.js +244 -0
- package/dist/cli/src/listModels.d.ts +14 -0
- package/dist/cli/src/listModels.js +196 -0
- package/dist/cli/src/listSessions.d.ts +9 -0
- package/dist/cli/src/listSessions.js +37 -0
- package/dist/cli/src/machineConfig.d.ts +20 -0
- package/dist/cli/src/machineConfig.js +66 -0
- package/dist/cli/src/pickFolder.d.ts +5 -0
- package/dist/cli/src/pickFolder.js +65 -0
- package/dist/cli/src/proc.d.ts +31 -0
- package/dist/cli/src/proc.js +97 -0
- package/dist/cli/src/service.d.ts +17 -0
- package/dist/cli/src/service.js +255 -0
- package/dist/cli/src/workRoots.d.ts +30 -0
- package/dist/cli/src/workRoots.js +228 -0
- package/dist/cli/src/worker.d.ts +8 -0
- package/dist/cli/src/worker.js +51 -0
- package/dist/cli/src/workerCore.d.ts +47 -0
- package/dist/cli/src/workerCore.js +721 -0
- package/dist/cli/src/workspace.d.ts +10 -0
- package/dist/cli/src/workspace.js +329 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +51 -0
- package/dist/cursorAuth.d.ts +10 -0
- package/dist/cursorAuth.js +95 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -0
- package/dist/listModels.d.ts +6 -0
- package/dist/listModels.js +62 -0
- package/dist/listSessions.d.ts +9 -0
- package/dist/listSessions.js +37 -0
- package/dist/pickFolder.d.ts +5 -0
- package/dist/pickFolder.js +65 -0
- package/dist/shared/actionDrafts.d.ts +83 -0
- package/dist/shared/actionDrafts.js +169 -0
- package/dist/shared/agentArtifacts.d.ts +49 -0
- package/dist/shared/agentArtifacts.js +169 -0
- package/dist/shared/agentLabel.d.ts +18 -0
- package/dist/shared/agentLabel.js +22 -0
- package/dist/shared/agentMentions.d.ts +38 -0
- package/dist/shared/agentMentions.js +110 -0
- package/dist/shared/agentRepos.d.ts +28 -0
- package/dist/shared/agentRepos.js +60 -0
- package/dist/shared/agentRunStatus.d.ts +12 -0
- package/dist/shared/agentRunStatus.js +17 -0
- package/dist/shared/agentUsage.d.ts +24 -0
- package/dist/shared/agentUsage.js +139 -0
- package/dist/shared/approvals.d.ts +44 -0
- package/dist/shared/approvals.js +97 -0
- package/dist/shared/askEigen.d.ts +190 -0
- package/dist/shared/askEigen.js +76 -0
- package/dist/shared/attribution.d.ts +25 -0
- package/dist/shared/attribution.js +63 -0
- package/dist/shared/automations.d.ts +135 -0
- package/dist/shared/automations.js +553 -0
- package/dist/shared/backends/claudeCode.d.ts +44 -0
- package/dist/shared/backends/claudeCode.js +548 -0
- package/dist/shared/backends/codex.d.ts +15 -0
- package/dist/shared/backends/codex.js +287 -0
- package/dist/shared/backends/cursor.d.ts +111 -0
- package/dist/shared/backends/cursor.js +1216 -0
- package/dist/shared/backends/index.d.ts +18 -0
- package/dist/shared/backends/index.js +58 -0
- package/dist/shared/backends/opencode.d.ts +43 -0
- package/dist/shared/backends/opencode.js +415 -0
- package/dist/shared/backends/types.d.ts +171 -0
- package/dist/shared/backends/types.js +19 -0
- package/dist/shared/backends/usage.d.ts +8 -0
- package/dist/shared/backends/usage.js +71 -0
- package/dist/shared/billingPlans.d.ts +60 -0
- package/dist/shared/billingPlans.js +262 -0
- package/dist/shared/brain.d.ts +353 -0
- package/dist/shared/brain.js +487 -0
- package/dist/shared/clarifyingQuestions.d.ts +13 -0
- package/dist/shared/clarifyingQuestions.js +207 -0
- package/dist/shared/claudeModels.d.ts +10 -0
- package/dist/shared/claudeModels.js +52 -0
- package/dist/shared/cloudBackends.d.ts +21 -0
- package/dist/shared/cloudBackends.js +29 -0
- package/dist/shared/codexModels.d.ts +10 -0
- package/dist/shared/codexModels.js +28 -0
- package/dist/shared/contextWindows.d.ts +2 -0
- package/dist/shared/contextWindows.js +30 -0
- package/dist/shared/cursorMcpConfig.d.ts +14 -0
- package/dist/shared/cursorMcpConfig.js +43 -0
- package/dist/shared/cursorProjectMcp.d.ts +10 -0
- package/dist/shared/cursorProjectMcp.js +42 -0
- package/dist/shared/docs.d.ts +59 -0
- package/dist/shared/docs.js +87 -0
- package/dist/shared/events.d.ts +932 -0
- package/dist/shared/events.js +57 -0
- package/dist/shared/integrations.d.ts +266 -0
- package/dist/shared/integrations.js +449 -0
- package/dist/shared/issueTranscript.d.ts +64 -0
- package/dist/shared/issueTranscript.js +387 -0
- package/dist/shared/issues.d.ts +139 -0
- package/dist/shared/issues.js +217 -0
- package/dist/shared/machines.d.ts +119 -0
- package/dist/shared/machines.js +58 -0
- package/dist/shared/markdownMedia.d.ts +13 -0
- package/dist/shared/markdownMedia.js +41 -0
- package/dist/shared/notificationText.d.ts +6 -0
- package/dist/shared/notificationText.js +38 -0
- package/dist/shared/opencodeModels.d.ts +19 -0
- package/dist/shared/opencodeModels.js +62 -0
- package/dist/shared/opencodeSession.d.ts +7 -0
- package/dist/shared/opencodeSession.js +30 -0
- package/dist/shared/orchestration.d.ts +58 -0
- package/dist/shared/orchestration.js +12 -0
- package/dist/shared/orgs.d.ts +47 -0
- package/dist/shared/orgs.js +49 -0
- package/dist/shared/personalOrg.d.ts +5 -0
- package/dist/shared/personalOrg.js +12 -0
- package/dist/shared/plans.d.ts +3 -0
- package/dist/shared/plans.js +19 -0
- package/dist/shared/providerModels.d.ts +40 -0
- package/dist/shared/providerModels.js +503 -0
- package/dist/shared/roomAttention.d.ts +17 -0
- package/dist/shared/roomAttention.js +36 -0
- package/dist/shared/roomContext.d.ts +120 -0
- package/dist/shared/roomContext.js +133 -0
- package/dist/shared/roomPermissions.d.ts +56 -0
- package/dist/shared/roomPermissions.js +133 -0
- package/dist/shared/sessionSeats.d.ts +29 -0
- package/dist/shared/sessionSeats.js +57 -0
- package/dist/shared/slashCommands.d.ts +39 -0
- package/dist/shared/slashCommands.js +166 -0
- package/dist/shared/space.d.ts +33 -0
- package/dist/shared/space.js +54 -0
- package/dist/shared/spaceFeed.d.ts +78 -0
- package/dist/shared/spaceFeed.js +194 -0
- package/dist/shared/stringifyUnknown.d.ts +1 -0
- package/dist/shared/stringifyUnknown.js +1 -0
- package/dist/shared/swarm.d.ts +320 -0
- package/dist/shared/swarm.js +328 -0
- package/dist/shared/swarmVisual.d.ts +79 -0
- package/dist/shared/swarmVisual.js +212 -0
- package/dist/shared/terminalFilter.d.ts +13 -0
- package/dist/shared/terminalFilter.js +46 -0
- package/dist/shared/toolIntegration.d.ts +56 -0
- package/dist/shared/toolIntegration.js +180 -0
- package/dist/shared/typing.d.ts +9 -0
- package/dist/shared/typing.js +37 -0
- package/dist/shared/uploads.d.ts +31 -0
- package/dist/shared/uploads.js +65 -0
- package/dist/shared/voiceApprovals.d.ts +36 -0
- package/dist/shared/voiceApprovals.js +229 -0
- package/dist/shared/workspace.d.ts +49 -0
- package/dist/shared/workspace.js +53 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +266 -0
- 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,43 @@
|
|
|
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 thinkingPartId;
|
|
22
|
+
private startedTools;
|
|
23
|
+
private doneTools;
|
|
24
|
+
private anonSeq;
|
|
25
|
+
/**
|
|
26
|
+
* A `stop` step-finish can belong to the compaction summary, after which the
|
|
27
|
+
* same turn continues. Hold it until the stream goes quiet (flush) instead of
|
|
28
|
+
* ending the reply as soon as it arrives.
|
|
29
|
+
*/
|
|
30
|
+
private pendingDone;
|
|
31
|
+
/** A held `done` was flushed; clear the terminal flag if the turn resumes. */
|
|
32
|
+
private deferredTerminal;
|
|
33
|
+
buildArgs(opts: BuildArgsOptions): string[];
|
|
34
|
+
/** OpenCode takes MCP servers through its merged config, not a CLI flag. */
|
|
35
|
+
buildEnv(opts: BuildArgsOptions): Record<string, string>;
|
|
36
|
+
private appendText;
|
|
37
|
+
/** One thought per reasoning part. A new part finalizes the previous thought. */
|
|
38
|
+
private emitThinking;
|
|
39
|
+
private parseTool;
|
|
40
|
+
parseLine(json: unknown, ctx?: ParseLineContext): NormalizedAgentEvent[];
|
|
41
|
+
flush(ctx?: ParseLineContext): NormalizedAgentEvent[];
|
|
42
|
+
}
|
|
43
|
+
export declare const opencodeBackend: OpencodeBackend;
|
|
@@ -0,0 +1,415 @@
|
|
|
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
|
+
thinkingPartId = "";
|
|
149
|
+
startedTools = new Set();
|
|
150
|
+
doneTools = new Set();
|
|
151
|
+
anonSeq = 0;
|
|
152
|
+
/**
|
|
153
|
+
* A `stop` step-finish can belong to the compaction summary, after which the
|
|
154
|
+
* same turn continues. Hold it until the stream goes quiet (flush) instead of
|
|
155
|
+
* ending the reply as soon as it arrives.
|
|
156
|
+
*/
|
|
157
|
+
pendingDone = null;
|
|
158
|
+
/** A held `done` was flushed; clear the terminal flag if the turn resumes. */
|
|
159
|
+
deferredTerminal = false;
|
|
160
|
+
buildArgs(opts) {
|
|
161
|
+
const args = ["run", "--format", "json", "--thinking"];
|
|
162
|
+
if (opts.mode === "plan") {
|
|
163
|
+
args.push("--agent", "plan");
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
args.push("--auto");
|
|
167
|
+
}
|
|
168
|
+
if (opts.modelId && opts.modelId !== "auto") {
|
|
169
|
+
args.push("--model", opts.modelId);
|
|
170
|
+
}
|
|
171
|
+
if (opts.sessionId) {
|
|
172
|
+
args.push("--session", opts.sessionId);
|
|
173
|
+
}
|
|
174
|
+
args.push("--", opts.prompt);
|
|
175
|
+
return args;
|
|
176
|
+
}
|
|
177
|
+
/** OpenCode takes MCP servers through its merged config, not a CLI flag. */
|
|
178
|
+
buildEnv(opts) {
|
|
179
|
+
const servers = opts.mcpServers;
|
|
180
|
+
if (!servers || !Object.keys(servers).length)
|
|
181
|
+
return {};
|
|
182
|
+
const mcp = {};
|
|
183
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
184
|
+
mcp[name] = {
|
|
185
|
+
type: "remote",
|
|
186
|
+
url: server.url,
|
|
187
|
+
headers: server.headers,
|
|
188
|
+
enabled: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
193
|
+
$schema: "https://opencode.ai/config.json",
|
|
194
|
+
mcp,
|
|
195
|
+
}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
appendText(ctx, partId, incoming, kind, full) {
|
|
199
|
+
const sentMap = kind === "text" ? this.textSent : this.thinkingSent;
|
|
200
|
+
const buffer = kind === "text" ? ctx.assistantBuf : (ctx.thinkingBuf ??= { value: "" });
|
|
201
|
+
const already = sentMap.get(partId) || "";
|
|
202
|
+
let chunk = "";
|
|
203
|
+
if (!incoming)
|
|
204
|
+
return "";
|
|
205
|
+
if (full) {
|
|
206
|
+
if (incoming.startsWith(already))
|
|
207
|
+
chunk = incoming.slice(already.length);
|
|
208
|
+
else if (already && already.startsWith(incoming))
|
|
209
|
+
chunk = "";
|
|
210
|
+
else
|
|
211
|
+
chunk = incoming;
|
|
212
|
+
sentMap.set(partId, incoming);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
chunk = incoming;
|
|
216
|
+
sentMap.set(partId, already + incoming);
|
|
217
|
+
}
|
|
218
|
+
if (!chunk)
|
|
219
|
+
return "";
|
|
220
|
+
// Text parts after a tool continue the reply. Thinking parts must not —
|
|
221
|
+
// each reasoning block is its own thought (see emitThinking).
|
|
222
|
+
if (kind === "text" &&
|
|
223
|
+
!already &&
|
|
224
|
+
buffer.value &&
|
|
225
|
+
!/\s$/.test(buffer.value) &&
|
|
226
|
+
!/^\s/.test(chunk)) {
|
|
227
|
+
buffer.value += "\n\n";
|
|
228
|
+
}
|
|
229
|
+
buffer.value += chunk;
|
|
230
|
+
return buffer.value;
|
|
231
|
+
}
|
|
232
|
+
/** One thought per reasoning part. A new part finalizes the previous thought. */
|
|
233
|
+
emitThinking(ctx, partId, incoming, full) {
|
|
234
|
+
const out = [];
|
|
235
|
+
const thinking = (ctx.thinkingBuf ??= { value: "" });
|
|
236
|
+
if (this.thinkingPartId && this.thinkingPartId !== partId && thinking.value) {
|
|
237
|
+
out.push({ kind: "thinking_final", text: thinking.value });
|
|
238
|
+
thinking.value = "";
|
|
239
|
+
}
|
|
240
|
+
this.thinkingPartId = partId;
|
|
241
|
+
const text = this.appendText(ctx, partId, incoming, "thinking", full);
|
|
242
|
+
if (text)
|
|
243
|
+
out.push({ kind: "thinking_delta", text });
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
parseTool(ev, part, ctx) {
|
|
247
|
+
const out = [];
|
|
248
|
+
const rawName = textField(part.tool) || "tool";
|
|
249
|
+
const name = opencodeToolName(rawName);
|
|
250
|
+
const callId = textField(part.callID) ||
|
|
251
|
+
textField(part.id) ||
|
|
252
|
+
`opencode-tool-${(this.anonSeq += 1)}`;
|
|
253
|
+
const state = asRecord(part.state) || {};
|
|
254
|
+
const args = asRecord(state.input);
|
|
255
|
+
const path = toolPath(args);
|
|
256
|
+
const status = textField(state.status).toLowerCase();
|
|
257
|
+
const title = textField(state.title);
|
|
258
|
+
const detail = toolDetail(name, args, path, title);
|
|
259
|
+
const todos = todosFromToolArgs(args);
|
|
260
|
+
const integration = opencodeToolIntegration(rawName, args);
|
|
261
|
+
const error = toolError(state);
|
|
262
|
+
const done = status === "completed" || status === "error" || status === "failed";
|
|
263
|
+
if (!this.startedTools.has(callId)) {
|
|
264
|
+
this.startedTools.add(callId);
|
|
265
|
+
(ctx.thinkingBuf ??= { value: "" }).value = "";
|
|
266
|
+
this.thinkingPartId = "";
|
|
267
|
+
out.push({
|
|
268
|
+
kind: "tool_start",
|
|
269
|
+
callId,
|
|
270
|
+
name,
|
|
271
|
+
detail: todos.length > 0
|
|
272
|
+
? `${todos.length} todo${todos.length === 1 ? "" : "s"} · ${todoStatusSummary(todos)}`
|
|
273
|
+
: detail,
|
|
274
|
+
...(path ? { path } : {}),
|
|
275
|
+
...(todos.length ? { todos } : {}),
|
|
276
|
+
...(integration ? { integration } : {}),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (done && !this.doneTools.has(callId)) {
|
|
280
|
+
this.doneTools.add(callId);
|
|
281
|
+
const result = resultText(state);
|
|
282
|
+
out.push({
|
|
283
|
+
kind: "tool_done",
|
|
284
|
+
callId,
|
|
285
|
+
name,
|
|
286
|
+
detail: todos.length > 0
|
|
287
|
+
? `${todos.length} todo${todos.length === 1 ? "" : "s"} · ${todoStatusSummary(todos)}`
|
|
288
|
+
: formatToolResultDetail(name, args, result || (error ? "Failed" : "Done"), error ? "Failed" : "Done"),
|
|
289
|
+
...(path ? { path } : {}),
|
|
290
|
+
...(status !== "completed" ? { isError: true } : {}),
|
|
291
|
+
...(todos.length ? { todos } : {}),
|
|
292
|
+
...(integration ? { integration } : {}),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
parseLine(json, ctx = {
|
|
298
|
+
assistantBuf: { value: "" },
|
|
299
|
+
gotTerminalEvent: { value: false },
|
|
300
|
+
}) {
|
|
301
|
+
if (!json || typeof json !== "object")
|
|
302
|
+
return [];
|
|
303
|
+
const ev = json;
|
|
304
|
+
const type = textField(ev.type);
|
|
305
|
+
const part = asRecord(ev.part);
|
|
306
|
+
const out = [];
|
|
307
|
+
const sessionId = textField(ev.sessionID) || textField(part?.sessionID);
|
|
308
|
+
if (sessionId && !this.sessionEmitted) {
|
|
309
|
+
this.sessionEmitted = true;
|
|
310
|
+
out.push({ kind: "session", sessionId });
|
|
311
|
+
}
|
|
312
|
+
// Any new step/output after a held `stop` means the turn continued (this is
|
|
313
|
+
// how OpenCode's compaction summary looks on the wire).
|
|
314
|
+
if (type === "step_start" ||
|
|
315
|
+
type === "part_delta" ||
|
|
316
|
+
type === "text" ||
|
|
317
|
+
type === "reasoning" ||
|
|
318
|
+
type === "tool_use") {
|
|
319
|
+
this.pendingDone = null;
|
|
320
|
+
if (this.deferredTerminal) {
|
|
321
|
+
this.deferredTerminal = false;
|
|
322
|
+
ctx.gotTerminalEvent.value = false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (type === "error") {
|
|
326
|
+
this.pendingDone = null;
|
|
327
|
+
this.deferredTerminal = false;
|
|
328
|
+
ctx.gotTerminalEvent.value = true;
|
|
329
|
+
const error = asRecord(ev.error);
|
|
330
|
+
const data = asRecord(error?.data);
|
|
331
|
+
const message = textField(data?.message) ||
|
|
332
|
+
textField(error?.message) ||
|
|
333
|
+
stringifyUnknown(error?.data ?? error) ||
|
|
334
|
+
textField(ev.message) ||
|
|
335
|
+
ctx.stderr ||
|
|
336
|
+
"OpenCode error";
|
|
337
|
+
out.push({ kind: "error", message });
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
if (type === "step_finish") {
|
|
341
|
+
if (part) {
|
|
342
|
+
const { usage } = stepUsage(part);
|
|
343
|
+
if (usage) {
|
|
344
|
+
this.usage.inputTokens += usage.inputTokens;
|
|
345
|
+
this.usage.outputTokens += usage.outputTokens;
|
|
346
|
+
this.usage.cacheReadTokens += usage.cacheReadTokens;
|
|
347
|
+
this.usage.cacheWriteTokens += usage.cacheWriteTokens;
|
|
348
|
+
this.usage.costUsd += usage.costUsd ?? 0;
|
|
349
|
+
}
|
|
350
|
+
if (textField(part.reason) === "stop") {
|
|
351
|
+
this.pendingDone = {
|
|
352
|
+
kind: "done",
|
|
353
|
+
result: ctx.assistantBuf.value,
|
|
354
|
+
usage: {
|
|
355
|
+
...this.usage,
|
|
356
|
+
...(this.usage.costUsd > 0 ? { costUsd: this.usage.costUsd } : {}),
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
this.pendingDone = null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
if (type === "part_delta") {
|
|
367
|
+
const partId = textField(ev.partID) || textField(part?.id) || `delta-${(this.anonSeq += 1)}`;
|
|
368
|
+
const partType = textField(ev.partType) || textField(part?.type);
|
|
369
|
+
const delta = textField(ev.delta);
|
|
370
|
+
if (!delta || ctx.toolsOnly)
|
|
371
|
+
return out;
|
|
372
|
+
if (partType === "reasoning" || partType === "thinking") {
|
|
373
|
+
return out.concat(this.emitThinking(ctx, partId, delta, false));
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const full = this.appendText(ctx, partId, delta, "text", false);
|
|
377
|
+
if (full)
|
|
378
|
+
out.push({ kind: "assistant_delta", text: full });
|
|
379
|
+
}
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
if (type === "text" || type === "reasoning") {
|
|
383
|
+
if (!part || ctx.toolsOnly)
|
|
384
|
+
return out;
|
|
385
|
+
const partId = textField(part.id) || `part-${(this.anonSeq += 1)}`;
|
|
386
|
+
const text = textField(part.text);
|
|
387
|
+
if (!text)
|
|
388
|
+
return out;
|
|
389
|
+
if (type === "reasoning") {
|
|
390
|
+
return out.concat(this.emitThinking(ctx, partId, text, true));
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
const full = this.appendText(ctx, partId, text, "text", true);
|
|
394
|
+
if (full)
|
|
395
|
+
out.push({ kind: "assistant_delta", text: full });
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
if (type === "tool_use" && part) {
|
|
400
|
+
return out.concat(this.parseTool(ev, part, ctx));
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
flush(ctx) {
|
|
405
|
+
const done = this.pendingDone;
|
|
406
|
+
if (!done)
|
|
407
|
+
return [];
|
|
408
|
+
this.pendingDone = null;
|
|
409
|
+
this.deferredTerminal = true;
|
|
410
|
+
if (ctx)
|
|
411
|
+
ctx.gotTerminalEvent.value = true;
|
|
412
|
+
return [done];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
export const opencodeBackend = new OpencodeBackend();
|