@getforgeai/cli 0.1.0-beta.1
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/LICENSE +201 -0
- package/README.md +80 -0
- package/dist/bin/forgeai.d.ts +3 -0
- package/dist/bin/forgeai.js +8 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +180 -0
- package/dist/commands/auth-login.d.ts +4 -0
- package/dist/commands/auth-login.js +202 -0
- package/dist/commands/auth-logout.d.ts +2 -0
- package/dist/commands/auth-logout.js +41 -0
- package/dist/commands/auth-status.d.ts +4 -0
- package/dist/commands/auth-status.js +81 -0
- package/dist/commands/cli-list.d.ts +4 -0
- package/dist/commands/cli-list.js +40 -0
- package/dist/commands/cli-unlink.d.ts +4 -0
- package/dist/commands/cli-unlink.js +45 -0
- package/dist/commands/config-set.d.ts +5 -0
- package/dist/commands/config-set.js +63 -0
- package/dist/commands/connect.d.ts +9 -0
- package/dist/commands/connect.js +165 -0
- package/dist/commands/disconnect.d.ts +5 -0
- package/dist/commands/disconnect.js +126 -0
- package/dist/commands/journal-clear.d.ts +2 -0
- package/dist/commands/journal-clear.js +16 -0
- package/dist/commands/journal-list.d.ts +2 -0
- package/dist/commands/journal-list.js +20 -0
- package/dist/commands/journal-status.d.ts +2 -0
- package/dist/commands/journal-status.js +18 -0
- package/dist/commands/mcp-serve.d.ts +13 -0
- package/dist/commands/mcp-serve.js +74 -0
- package/dist/commands/org-list.d.ts +4 -0
- package/dist/commands/org-list.js +56 -0
- package/dist/commands/org-unlink.d.ts +4 -0
- package/dist/commands/org-unlink.js +63 -0
- package/dist/commands/reconnect.d.ts +4 -0
- package/dist/commands/reconnect.js +46 -0
- package/dist/commands/release-open.d.ts +2 -0
- package/dist/commands/release-open.js +43 -0
- package/dist/commands/session-respond.d.ts +2 -0
- package/dist/commands/session-respond.js +29 -0
- package/dist/commands/task-open.d.ts +2 -0
- package/dist/commands/task-open.js +42 -0
- package/dist/commands/vm.d.ts +21 -0
- package/dist/commands/vm.js +122 -0
- package/dist/commands/workflow-open.d.ts +2 -0
- package/dist/commands/workflow-open.js +43 -0
- package/dist/config/config-manager.d.ts +31 -0
- package/dist/config/config-manager.js +70 -0
- package/dist/config/token-manager.d.ts +11 -0
- package/dist/config/token-manager.js +87 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +22 -0
- package/dist/lib/auth-client.d.ts +1670 -0
- package/dist/lib/auth-client.js +10 -0
- package/dist/lib/branch-manager.d.ts +79 -0
- package/dist/lib/branch-manager.js +620 -0
- package/dist/lib/clone-and-checkout.d.ts +6 -0
- package/dist/lib/clone-and-checkout.js +41 -0
- package/dist/lib/connection-manager.d.ts +58 -0
- package/dist/lib/connection-manager.js +436 -0
- package/dist/lib/daemon-entry.d.ts +2 -0
- package/dist/lib/daemon-entry.js +210 -0
- package/dist/lib/daemon.d.ts +38 -0
- package/dist/lib/daemon.js +148 -0
- package/dist/lib/event-journal.d.ts +24 -0
- package/dist/lib/event-journal.js +76 -0
- package/dist/lib/ipc-client.d.ts +10 -0
- package/dist/lib/ipc-client.js +70 -0
- package/dist/lib/ipc-server.d.ts +36 -0
- package/dist/lib/ipc-server.js +70 -0
- package/dist/lib/node-version-guard.d.ts +8 -0
- package/dist/lib/node-version-guard.js +60 -0
- package/dist/lib/open-directory.d.ts +7 -0
- package/dist/lib/open-directory.js +41 -0
- package/dist/lib/repo-manager.d.ts +29 -0
- package/dist/lib/repo-manager.js +103 -0
- package/dist/lib/resilient-emitter.d.ts +53 -0
- package/dist/lib/resilient-emitter.js +160 -0
- package/dist/lib/rest-client.d.ts +164 -0
- package/dist/lib/rest-client.js +139 -0
- package/dist/lib/skill-manager.d.ts +23 -0
- package/dist/lib/skill-manager.js +167 -0
- package/dist/lib/socket-client.d.ts +68 -0
- package/dist/lib/socket-client.js +160 -0
- package/dist/mcp/mcp-probe.d.ts +11 -0
- package/dist/mcp/mcp-probe.js +106 -0
- package/dist/mcp/mcp-response.d.ts +29 -0
- package/dist/mcp/mcp-response.js +67 -0
- package/dist/mcp/mcp-server.d.ts +19 -0
- package/dist/mcp/mcp-server.js +76 -0
- package/dist/mcp/resources/project-conventions.d.ts +4 -0
- package/dist/mcp/resources/project-conventions.js +50 -0
- package/dist/mcp/resources/task-current.d.ts +4 -0
- package/dist/mcp/resources/task-current.js +51 -0
- package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
- package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
- package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
- package/dist/mcp/tools/forge-ask-human.js +80 -0
- package/dist/mcp/tools/forge-column-done.d.ts +4 -0
- package/dist/mcp/tools/forge-column-done.js +46 -0
- package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
- package/dist/mcp/tools/forge-complete-step.js +139 -0
- package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task-group.js +22 -0
- package/dist/mcp/tools/forge-create-task.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task.js +28 -0
- package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task-group.js +17 -0
- package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task.js +17 -0
- package/dist/mcp/tools/forge-get-task.d.ts +4 -0
- package/dist/mcp/tools/forge-get-task.js +17 -0
- package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
- package/dist/mcp/tools/forge-merge-back.js +179 -0
- package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-release-merge.js +159 -0
- package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
- package/dist/mcp/tools/forge-release-notes.js +51 -0
- package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
- package/dist/mcp/tools/forge-release-tag.js +125 -0
- package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
- package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
- package/dist/mcp/tools/forge-requeue.d.ts +4 -0
- package/dist/mcp/tools/forge-requeue.js +39 -0
- package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
- package/dist/mcp/tools/forge-submit-pr.js +68 -0
- package/dist/mcp/tools/forge-update-status.d.ts +4 -0
- package/dist/mcp/tools/forge-update-status.js +77 -0
- package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task-group.js +21 -0
- package/dist/mcp/tools/forge-update-task.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task.js +25 -0
- package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-validate-merge.js +194 -0
- package/dist/mcp/types.d.ts +39 -0
- package/dist/mcp/types.js +8 -0
- package/dist/methodology/artifact-manager.d.ts +14 -0
- package/dist/methodology/artifact-manager.js +55 -0
- package/dist/orchestrator/agent-registry.d.ts +53 -0
- package/dist/orchestrator/agent-registry.js +96 -0
- package/dist/orchestrator/agent-spawner.d.ts +112 -0
- package/dist/orchestrator/agent-spawner.js +1518 -0
- package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
- package/dist/orchestrator/connectors/connector.d.ts +95 -0
- package/dist/orchestrator/connectors/connector.js +25 -0
- package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
- package/dist/orchestrator/connectors/docker-connector.js +337 -0
- package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
- package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
- package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/opencode-connector.js +39 -0
- package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/vm-connector.js +139 -0
- package/dist/orchestrator/dispatch-queue.d.ts +43 -0
- package/dist/orchestrator/dispatch-queue.js +93 -0
- package/dist/orchestrator/resume-handler.d.ts +34 -0
- package/dist/orchestrator/resume-handler.js +159 -0
- package/dist/orchestrator/stream-batcher.d.ts +16 -0
- package/dist/orchestrator/stream-batcher.js +50 -0
- package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
- package/dist/orchestrator/stream-json-extractor.js +579 -0
- package/dist/vm/claude-token-manager.d.ts +36 -0
- package/dist/vm/claude-token-manager.js +223 -0
- package/dist/vm/docker-executor.d.ts +131 -0
- package/dist/vm/docker-executor.js +360 -0
- package/dist/vm/docker-process-proxy.d.ts +31 -0
- package/dist/vm/docker-process-proxy.js +105 -0
- package/dist/vm/forge-vm-agent.d.ts +11 -0
- package/dist/vm/forge-vm-agent.js +159 -0
- package/dist/vm/git-credentials.d.ts +43 -0
- package/dist/vm/git-credentials.js +53 -0
- package/dist/vm/image-manager.d.ts +7 -0
- package/dist/vm/image-manager.js +61 -0
- package/dist/vm/log.d.ts +9 -0
- package/dist/vm/log.js +18 -0
- package/dist/vm/mcp-bridge.d.ts +63 -0
- package/dist/vm/mcp-bridge.js +243 -0
- package/dist/vm/mcp-stub.d.ts +15 -0
- package/dist/vm/mcp-stub.js +67 -0
- package/dist/vm/native-addon.d.ts +69 -0
- package/dist/vm/native-addon.js +68 -0
- package/dist/vm/provider-resolver.d.ts +7 -0
- package/dist/vm/provider-resolver.js +27 -0
- package/dist/vm/providers/lima.d.ts +17 -0
- package/dist/vm/providers/lima.js +191 -0
- package/dist/vm/providers/qemu.d.ts +28 -0
- package/dist/vm/providers/qemu.js +260 -0
- package/dist/vm/providers/vfkit.d.ts +40 -0
- package/dist/vm/providers/vfkit.js +371 -0
- package/dist/vm/providers/wsl2.d.ts +27 -0
- package/dist/vm/providers/wsl2.js +226 -0
- package/dist/vm/session-manager.d.ts +130 -0
- package/dist/vm/session-manager.js +494 -0
- package/dist/vm/session-snapshot.d.ts +31 -0
- package/dist/vm/session-snapshot.js +180 -0
- package/dist/vm/types.d.ts +73 -0
- package/dist/vm/types.js +2 -0
- package/dist/vm/vm-helper-client.d.ts +81 -0
- package/dist/vm/vm-helper-client.js +335 -0
- package/dist/vm/vm-helper.d.ts +11 -0
- package/dist/vm/vm-helper.js +156 -0
- package/dist/vm/vm-ipc-protocol.d.ts +84 -0
- package/dist/vm/vm-ipc-protocol.js +9 -0
- package/dist/vm/vm-manager-facade.d.ts +23 -0
- package/dist/vm/vm-manager-facade.js +43 -0
- package/dist/vm/vm-manager.d.ts +74 -0
- package/dist/vm/vm-manager.js +515 -0
- package/dist/vm/vm-process-proxy.d.ts +34 -0
- package/dist/vm/vm-process-proxy.js +64 -0
- package/dist/vm/vsock-bridge.d.ts +44 -0
- package/dist/vm/vsock-bridge.js +140 -0
- package/dist/vm/vsock-protocol.d.ts +98 -0
- package/dist/vm/vsock-protocol.js +57 -0
- package/package.json +65 -0
- package/rootfs/Dockerfile +50 -0
- package/rootfs/forge-mcp-relay.mjs +158 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { vmLog } from "../vm/log.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts text deltas from Claude Code's `--output-format stream-json` output.
|
|
4
|
+
*
|
|
5
|
+
* With `--include-partial-messages`, Claude emits newline-delimited JSON objects.
|
|
6
|
+
* Text tokens arrive as `stream_event` objects with `event.delta.type === "text_delta"`.
|
|
7
|
+
*
|
|
8
|
+
* Official filter pattern (from docs):
|
|
9
|
+
* select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text
|
|
10
|
+
*
|
|
11
|
+
* This extractor also handles:
|
|
12
|
+
* - `assistant` snapshot messages containing AskUserQuestion tool_use blocks:
|
|
13
|
+
* extracts the question text and emits it so the wizard chat user can see it.
|
|
14
|
+
* When `onQuestion` is provided, emits structured data instead of formatted text.
|
|
15
|
+
* - Ignores: thinking, other tool_use, system events, rate limits, etc.
|
|
16
|
+
*/
|
|
17
|
+
/** Tools we extract activity from, with their detail extractor. */
|
|
18
|
+
const TOOL_DETAIL_EXTRACTORS = {
|
|
19
|
+
Read: (input) => truncateEnd(input.file_path, 60),
|
|
20
|
+
Edit: (input) => truncateEnd(input.file_path, 60),
|
|
21
|
+
Write: (input) => truncateEnd(input.file_path, 60),
|
|
22
|
+
Bash: (input) => {
|
|
23
|
+
const cmd = input.command;
|
|
24
|
+
if (!cmd)
|
|
25
|
+
return null;
|
|
26
|
+
const firstLine = cmd.split("\n")[0] ?? cmd;
|
|
27
|
+
return firstLine.length > 80 ? firstLine.slice(0, 77) + "..." : firstLine;
|
|
28
|
+
},
|
|
29
|
+
Grep: (input) => {
|
|
30
|
+
const pattern = input.pattern;
|
|
31
|
+
if (!pattern)
|
|
32
|
+
return null;
|
|
33
|
+
const truncated = pattern.length > 40 ? pattern.slice(0, 37) + "..." : pattern;
|
|
34
|
+
return `"${truncated}"`;
|
|
35
|
+
},
|
|
36
|
+
Glob: (input) => {
|
|
37
|
+
const pattern = input.pattern;
|
|
38
|
+
if (!pattern)
|
|
39
|
+
return null;
|
|
40
|
+
return pattern.length > 60 ? pattern.slice(0, 57) + "..." : pattern;
|
|
41
|
+
},
|
|
42
|
+
WebFetch: (input) => {
|
|
43
|
+
const url = input.url;
|
|
44
|
+
if (!url)
|
|
45
|
+
return null;
|
|
46
|
+
return url.length > 60 ? url.slice(0, 57) + "..." : url;
|
|
47
|
+
},
|
|
48
|
+
WebSearch: (input) => {
|
|
49
|
+
const query = input.query;
|
|
50
|
+
if (!query)
|
|
51
|
+
return null;
|
|
52
|
+
return query.length > 60 ? query.slice(0, 57) + "..." : query;
|
|
53
|
+
},
|
|
54
|
+
Agent: (input) => {
|
|
55
|
+
const description = input.description;
|
|
56
|
+
if (!description)
|
|
57
|
+
return null;
|
|
58
|
+
return description.length > 60
|
|
59
|
+
? description.slice(0, 57) + "..."
|
|
60
|
+
: description;
|
|
61
|
+
},
|
|
62
|
+
// ForgeAI MCP tools
|
|
63
|
+
forge_create_task_group: (input) => truncateStart(input.name, 60),
|
|
64
|
+
forge_add_group_dependency: (input) => {
|
|
65
|
+
const blocking = input.blockingGroupName;
|
|
66
|
+
const blocked = input.blockedGroupName;
|
|
67
|
+
if (blocking && blocked)
|
|
68
|
+
return `${blocking} → ${blocked}`;
|
|
69
|
+
return blocking ?? blocked ?? null;
|
|
70
|
+
},
|
|
71
|
+
forge_create_task: (input) => truncateStart(input.title, 60),
|
|
72
|
+
forge_add_task_dependency: (input) => {
|
|
73
|
+
const blocking = input.blockingTaskTitle;
|
|
74
|
+
const blocked = input.blockedTaskTitle;
|
|
75
|
+
if (blocking && blocked)
|
|
76
|
+
return `${blocking} → ${blocked}`;
|
|
77
|
+
return blocking ?? blocked ?? null;
|
|
78
|
+
},
|
|
79
|
+
forge_complete_step: (input) => {
|
|
80
|
+
const deliverables = input.deliverables;
|
|
81
|
+
if (Array.isArray(deliverables))
|
|
82
|
+
return `${deliverables.length} deliverables`;
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
forge_get_task: () => null,
|
|
86
|
+
forge_update_status: (input) => input.status ?? null,
|
|
87
|
+
forge_ask_human: (input) => truncateStart(input.question, 60),
|
|
88
|
+
forge_requeue: (input) => truncateStart(input.reason, 60),
|
|
89
|
+
forge_column_done: (input) => truncateStart(input.taskId, 60),
|
|
90
|
+
};
|
|
91
|
+
function truncateEnd(value, maxLen) {
|
|
92
|
+
if (!value)
|
|
93
|
+
return null;
|
|
94
|
+
if (value.length <= maxLen)
|
|
95
|
+
return value;
|
|
96
|
+
return "..." + value.slice(-(maxLen - 3));
|
|
97
|
+
}
|
|
98
|
+
function truncateStart(value, maxLen) {
|
|
99
|
+
if (!value)
|
|
100
|
+
return null;
|
|
101
|
+
if (value.length <= maxLen)
|
|
102
|
+
return value;
|
|
103
|
+
return value.slice(0, maxLen - 3) + "...";
|
|
104
|
+
}
|
|
105
|
+
export class StreamJsonExtractor {
|
|
106
|
+
onText;
|
|
107
|
+
lineBuf = "";
|
|
108
|
+
/** Track which tool_use IDs we've already emitted (questions + activity). */
|
|
109
|
+
emittedToolUseIds = new Set();
|
|
110
|
+
/** Track which system event UUIDs we've already processed (sub-agent progress). */
|
|
111
|
+
emittedSystemUuids = new Set();
|
|
112
|
+
/** Incremented on each message_start — groups text from the same API turn. */
|
|
113
|
+
turnIndex = 0;
|
|
114
|
+
onQuestion;
|
|
115
|
+
onToolActivity;
|
|
116
|
+
onTokenUsage;
|
|
117
|
+
inputTokens = 0;
|
|
118
|
+
outputTokens = 0;
|
|
119
|
+
/** Error text from the last `type: "result"` event with `is_error: true`. */
|
|
120
|
+
lastError = null;
|
|
121
|
+
/** Session ID for log context (optional). */
|
|
122
|
+
sessionId;
|
|
123
|
+
constructor(onText, onQuestion, onToolActivity, onTokenUsage, sessionId) {
|
|
124
|
+
this.onText = onText;
|
|
125
|
+
this.onQuestion = onQuestion;
|
|
126
|
+
this.onToolActivity = onToolActivity;
|
|
127
|
+
this.onTokenUsage = onTokenUsage;
|
|
128
|
+
this.sessionId = sessionId;
|
|
129
|
+
}
|
|
130
|
+
write(data) {
|
|
131
|
+
this.lineBuf += data;
|
|
132
|
+
// Process complete lines (newline-delimited JSON)
|
|
133
|
+
let newlineIdx;
|
|
134
|
+
while ((newlineIdx = this.lineBuf.indexOf("\n")) !== -1) {
|
|
135
|
+
const line = this.lineBuf.slice(0, newlineIdx).trim();
|
|
136
|
+
this.lineBuf = this.lineBuf.slice(newlineIdx + 1);
|
|
137
|
+
if (line)
|
|
138
|
+
this.parseLine(line);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Flush any remaining partial line on process exit. */
|
|
142
|
+
flush() {
|
|
143
|
+
const line = this.lineBuf.trim();
|
|
144
|
+
this.lineBuf = "";
|
|
145
|
+
if (line)
|
|
146
|
+
this.parseLine(line);
|
|
147
|
+
}
|
|
148
|
+
parseLine(line) {
|
|
149
|
+
try {
|
|
150
|
+
const obj = JSON.parse(line);
|
|
151
|
+
const objType = obj.type;
|
|
152
|
+
switch (objType) {
|
|
153
|
+
case "stream_event":
|
|
154
|
+
this.handleStreamEvent(obj);
|
|
155
|
+
break;
|
|
156
|
+
case "assistant":
|
|
157
|
+
this.handleAssistantSnapshot(obj);
|
|
158
|
+
break;
|
|
159
|
+
case "result":
|
|
160
|
+
this.handleResult(obj);
|
|
161
|
+
break;
|
|
162
|
+
case "system":
|
|
163
|
+
this.handleSystemEvent(obj);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Not valid JSON — skip (could be stderr leaking into stdout)
|
|
169
|
+
vmLog.dim("stream-json", "Non-JSON line skipped", this.sessionId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
handleMessageStart(event) {
|
|
173
|
+
this.turnIndex++;
|
|
174
|
+
const message = event.message;
|
|
175
|
+
const usage = message?.usage;
|
|
176
|
+
const inputTokens = this.calculateInputTokens(usage);
|
|
177
|
+
vmLog.dim("stream-json", `message_start: turnIndex=${this.turnIndex}, input_tokens=${inputTokens}, raw=${JSON.stringify(usage)}, onTokenUsage=${!!this.onTokenUsage}`, this.sessionId);
|
|
178
|
+
if (inputTokens > 0 && this.onTokenUsage) {
|
|
179
|
+
this.inputTokens += inputTokens;
|
|
180
|
+
this.onTokenUsage({
|
|
181
|
+
inputTokens: this.inputTokens,
|
|
182
|
+
outputTokens: this.outputTokens,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
calculateInputTokens(usage) {
|
|
187
|
+
return ((typeof usage?.input_tokens === "number" ? usage.input_tokens : 0) +
|
|
188
|
+
(typeof usage?.cache_creation_input_tokens === "number"
|
|
189
|
+
? usage.cache_creation_input_tokens
|
|
190
|
+
: 0) +
|
|
191
|
+
(typeof usage?.cache_read_input_tokens === "number"
|
|
192
|
+
? usage.cache_read_input_tokens
|
|
193
|
+
: 0));
|
|
194
|
+
}
|
|
195
|
+
handleContentBlockStart(event) {
|
|
196
|
+
const contentBlock = event.content_block;
|
|
197
|
+
if (contentBlock?.type === "compaction") {
|
|
198
|
+
vmLog.dim("stream-json", "compaction block started", this.sessionId);
|
|
199
|
+
this.onToolActivity?.({
|
|
200
|
+
toolName: "Compaction",
|
|
201
|
+
toolDetail: "Summarizing conversation context...",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
handleMessageDelta(event) {
|
|
206
|
+
const usage = event.usage;
|
|
207
|
+
const outputTokens = usage?.output_tokens;
|
|
208
|
+
const outputTokensDisplay = typeof outputTokens === "number" ? String(outputTokens) : "N/A";
|
|
209
|
+
vmLog.dim("stream-json", `message_delta: output_tokens=${outputTokensDisplay}, onTokenUsage=${!!this.onTokenUsage}`, this.sessionId);
|
|
210
|
+
if (typeof outputTokens === "number" && this.onTokenUsage) {
|
|
211
|
+
this.outputTokens += outputTokens;
|
|
212
|
+
this.onTokenUsage({
|
|
213
|
+
inputTokens: this.inputTokens,
|
|
214
|
+
outputTokens: this.outputTokens,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
handleStreamEvent(obj) {
|
|
219
|
+
const event = obj.event;
|
|
220
|
+
if (!event)
|
|
221
|
+
return;
|
|
222
|
+
const eventType = event.type;
|
|
223
|
+
if (eventType === "message_start") {
|
|
224
|
+
this.handleMessageStart(event);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (eventType === "content_block_start") {
|
|
228
|
+
this.handleContentBlockStart(event);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (eventType === "message_delta") {
|
|
232
|
+
this.handleMessageDelta(event);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const delta = event.delta;
|
|
236
|
+
if (!delta)
|
|
237
|
+
return;
|
|
238
|
+
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
239
|
+
this.onText(delta.text);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Handle `type: "result"` event — contains final usage stats.
|
|
244
|
+
* Used as fallback if message_start/message_delta didn't provide usage.
|
|
245
|
+
*/
|
|
246
|
+
handleResult(obj) {
|
|
247
|
+
const result = obj.result;
|
|
248
|
+
if (!result)
|
|
249
|
+
return;
|
|
250
|
+
// Capture error text from result events (e.g. API billing errors)
|
|
251
|
+
// Structure: { type: "result", result: { is_error: true, error: "..." } }
|
|
252
|
+
// Fallback: error text may also be in result as a string (older formats)
|
|
253
|
+
if (result.is_error === true) {
|
|
254
|
+
if (typeof result.error === "string") {
|
|
255
|
+
this.lastError = result.error;
|
|
256
|
+
}
|
|
257
|
+
else if (typeof result.result === "string") {
|
|
258
|
+
this.lastError = result.result;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!this.onTokenUsage)
|
|
262
|
+
return;
|
|
263
|
+
const usage = result.usage;
|
|
264
|
+
if (!usage)
|
|
265
|
+
return;
|
|
266
|
+
// Include cache tokens for accurate input count
|
|
267
|
+
const rawInput = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
|
|
268
|
+
const cacheCreation = typeof usage.cache_creation_input_tokens === "number"
|
|
269
|
+
? usage.cache_creation_input_tokens
|
|
270
|
+
: 0;
|
|
271
|
+
const cacheRead = typeof usage.cache_read_input_tokens === "number"
|
|
272
|
+
? usage.cache_read_input_tokens
|
|
273
|
+
: 0;
|
|
274
|
+
const totalInput = rawInput + cacheCreation + cacheRead;
|
|
275
|
+
const outputTokens = usage.output_tokens;
|
|
276
|
+
if (totalInput > 0)
|
|
277
|
+
this.inputTokens = totalInput;
|
|
278
|
+
if (typeof outputTokens === "number")
|
|
279
|
+
this.outputTokens = outputTokens;
|
|
280
|
+
this.onTokenUsage({
|
|
281
|
+
inputTokens: this.inputTokens,
|
|
282
|
+
outputTokens: this.outputTokens,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
handleAskUserQuestion(blockId, questions) {
|
|
286
|
+
this.emittedToolUseIds.add(blockId);
|
|
287
|
+
if (this.onQuestion) {
|
|
288
|
+
const structured = this.extractStructuredQuestions(questions);
|
|
289
|
+
if (structured.length > 0) {
|
|
290
|
+
this.onQuestion(structured);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const formatted = this.formatQuestions(questions);
|
|
295
|
+
if (formatted) {
|
|
296
|
+
this.onText(formatted);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
handleToolActivity(blockId, name, input) {
|
|
301
|
+
if (!this.onToolActivity)
|
|
302
|
+
return;
|
|
303
|
+
const extractor = TOOL_DETAIL_EXTRACTORS[name];
|
|
304
|
+
const detail = extractor && input ? extractor(input) : null;
|
|
305
|
+
if (detail !== null || !extractor) {
|
|
306
|
+
this.emittedToolUseIds.add(blockId);
|
|
307
|
+
const toolUseId = name === "Agent" ? blockId : undefined;
|
|
308
|
+
this.onToolActivity({
|
|
309
|
+
toolName: name,
|
|
310
|
+
toolDetail: detail ?? name,
|
|
311
|
+
toolUseId,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
isValidToolUseBlock(block) {
|
|
316
|
+
if (block.type !== "tool_use")
|
|
317
|
+
return false;
|
|
318
|
+
if (typeof block.id !== "string")
|
|
319
|
+
return false;
|
|
320
|
+
if (this.emittedToolUseIds.has(block.id))
|
|
321
|
+
return false;
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Handle `type: "assistant"` snapshot messages.
|
|
326
|
+
* These contain cumulative content blocks after streaming completes for each block.
|
|
327
|
+
* We extract AskUserQuestion tool_use blocks and format questions as readable text.
|
|
328
|
+
*/
|
|
329
|
+
handleAssistantSnapshot(obj) {
|
|
330
|
+
const message = obj.message;
|
|
331
|
+
if (!message)
|
|
332
|
+
return;
|
|
333
|
+
const content = message.content;
|
|
334
|
+
if (!Array.isArray(content))
|
|
335
|
+
return;
|
|
336
|
+
for (const block of content) {
|
|
337
|
+
if (!this.isValidToolUseBlock(block))
|
|
338
|
+
continue;
|
|
339
|
+
const blockId = block.id;
|
|
340
|
+
const name = block.name;
|
|
341
|
+
if (!name)
|
|
342
|
+
continue;
|
|
343
|
+
const input = block.input;
|
|
344
|
+
if (name === "AskUserQuestion") {
|
|
345
|
+
if (!input)
|
|
346
|
+
continue;
|
|
347
|
+
const questions = input.questions;
|
|
348
|
+
if (!Array.isArray(questions) || questions.length === 0)
|
|
349
|
+
continue;
|
|
350
|
+
this.handleAskUserQuestion(blockId, questions);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
this.handleToolActivity(blockId, name, input);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Handle `type: "system"` events — sub-agent progress updates.
|
|
358
|
+
* System events with `last_tool_name` + `tool_use_id` represent tools used by sub-agents.
|
|
359
|
+
* `tool_use_id` references the parent Agent tool_use block id.
|
|
360
|
+
*/
|
|
361
|
+
handleSystemEvent(obj) {
|
|
362
|
+
if (!this.onToolActivity)
|
|
363
|
+
return;
|
|
364
|
+
const toolUseId = obj.tool_use_id;
|
|
365
|
+
const uuid = obj.uuid;
|
|
366
|
+
if (!toolUseId)
|
|
367
|
+
return;
|
|
368
|
+
// Deduplicate by uuid
|
|
369
|
+
if (uuid && this.emittedSystemUuids.has(uuid))
|
|
370
|
+
return;
|
|
371
|
+
if (uuid)
|
|
372
|
+
this.emittedSystemUuids.add(uuid);
|
|
373
|
+
const lastToolName = obj.last_tool_name;
|
|
374
|
+
const status = obj.status;
|
|
375
|
+
// Agent completion event (has status, no last_tool_name)
|
|
376
|
+
if (status && !lastToolName) {
|
|
377
|
+
this.onToolActivity({
|
|
378
|
+
toolName: "__agent_complete__",
|
|
379
|
+
toolDetail: "",
|
|
380
|
+
parentToolUseId: toolUseId,
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Sub-agent progress event (has last_tool_name)
|
|
385
|
+
if (!lastToolName)
|
|
386
|
+
return;
|
|
387
|
+
const description = obj.description;
|
|
388
|
+
let detail;
|
|
389
|
+
if (!description) {
|
|
390
|
+
detail = lastToolName;
|
|
391
|
+
}
|
|
392
|
+
else if (description.length > 60) {
|
|
393
|
+
detail = description.slice(0, 57) + "...";
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
detail = description;
|
|
397
|
+
}
|
|
398
|
+
this.onToolActivity({
|
|
399
|
+
toolName: lastToolName,
|
|
400
|
+
toolDetail: detail,
|
|
401
|
+
parentToolUseId: toolUseId,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
extractStructuredQuestions(questions) {
|
|
405
|
+
const result = [];
|
|
406
|
+
for (const q of questions) {
|
|
407
|
+
const question = q.question;
|
|
408
|
+
if (!question)
|
|
409
|
+
continue;
|
|
410
|
+
const data = { question };
|
|
411
|
+
if (typeof q.header === "string") {
|
|
412
|
+
data.header = q.header;
|
|
413
|
+
}
|
|
414
|
+
if (typeof q.multiSelect === "boolean") {
|
|
415
|
+
data.multiSelect = q.multiSelect;
|
|
416
|
+
}
|
|
417
|
+
const options = q.options;
|
|
418
|
+
if (Array.isArray(options)) {
|
|
419
|
+
data.options = options
|
|
420
|
+
.filter((opt) => typeof opt.label === "string")
|
|
421
|
+
.map((opt) => ({
|
|
422
|
+
label: opt.label,
|
|
423
|
+
...(typeof opt.description === "string"
|
|
424
|
+
? { description: opt.description }
|
|
425
|
+
: {}),
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
result.push(data);
|
|
429
|
+
}
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
formatOptionLine(label, description) {
|
|
433
|
+
if (!label)
|
|
434
|
+
return null;
|
|
435
|
+
return description ? `- ${label}: ${description}` : `- ${label}`;
|
|
436
|
+
}
|
|
437
|
+
formatQuestionOptions(options) {
|
|
438
|
+
if (!Array.isArray(options))
|
|
439
|
+
return [];
|
|
440
|
+
const lines = [];
|
|
441
|
+
for (const opt of options) {
|
|
442
|
+
const label = opt.label;
|
|
443
|
+
const desc = opt.description;
|
|
444
|
+
const line = this.formatOptionLine(label, desc);
|
|
445
|
+
if (line)
|
|
446
|
+
lines.push(line);
|
|
447
|
+
}
|
|
448
|
+
return lines;
|
|
449
|
+
}
|
|
450
|
+
formatQuestions(questions) {
|
|
451
|
+
const parts = [];
|
|
452
|
+
for (const q of questions) {
|
|
453
|
+
const question = q.question;
|
|
454
|
+
if (!question)
|
|
455
|
+
continue;
|
|
456
|
+
parts.push(`\n${question}`);
|
|
457
|
+
const options = q.options;
|
|
458
|
+
parts.push(...this.formatQuestionOptions(options));
|
|
459
|
+
}
|
|
460
|
+
return parts.length > 0 ? "\n" + parts.join("\n") + "\n" : "";
|
|
461
|
+
}
|
|
462
|
+
// --- Billing / token error detection ---
|
|
463
|
+
static BILLING_PATTERNS = [
|
|
464
|
+
/credit.{0,20}balance/i,
|
|
465
|
+
/insufficient.{0,20}(?:credit|fund|balance|quota)/i,
|
|
466
|
+
/exceed.{0,20}(?:quota|budget|limit)/i,
|
|
467
|
+
/usage.{0,20}limit.{0,20}reached/i,
|
|
468
|
+
/limit.{0,20}(?:will )?reset/i,
|
|
469
|
+
/(?:hit|reached).{0,10}(?:your )?limit/i,
|
|
470
|
+
/billing/i,
|
|
471
|
+
/payment.{0,20}(?:required|failed|method)/i,
|
|
472
|
+
/account.{0,20}(?:suspended|deactivated)/i,
|
|
473
|
+
/out of (?:credit|token)/i,
|
|
474
|
+
];
|
|
475
|
+
/**
|
|
476
|
+
* Check if the captured error (from result event or accumulated stderr)
|
|
477
|
+
* indicates a billing/token exhaustion issue.
|
|
478
|
+
* Optionally accepts extra text (e.g. stderr buffer) to scan.
|
|
479
|
+
*/
|
|
480
|
+
isBillingError(extraText) {
|
|
481
|
+
const texts = [this.lastError, extraText].filter(Boolean);
|
|
482
|
+
return texts.some((text) => StreamJsonExtractor.BILLING_PATTERNS.some((re) => re.test(text)));
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Extract the usage-limit reset timestamp from error text.
|
|
486
|
+
* Handles multiple formats:
|
|
487
|
+
* - "reset at 2026-03-08T15:00:00+00:00" (ISO datetime)
|
|
488
|
+
* - "resets 7pm (Europe/Paris)" (time + timezone)
|
|
489
|
+
* - "resets in 2 hours" (relative duration)
|
|
490
|
+
* Returns an ISO string if a valid future timestamp is found, otherwise null.
|
|
491
|
+
*/
|
|
492
|
+
extractResetTimestamp(extraText) {
|
|
493
|
+
const texts = [this.lastError, extraText].filter(Boolean);
|
|
494
|
+
for (const text of texts) {
|
|
495
|
+
// Pattern 1: "reset at <ISO datetime>"
|
|
496
|
+
const isoMatch = /reset\s+at\s+(.+?)(?:\.|$)/i.exec(text);
|
|
497
|
+
if (isoMatch) {
|
|
498
|
+
const dateStr = isoMatch[1].trim();
|
|
499
|
+
const parsed = new Date(dateStr);
|
|
500
|
+
if (!Number.isNaN(parsed.getTime()) && parsed.getTime() > Date.now()) {
|
|
501
|
+
return parsed.toISOString();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Pattern 2: "resets 7pm (Europe/Paris)" or "resets 19:00 (Europe/Paris)"
|
|
505
|
+
const tzMatch = /resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(([^)]+)\)/i.exec(text);
|
|
506
|
+
if (tzMatch) {
|
|
507
|
+
const result = StreamJsonExtractor.resolveTimeInTimezone(tzMatch[1], tzMatch[2], tzMatch[3], tzMatch[4]);
|
|
508
|
+
if (result)
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
// Pattern 3: "resets in 2 hours" or "resets in 30 minutes"
|
|
512
|
+
const relMatch = /resets?\s+in\s+(\d+)\s*(hour|hr|minute|min)s?/i.exec(text);
|
|
513
|
+
if (relMatch) {
|
|
514
|
+
const amount = parseInt(relMatch[1], 10);
|
|
515
|
+
const unit = relMatch[2].toLowerCase();
|
|
516
|
+
const ms = unit.startsWith("hour") || unit === "hr"
|
|
517
|
+
? amount * 3600_000
|
|
518
|
+
: amount * 60_000;
|
|
519
|
+
const future = new Date(Date.now() + ms);
|
|
520
|
+
return future.toISOString();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Resolve a time-of-day (e.g. "7pm") in a named timezone to an ISO string.
|
|
527
|
+
* Uses Intl.DateTimeFormat to avoid external dependencies.
|
|
528
|
+
*/
|
|
529
|
+
static resolveTimeInTimezone(hourStr, minuteStr, ampm, timezone) {
|
|
530
|
+
try {
|
|
531
|
+
let hour = parseInt(hourStr, 10);
|
|
532
|
+
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
|
|
533
|
+
if (ampm) {
|
|
534
|
+
const isPm = ampm.toLowerCase() === "pm";
|
|
535
|
+
if (isPm && hour < 12)
|
|
536
|
+
hour += 12;
|
|
537
|
+
if (!isPm && hour === 12)
|
|
538
|
+
hour = 0;
|
|
539
|
+
}
|
|
540
|
+
// Get current date in the target timezone
|
|
541
|
+
const now = new Date();
|
|
542
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
543
|
+
timeZone: timezone,
|
|
544
|
+
year: "numeric",
|
|
545
|
+
month: "2-digit",
|
|
546
|
+
day: "2-digit",
|
|
547
|
+
hour: "2-digit",
|
|
548
|
+
minute: "2-digit",
|
|
549
|
+
hour12: false,
|
|
550
|
+
});
|
|
551
|
+
const parts = formatter.formatToParts(now);
|
|
552
|
+
const get = (type) => parseInt(parts.find((p) => p.type === type)?.value ?? "0", 10);
|
|
553
|
+
const tzYear = get("year");
|
|
554
|
+
const tzMonth = get("month");
|
|
555
|
+
const tzDay = get("day");
|
|
556
|
+
const tzHour = get("hour");
|
|
557
|
+
const tzMinute = get("minute");
|
|
558
|
+
// Build target time in the timezone's "local" frame
|
|
559
|
+
// Then compute the UTC offset to convert to absolute time
|
|
560
|
+
const localNowMs = Date.UTC(tzYear, tzMonth - 1, tzDay, tzHour, tzMinute);
|
|
561
|
+
const offsetMs = localNowMs - now.getTime();
|
|
562
|
+
// Target time today in timezone-local frame
|
|
563
|
+
let targetLocalMs = Date.UTC(tzYear, tzMonth - 1, tzDay, hour, minute);
|
|
564
|
+
// If already past, try tomorrow
|
|
565
|
+
if (targetLocalMs - offsetMs <= now.getTime()) {
|
|
566
|
+
targetLocalMs += 86_400_000;
|
|
567
|
+
}
|
|
568
|
+
const result = new Date(targetLocalMs - offsetMs);
|
|
569
|
+
if (result.getTime() > Date.now()) {
|
|
570
|
+
return result.toISOString();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
// Invalid timezone or parse error — fall through
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
//# sourceMappingURL=stream-json-extractor.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude OAuth Token Manager — secure storage + interactive setup.
|
|
3
|
+
*
|
|
4
|
+
* Uses `claude setup-token` to obtain a long-lived OAuth token (1 year)
|
|
5
|
+
* for programmatic use in Docker containers.
|
|
6
|
+
*
|
|
7
|
+
* Storage:
|
|
8
|
+
* - macOS: Keychain via `security` command (service: "ForgeAI-claude-oauth")
|
|
9
|
+
* - Linux/Windows: encrypted file at ~/.forgeai/auth/claude-oauth.json
|
|
10
|
+
*
|
|
11
|
+
* Flow:
|
|
12
|
+
* 1. ensureClaudeToken() checks for stored token
|
|
13
|
+
* 2. If absent, runs `claude setup-token` interactively
|
|
14
|
+
* 3. Captures the token from stdout
|
|
15
|
+
* 4. Saves to Keychain (macOS) or encrypted file (other platforms)
|
|
16
|
+
* 5. Returns the token for injection into containers
|
|
17
|
+
*/
|
|
18
|
+
/** Store the Claude OAuth token securely. Keychain on macOS, encrypted file elsewhere. */
|
|
19
|
+
export declare function saveClaudeToken(token: string): Promise<void>;
|
|
20
|
+
/** Load the Claude OAuth token. Returns null if not found or invalid. */
|
|
21
|
+
export declare function loadClaudeToken(): Promise<string | null>;
|
|
22
|
+
/** Check if a Claude OAuth token is stored. */
|
|
23
|
+
export declare function hasClaudeToken(): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Run `claude setup-token` interactively to obtain a long-lived OAuth token.
|
|
26
|
+
* The user must be logged into Claude Code on this machine.
|
|
27
|
+
* Returns the token string, or throws if the process fails.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runClaudeSetupToken(): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Ensure a Claude OAuth token is available.
|
|
32
|
+
* If not stored, runs `claude setup-token` interactively.
|
|
33
|
+
* Returns the token.
|
|
34
|
+
*/
|
|
35
|
+
export declare function ensureClaudeToken(): Promise<string>;
|
|
36
|
+
//# sourceMappingURL=claude-token-manager.d.ts.map
|