@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.
Files changed (221) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +80 -0
  3. package/dist/bin/forgeai.d.ts +3 -0
  4. package/dist/bin/forgeai.js +8 -0
  5. package/dist/cli.d.ts +4 -0
  6. package/dist/cli.js +180 -0
  7. package/dist/commands/auth-login.d.ts +4 -0
  8. package/dist/commands/auth-login.js +202 -0
  9. package/dist/commands/auth-logout.d.ts +2 -0
  10. package/dist/commands/auth-logout.js +41 -0
  11. package/dist/commands/auth-status.d.ts +4 -0
  12. package/dist/commands/auth-status.js +81 -0
  13. package/dist/commands/cli-list.d.ts +4 -0
  14. package/dist/commands/cli-list.js +40 -0
  15. package/dist/commands/cli-unlink.d.ts +4 -0
  16. package/dist/commands/cli-unlink.js +45 -0
  17. package/dist/commands/config-set.d.ts +5 -0
  18. package/dist/commands/config-set.js +63 -0
  19. package/dist/commands/connect.d.ts +9 -0
  20. package/dist/commands/connect.js +165 -0
  21. package/dist/commands/disconnect.d.ts +5 -0
  22. package/dist/commands/disconnect.js +126 -0
  23. package/dist/commands/journal-clear.d.ts +2 -0
  24. package/dist/commands/journal-clear.js +16 -0
  25. package/dist/commands/journal-list.d.ts +2 -0
  26. package/dist/commands/journal-list.js +20 -0
  27. package/dist/commands/journal-status.d.ts +2 -0
  28. package/dist/commands/journal-status.js +18 -0
  29. package/dist/commands/mcp-serve.d.ts +13 -0
  30. package/dist/commands/mcp-serve.js +74 -0
  31. package/dist/commands/org-list.d.ts +4 -0
  32. package/dist/commands/org-list.js +56 -0
  33. package/dist/commands/org-unlink.d.ts +4 -0
  34. package/dist/commands/org-unlink.js +63 -0
  35. package/dist/commands/reconnect.d.ts +4 -0
  36. package/dist/commands/reconnect.js +46 -0
  37. package/dist/commands/release-open.d.ts +2 -0
  38. package/dist/commands/release-open.js +43 -0
  39. package/dist/commands/session-respond.d.ts +2 -0
  40. package/dist/commands/session-respond.js +29 -0
  41. package/dist/commands/task-open.d.ts +2 -0
  42. package/dist/commands/task-open.js +42 -0
  43. package/dist/commands/vm.d.ts +21 -0
  44. package/dist/commands/vm.js +122 -0
  45. package/dist/commands/workflow-open.d.ts +2 -0
  46. package/dist/commands/workflow-open.js +43 -0
  47. package/dist/config/config-manager.d.ts +31 -0
  48. package/dist/config/config-manager.js +70 -0
  49. package/dist/config/token-manager.d.ts +11 -0
  50. package/dist/config/token-manager.js +87 -0
  51. package/dist/index.d.ts +11 -0
  52. package/dist/index.js +22 -0
  53. package/dist/lib/auth-client.d.ts +1670 -0
  54. package/dist/lib/auth-client.js +10 -0
  55. package/dist/lib/branch-manager.d.ts +79 -0
  56. package/dist/lib/branch-manager.js +620 -0
  57. package/dist/lib/clone-and-checkout.d.ts +6 -0
  58. package/dist/lib/clone-and-checkout.js +41 -0
  59. package/dist/lib/connection-manager.d.ts +58 -0
  60. package/dist/lib/connection-manager.js +436 -0
  61. package/dist/lib/daemon-entry.d.ts +2 -0
  62. package/dist/lib/daemon-entry.js +210 -0
  63. package/dist/lib/daemon.d.ts +38 -0
  64. package/dist/lib/daemon.js +148 -0
  65. package/dist/lib/event-journal.d.ts +24 -0
  66. package/dist/lib/event-journal.js +76 -0
  67. package/dist/lib/ipc-client.d.ts +10 -0
  68. package/dist/lib/ipc-client.js +70 -0
  69. package/dist/lib/ipc-server.d.ts +36 -0
  70. package/dist/lib/ipc-server.js +70 -0
  71. package/dist/lib/node-version-guard.d.ts +8 -0
  72. package/dist/lib/node-version-guard.js +60 -0
  73. package/dist/lib/open-directory.d.ts +7 -0
  74. package/dist/lib/open-directory.js +41 -0
  75. package/dist/lib/repo-manager.d.ts +29 -0
  76. package/dist/lib/repo-manager.js +103 -0
  77. package/dist/lib/resilient-emitter.d.ts +53 -0
  78. package/dist/lib/resilient-emitter.js +160 -0
  79. package/dist/lib/rest-client.d.ts +164 -0
  80. package/dist/lib/rest-client.js +139 -0
  81. package/dist/lib/skill-manager.d.ts +23 -0
  82. package/dist/lib/skill-manager.js +167 -0
  83. package/dist/lib/socket-client.d.ts +68 -0
  84. package/dist/lib/socket-client.js +160 -0
  85. package/dist/mcp/mcp-probe.d.ts +11 -0
  86. package/dist/mcp/mcp-probe.js +106 -0
  87. package/dist/mcp/mcp-response.d.ts +29 -0
  88. package/dist/mcp/mcp-response.js +67 -0
  89. package/dist/mcp/mcp-server.d.ts +19 -0
  90. package/dist/mcp/mcp-server.js +76 -0
  91. package/dist/mcp/resources/project-conventions.d.ts +4 -0
  92. package/dist/mcp/resources/project-conventions.js +50 -0
  93. package/dist/mcp/resources/task-current.d.ts +4 -0
  94. package/dist/mcp/resources/task-current.js +51 -0
  95. package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
  96. package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
  97. package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
  98. package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
  99. package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
  100. package/dist/mcp/tools/forge-ask-human.js +80 -0
  101. package/dist/mcp/tools/forge-column-done.d.ts +4 -0
  102. package/dist/mcp/tools/forge-column-done.js +46 -0
  103. package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
  104. package/dist/mcp/tools/forge-complete-step.js +139 -0
  105. package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
  106. package/dist/mcp/tools/forge-create-task-group.js +22 -0
  107. package/dist/mcp/tools/forge-create-task.d.ts +4 -0
  108. package/dist/mcp/tools/forge-create-task.js +28 -0
  109. package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
  110. package/dist/mcp/tools/forge-delete-task-group.js +17 -0
  111. package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
  112. package/dist/mcp/tools/forge-delete-task.js +17 -0
  113. package/dist/mcp/tools/forge-get-task.d.ts +4 -0
  114. package/dist/mcp/tools/forge-get-task.js +17 -0
  115. package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
  116. package/dist/mcp/tools/forge-merge-back.js +179 -0
  117. package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
  118. package/dist/mcp/tools/forge-release-merge.js +159 -0
  119. package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
  120. package/dist/mcp/tools/forge-release-notes.js +51 -0
  121. package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
  122. package/dist/mcp/tools/forge-release-tag.js +125 -0
  123. package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
  124. package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
  125. package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
  126. package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
  127. package/dist/mcp/tools/forge-requeue.d.ts +4 -0
  128. package/dist/mcp/tools/forge-requeue.js +39 -0
  129. package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
  130. package/dist/mcp/tools/forge-submit-pr.js +68 -0
  131. package/dist/mcp/tools/forge-update-status.d.ts +4 -0
  132. package/dist/mcp/tools/forge-update-status.js +77 -0
  133. package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
  134. package/dist/mcp/tools/forge-update-task-group.js +21 -0
  135. package/dist/mcp/tools/forge-update-task.d.ts +4 -0
  136. package/dist/mcp/tools/forge-update-task.js +25 -0
  137. package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
  138. package/dist/mcp/tools/forge-validate-merge.js +194 -0
  139. package/dist/mcp/types.d.ts +39 -0
  140. package/dist/mcp/types.js +8 -0
  141. package/dist/methodology/artifact-manager.d.ts +14 -0
  142. package/dist/methodology/artifact-manager.js +55 -0
  143. package/dist/orchestrator/agent-registry.d.ts +53 -0
  144. package/dist/orchestrator/agent-registry.js +96 -0
  145. package/dist/orchestrator/agent-spawner.d.ts +112 -0
  146. package/dist/orchestrator/agent-spawner.js +1518 -0
  147. package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
  148. package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
  149. package/dist/orchestrator/connectors/connector.d.ts +95 -0
  150. package/dist/orchestrator/connectors/connector.js +25 -0
  151. package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
  152. package/dist/orchestrator/connectors/docker-connector.js +337 -0
  153. package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
  154. package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
  155. package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
  156. package/dist/orchestrator/connectors/opencode-connector.js +39 -0
  157. package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
  158. package/dist/orchestrator/connectors/vm-connector.js +139 -0
  159. package/dist/orchestrator/dispatch-queue.d.ts +43 -0
  160. package/dist/orchestrator/dispatch-queue.js +93 -0
  161. package/dist/orchestrator/resume-handler.d.ts +34 -0
  162. package/dist/orchestrator/resume-handler.js +159 -0
  163. package/dist/orchestrator/stream-batcher.d.ts +16 -0
  164. package/dist/orchestrator/stream-batcher.js +50 -0
  165. package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
  166. package/dist/orchestrator/stream-json-extractor.js +579 -0
  167. package/dist/vm/claude-token-manager.d.ts +36 -0
  168. package/dist/vm/claude-token-manager.js +223 -0
  169. package/dist/vm/docker-executor.d.ts +131 -0
  170. package/dist/vm/docker-executor.js +360 -0
  171. package/dist/vm/docker-process-proxy.d.ts +31 -0
  172. package/dist/vm/docker-process-proxy.js +105 -0
  173. package/dist/vm/forge-vm-agent.d.ts +11 -0
  174. package/dist/vm/forge-vm-agent.js +159 -0
  175. package/dist/vm/git-credentials.d.ts +43 -0
  176. package/dist/vm/git-credentials.js +53 -0
  177. package/dist/vm/image-manager.d.ts +7 -0
  178. package/dist/vm/image-manager.js +61 -0
  179. package/dist/vm/log.d.ts +9 -0
  180. package/dist/vm/log.js +18 -0
  181. package/dist/vm/mcp-bridge.d.ts +63 -0
  182. package/dist/vm/mcp-bridge.js +243 -0
  183. package/dist/vm/mcp-stub.d.ts +15 -0
  184. package/dist/vm/mcp-stub.js +67 -0
  185. package/dist/vm/native-addon.d.ts +69 -0
  186. package/dist/vm/native-addon.js +68 -0
  187. package/dist/vm/provider-resolver.d.ts +7 -0
  188. package/dist/vm/provider-resolver.js +27 -0
  189. package/dist/vm/providers/lima.d.ts +17 -0
  190. package/dist/vm/providers/lima.js +191 -0
  191. package/dist/vm/providers/qemu.d.ts +28 -0
  192. package/dist/vm/providers/qemu.js +260 -0
  193. package/dist/vm/providers/vfkit.d.ts +40 -0
  194. package/dist/vm/providers/vfkit.js +371 -0
  195. package/dist/vm/providers/wsl2.d.ts +27 -0
  196. package/dist/vm/providers/wsl2.js +226 -0
  197. package/dist/vm/session-manager.d.ts +130 -0
  198. package/dist/vm/session-manager.js +494 -0
  199. package/dist/vm/session-snapshot.d.ts +31 -0
  200. package/dist/vm/session-snapshot.js +180 -0
  201. package/dist/vm/types.d.ts +73 -0
  202. package/dist/vm/types.js +2 -0
  203. package/dist/vm/vm-helper-client.d.ts +81 -0
  204. package/dist/vm/vm-helper-client.js +335 -0
  205. package/dist/vm/vm-helper.d.ts +11 -0
  206. package/dist/vm/vm-helper.js +156 -0
  207. package/dist/vm/vm-ipc-protocol.d.ts +84 -0
  208. package/dist/vm/vm-ipc-protocol.js +9 -0
  209. package/dist/vm/vm-manager-facade.d.ts +23 -0
  210. package/dist/vm/vm-manager-facade.js +43 -0
  211. package/dist/vm/vm-manager.d.ts +74 -0
  212. package/dist/vm/vm-manager.js +515 -0
  213. package/dist/vm/vm-process-proxy.d.ts +34 -0
  214. package/dist/vm/vm-process-proxy.js +64 -0
  215. package/dist/vm/vsock-bridge.d.ts +44 -0
  216. package/dist/vm/vsock-bridge.js +140 -0
  217. package/dist/vm/vsock-protocol.d.ts +98 -0
  218. package/dist/vm/vsock-protocol.js +57 -0
  219. package/package.json +65 -0
  220. package/rootfs/Dockerfile +50 -0
  221. 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