agor-live 0.3.7

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 (297) hide show
  1. package/LICENSE +94 -0
  2. package/README.md +163 -0
  3. package/bin/agor-daemon.js +20 -0
  4. package/bin/agor.js +14 -0
  5. package/dist/cli/base-command.d.ts +29 -0
  6. package/dist/cli/base-command.js +41 -0
  7. package/dist/cli/commands/board/add-session.d.ts +15 -0
  8. package/dist/cli/commands/board/add-session.js +102 -0
  9. package/dist/cli/commands/board/list.d.ts +14 -0
  10. package/dist/cli/commands/board/list.js +74 -0
  11. package/dist/cli/commands/config/clear.d.ts +13 -0
  12. package/dist/cli/commands/config/clear.js +21 -0
  13. package/dist/cli/commands/config/get.d.ts +13 -0
  14. package/dist/cli/commands/config/get.js +41 -0
  15. package/dist/cli/commands/config/index.d.ts +13 -0
  16. package/dist/cli/commands/config/index.js +118 -0
  17. package/dist/cli/commands/config/set.d.ts +14 -0
  18. package/dist/cli/commands/config/set.js +50 -0
  19. package/dist/cli/commands/config/unset.d.ts +13 -0
  20. package/dist/cli/commands/config/unset.js +35 -0
  21. package/dist/cli/commands/daemon/index.d.ts +13 -0
  22. package/dist/cli/commands/daemon/index.js +65 -0
  23. package/dist/cli/commands/daemon/logs.d.ts +13 -0
  24. package/dist/cli/commands/daemon/logs.js +78 -0
  25. package/dist/cli/commands/daemon/restart.d.ts +13 -0
  26. package/dist/cli/commands/daemon/restart.js +177 -0
  27. package/dist/cli/commands/daemon/start.d.ts +13 -0
  28. package/dist/cli/commands/daemon/start.js +193 -0
  29. package/dist/cli/commands/daemon/status.d.ts +13 -0
  30. package/dist/cli/commands/daemon/status.js +93 -0
  31. package/dist/cli/commands/daemon/stop.d.ts +13 -0
  32. package/dist/cli/commands/daemon/stop.js +108 -0
  33. package/dist/cli/commands/init.d.ts +44 -0
  34. package/dist/cli/commands/init.js +459 -0
  35. package/dist/cli/commands/mcp/add.d.ts +26 -0
  36. package/dist/cli/commands/mcp/add.js +162 -0
  37. package/dist/cli/commands/mcp/list.d.ts +16 -0
  38. package/dist/cli/commands/mcp/list.js +89 -0
  39. package/dist/cli/commands/mcp/remove.d.ts +17 -0
  40. package/dist/cli/commands/mcp/remove.js +86 -0
  41. package/dist/cli/commands/mcp/show.d.ts +14 -0
  42. package/dist/cli/commands/mcp/show.js +131 -0
  43. package/dist/cli/commands/repo/add.d.ts +16 -0
  44. package/dist/cli/commands/repo/add.js +105 -0
  45. package/dist/cli/commands/repo/list.d.ts +17 -0
  46. package/dist/cli/commands/repo/list.js +99 -0
  47. package/dist/cli/commands/repo/rm.d.ts +17 -0
  48. package/dist/cli/commands/repo/rm.js +126 -0
  49. package/dist/cli/commands/repo/worktree/add.d.ts +21 -0
  50. package/dist/cli/commands/repo/worktree/add.js +145 -0
  51. package/dist/cli/commands/repo/worktree/list.d.ts +21 -0
  52. package/dist/cli/commands/repo/worktree/list.js +136 -0
  53. package/dist/cli/commands/session/list.d.ts +30 -0
  54. package/dist/cli/commands/session/list.js +204 -0
  55. package/dist/cli/commands/session/load-claude.d.ts +16 -0
  56. package/dist/cli/commands/session/load-claude.js +211 -0
  57. package/dist/cli/commands/user/create-admin.d.ts +13 -0
  58. package/dist/cli/commands/user/create-admin.js +65 -0
  59. package/dist/cli/commands/user/create.d.ts +16 -0
  60. package/dist/cli/commands/user/create.js +126 -0
  61. package/dist/cli/commands/user/delete.d.ts +16 -0
  62. package/dist/cli/commands/user/delete.js +77 -0
  63. package/dist/cli/commands/user/list.d.ts +13 -0
  64. package/dist/cli/commands/user/list.js +78 -0
  65. package/dist/cli/commands/user/update.d.ts +19 -0
  66. package/dist/cli/commands/user/update.js +149 -0
  67. package/dist/cli/hooks/command-not-found.d.ts +9 -0
  68. package/dist/cli/hooks/command-not-found.js +14 -0
  69. package/dist/cli/lib/banner.d.ts +25 -0
  70. package/dist/cli/lib/banner.js +25 -0
  71. package/dist/cli/lib/context.d.ts +27 -0
  72. package/dist/cli/lib/context.js +32 -0
  73. package/dist/cli/lib/daemon-manager.d.ts +48 -0
  74. package/dist/cli/lib/daemon-manager.js +109 -0
  75. package/dist/cli/lib/help.d.ts +13 -0
  76. package/dist/cli/lib/help.js +46 -0
  77. package/dist/core/agentic-tool-B_gFNpk5.d.ts +33 -0
  78. package/dist/core/agentic-tool-DsyX8diw.d.cts +33 -0
  79. package/dist/core/api/index.cjs +98 -0
  80. package/dist/core/api/index.d.cts +174 -0
  81. package/dist/core/api/index.d.ts +174 -0
  82. package/dist/core/api/index.js +62 -0
  83. package/dist/core/board-comment-BUm0fpmD.d.cts +134 -0
  84. package/dist/core/board-comment-gC_-twPx.d.ts +134 -0
  85. package/dist/core/claude/index.cjs +673 -0
  86. package/dist/core/claude/index.d.cts +124 -0
  87. package/dist/core/claude/index.d.ts +124 -0
  88. package/dist/core/claude/index.js +629 -0
  89. package/dist/core/config/browser.cjs +165 -0
  90. package/dist/core/config/browser.d.cts +289 -0
  91. package/dist/core/config/browser.d.ts +289 -0
  92. package/dist/core/config/browser.js +131 -0
  93. package/dist/core/config/index.cjs +518 -0
  94. package/dist/core/config/index.d.cts +246 -0
  95. package/dist/core/config/index.d.ts +246 -0
  96. package/dist/core/config/index.js +451 -0
  97. package/dist/core/db/index.cjs +3726 -0
  98. package/dist/core/db/index.d.cts +631 -0
  99. package/dist/core/db/index.d.ts +631 -0
  100. package/dist/core/db/index.js +3649 -0
  101. package/dist/core/dist/agentic-tool-B_gFNpk5.d.ts +33 -0
  102. package/dist/core/dist/agentic-tool-DsyX8diw.d.cts +33 -0
  103. package/dist/core/dist/api/index.cjs +98 -0
  104. package/dist/core/dist/api/index.d.cts +174 -0
  105. package/dist/core/dist/api/index.d.ts +174 -0
  106. package/dist/core/dist/api/index.js +62 -0
  107. package/dist/core/dist/board-comment-BUm0fpmD.d.cts +134 -0
  108. package/dist/core/dist/board-comment-gC_-twPx.d.ts +134 -0
  109. package/dist/core/dist/claude/index.cjs +673 -0
  110. package/dist/core/dist/claude/index.d.cts +124 -0
  111. package/dist/core/dist/claude/index.d.ts +124 -0
  112. package/dist/core/dist/claude/index.js +629 -0
  113. package/dist/core/dist/config/browser.cjs +165 -0
  114. package/dist/core/dist/config/browser.d.cts +289 -0
  115. package/dist/core/dist/config/browser.d.ts +289 -0
  116. package/dist/core/dist/config/browser.js +131 -0
  117. package/dist/core/dist/config/index.cjs +518 -0
  118. package/dist/core/dist/config/index.d.cts +246 -0
  119. package/dist/core/dist/config/index.d.ts +246 -0
  120. package/dist/core/dist/config/index.js +451 -0
  121. package/dist/core/dist/db/index.cjs +3726 -0
  122. package/dist/core/dist/db/index.d.cts +631 -0
  123. package/dist/core/dist/db/index.d.ts +631 -0
  124. package/dist/core/dist/db/index.js +3649 -0
  125. package/dist/core/dist/environment/variable-resolver.cjs +92 -0
  126. package/dist/core/dist/environment/variable-resolver.d.cts +52 -0
  127. package/dist/core/dist/environment/variable-resolver.d.ts +52 -0
  128. package/dist/core/dist/environment/variable-resolver.js +53 -0
  129. package/dist/core/dist/feathers/index.cjs +66 -0
  130. package/dist/core/dist/feathers/index.d.cts +7 -0
  131. package/dist/core/dist/feathers/index.d.ts +7 -0
  132. package/dist/core/dist/feathers/index.js +25 -0
  133. package/dist/core/dist/feathers-BzHEPnpl.d.cts +228 -0
  134. package/dist/core/dist/feathers-BzHEPnpl.d.ts +228 -0
  135. package/dist/core/dist/git/index.cjs +302 -0
  136. package/dist/core/dist/git/index.d.cts +137 -0
  137. package/dist/core/dist/git/index.d.ts +137 -0
  138. package/dist/core/dist/git/index.js +260 -0
  139. package/dist/core/dist/id-DMqyogFB.d.cts +131 -0
  140. package/dist/core/dist/id-DMqyogFB.d.ts +131 -0
  141. package/dist/core/dist/index.cjs +4653 -0
  142. package/dist/core/dist/index.d.cts +23 -0
  143. package/dist/core/dist/index.d.ts +23 -0
  144. package/dist/core/dist/index.js +4509 -0
  145. package/dist/core/dist/message-BoxZISHg.d.cts +120 -0
  146. package/dist/core/dist/message-DvBzHu7V.d.ts +120 -0
  147. package/dist/core/dist/permissions/index.cjs +112 -0
  148. package/dist/core/dist/permissions/index.d.cts +81 -0
  149. package/dist/core/dist/permissions/index.d.ts +81 -0
  150. package/dist/core/dist/permissions/index.js +85 -0
  151. package/dist/core/dist/repo-3CUrCRbq.d.cts +405 -0
  152. package/dist/core/dist/repo-CnvJ0B6-.d.ts +405 -0
  153. package/dist/core/dist/session-BPjJlVdZ.d.cts +429 -0
  154. package/dist/core/dist/session-wAzjHatv.d.ts +429 -0
  155. package/dist/core/dist/task-BIEgT1DK.d.cts +163 -0
  156. package/dist/core/dist/task-DuIfiUbW.d.ts +163 -0
  157. package/dist/core/dist/templates/handlebars-helpers.cjs +156 -0
  158. package/dist/core/dist/templates/handlebars-helpers.d.cts +45 -0
  159. package/dist/core/dist/templates/handlebars-helpers.d.ts +45 -0
  160. package/dist/core/dist/templates/handlebars-helpers.js +119 -0
  161. package/dist/core/dist/tools/claude/models.cjs +70 -0
  162. package/dist/core/dist/tools/claude/models.d.cts +27 -0
  163. package/dist/core/dist/tools/claude/models.d.ts +27 -0
  164. package/dist/core/dist/tools/claude/models.js +44 -0
  165. package/dist/core/dist/tools/index.cjs +3367 -0
  166. package/dist/core/dist/tools/index.d.cts +967 -0
  167. package/dist/core/dist/tools/index.d.ts +967 -0
  168. package/dist/core/dist/tools/index.js +3314 -0
  169. package/dist/core/dist/tools/models.cjs +119 -0
  170. package/dist/core/dist/tools/models.d.cts +47 -0
  171. package/dist/core/dist/tools/models.d.ts +47 -0
  172. package/dist/core/dist/tools/models.js +86 -0
  173. package/dist/core/dist/types/index.cjs +152 -0
  174. package/dist/core/dist/types/index.d.cts +214 -0
  175. package/dist/core/dist/types/index.d.ts +214 -0
  176. package/dist/core/dist/types/index.js +112 -0
  177. package/dist/core/dist/user-BmL3kFol.d.ts +50 -0
  178. package/dist/core/dist/user-eUuKj7yM.d.cts +50 -0
  179. package/dist/core/dist/utils/pricing.cjs +102 -0
  180. package/dist/core/dist/utils/pricing.d.cts +43 -0
  181. package/dist/core/dist/utils/pricing.d.ts +43 -0
  182. package/dist/core/dist/utils/pricing.js +75 -0
  183. package/dist/core/dist/worktrees-BzIxB1U6.d.cts +2745 -0
  184. package/dist/core/dist/worktrees-CYem1ya2.d.ts +2745 -0
  185. package/dist/core/environment/variable-resolver.cjs +92 -0
  186. package/dist/core/environment/variable-resolver.d.cts +52 -0
  187. package/dist/core/environment/variable-resolver.d.ts +52 -0
  188. package/dist/core/environment/variable-resolver.js +53 -0
  189. package/dist/core/feathers/index.cjs +66 -0
  190. package/dist/core/feathers/index.d.cts +7 -0
  191. package/dist/core/feathers/index.d.ts +7 -0
  192. package/dist/core/feathers/index.js +25 -0
  193. package/dist/core/feathers-BzHEPnpl.d.cts +228 -0
  194. package/dist/core/feathers-BzHEPnpl.d.ts +228 -0
  195. package/dist/core/git/index.cjs +302 -0
  196. package/dist/core/git/index.d.cts +137 -0
  197. package/dist/core/git/index.d.ts +137 -0
  198. package/dist/core/git/index.js +260 -0
  199. package/dist/core/id-DMqyogFB.d.cts +131 -0
  200. package/dist/core/id-DMqyogFB.d.ts +131 -0
  201. package/dist/core/index.cjs +4653 -0
  202. package/dist/core/index.d.cts +23 -0
  203. package/dist/core/index.d.ts +23 -0
  204. package/dist/core/index.js +4509 -0
  205. package/dist/core/message-BoxZISHg.d.cts +120 -0
  206. package/dist/core/message-DvBzHu7V.d.ts +120 -0
  207. package/dist/core/package.json +133 -0
  208. package/dist/core/permissions/index.cjs +112 -0
  209. package/dist/core/permissions/index.d.cts +81 -0
  210. package/dist/core/permissions/index.d.ts +81 -0
  211. package/dist/core/permissions/index.js +85 -0
  212. package/dist/core/repo-3CUrCRbq.d.cts +405 -0
  213. package/dist/core/repo-CnvJ0B6-.d.ts +405 -0
  214. package/dist/core/session-BPjJlVdZ.d.cts +429 -0
  215. package/dist/core/session-wAzjHatv.d.ts +429 -0
  216. package/dist/core/task-BIEgT1DK.d.cts +163 -0
  217. package/dist/core/task-DuIfiUbW.d.ts +163 -0
  218. package/dist/core/templates/handlebars-helpers.cjs +156 -0
  219. package/dist/core/templates/handlebars-helpers.d.cts +45 -0
  220. package/dist/core/templates/handlebars-helpers.d.ts +45 -0
  221. package/dist/core/templates/handlebars-helpers.js +119 -0
  222. package/dist/core/tools/claude/models.cjs +70 -0
  223. package/dist/core/tools/claude/models.d.cts +27 -0
  224. package/dist/core/tools/claude/models.d.ts +27 -0
  225. package/dist/core/tools/claude/models.js +44 -0
  226. package/dist/core/tools/index.cjs +3367 -0
  227. package/dist/core/tools/index.d.cts +967 -0
  228. package/dist/core/tools/index.d.ts +967 -0
  229. package/dist/core/tools/index.js +3314 -0
  230. package/dist/core/tools/models.cjs +119 -0
  231. package/dist/core/tools/models.d.cts +47 -0
  232. package/dist/core/tools/models.d.ts +47 -0
  233. package/dist/core/tools/models.js +86 -0
  234. package/dist/core/types/index.cjs +152 -0
  235. package/dist/core/types/index.d.cts +214 -0
  236. package/dist/core/types/index.d.ts +214 -0
  237. package/dist/core/types/index.js +112 -0
  238. package/dist/core/user-BmL3kFol.d.ts +50 -0
  239. package/dist/core/user-eUuKj7yM.d.cts +50 -0
  240. package/dist/core/utils/pricing.cjs +102 -0
  241. package/dist/core/utils/pricing.d.cts +43 -0
  242. package/dist/core/utils/pricing.d.ts +43 -0
  243. package/dist/core/utils/pricing.js +75 -0
  244. package/dist/core/worktrees-BzIxB1U6.d.cts +2745 -0
  245. package/dist/core/worktrees-CYem1ya2.d.ts +2745 -0
  246. package/dist/daemon/adapters/drizzle.d.ts +114 -0
  247. package/dist/daemon/adapters/drizzle.js +219 -0
  248. package/dist/daemon/declarations.d.ts +101 -0
  249. package/dist/daemon/declarations.js +0 -0
  250. package/dist/daemon/index.d.ts +2 -0
  251. package/dist/daemon/index.js +4093 -0
  252. package/dist/daemon/mcp/routes.d.ts +15 -0
  253. package/dist/daemon/mcp/routes.js +641 -0
  254. package/dist/daemon/mcp/tokens.d.ts +50 -0
  255. package/dist/daemon/mcp/tokens.js +85 -0
  256. package/dist/daemon/services/board-comments.d.ts +97 -0
  257. package/dist/daemon/services/board-comments.js +326 -0
  258. package/dist/daemon/services/board-objects.d.ts +71 -0
  259. package/dist/daemon/services/board-objects.js +117 -0
  260. package/dist/daemon/services/boards.d.ts +64 -0
  261. package/dist/daemon/services/boards.js +286 -0
  262. package/dist/daemon/services/config.d.ts +35 -0
  263. package/dist/daemon/services/config.js +68 -0
  264. package/dist/daemon/services/context.d.ts +55 -0
  265. package/dist/daemon/services/context.js +113 -0
  266. package/dist/daemon/services/health-monitor.d.ts +58 -0
  267. package/dist/daemon/services/health-monitor.js +158 -0
  268. package/dist/daemon/services/mcp-servers.d.ts +42 -0
  269. package/dist/daemon/services/mcp-servers.js +275 -0
  270. package/dist/daemon/services/messages.d.ts +49 -0
  271. package/dist/daemon/services/messages.js +269 -0
  272. package/dist/daemon/services/repos.d.ts +61 -0
  273. package/dist/daemon/services/repos.js +350 -0
  274. package/dist/daemon/services/session-mcp-servers.d.ts +56 -0
  275. package/dist/daemon/services/session-mcp-servers.js +51 -0
  276. package/dist/daemon/services/sessions.d.ts +64 -0
  277. package/dist/daemon/services/sessions.js +398 -0
  278. package/dist/daemon/services/tasks.d.ts +55 -0
  279. package/dist/daemon/services/tasks.js +318 -0
  280. package/dist/daemon/services/terminals.d.ts +75 -0
  281. package/dist/daemon/services/terminals.js +110 -0
  282. package/dist/daemon/services/users.d.ts +98 -0
  283. package/dist/daemon/services/users.js +177 -0
  284. package/dist/daemon/services/worktrees.d.ts +98 -0
  285. package/dist/daemon/services/worktrees.js +719 -0
  286. package/dist/daemon/strategies/anonymous.d.ts +20 -0
  287. package/dist/daemon/strategies/anonymous.js +32 -0
  288. package/dist/ui/assets/cc-CYmbalCD.png +0 -0
  289. package/dist/ui/assets/codex-4sLD1mVS.png +0 -0
  290. package/dist/ui/assets/cursor-BUy5pFVL.png +0 -0
  291. package/dist/ui/assets/gemini-ajOb7iAl.png +0 -0
  292. package/dist/ui/assets/index-Dc4ELxry.css +32 -0
  293. package/dist/ui/assets/index-KfIu8v4V.js +578 -0
  294. package/dist/ui/favicon.png +0 -0
  295. package/dist/ui/index.html +26 -0
  296. package/dist/ui/vite.svg +1 -0
  297. package/package.json +90 -0
@@ -0,0 +1,3314 @@
1
+ // src/tools/claude/claude-tool.ts
2
+ import * as fs5 from "fs/promises";
3
+ import * as os from "os";
4
+ import * as path4 from "path";
5
+
6
+ // src/lib/ids.ts
7
+ import { uuidv7 } from "uuidv7";
8
+ function generateId() {
9
+ return uuidv7();
10
+ }
11
+
12
+ // src/types/task.ts
13
+ var TaskStatus = {
14
+ CREATED: "created",
15
+ RUNNING: "running",
16
+ STOPPING: "stopping",
17
+ // Stop requested, waiting for SDK to halt
18
+ AWAITING_PERMISSION: "awaiting_permission",
19
+ COMPLETED: "completed",
20
+ FAILED: "failed",
21
+ STOPPED: "stopped"
22
+ // User-requested stop (distinct from failed)
23
+ };
24
+
25
+ // src/tools/claude/import/transcript-parser.ts
26
+ import fs from "fs";
27
+ import path from "path";
28
+ import readline from "readline";
29
+ function getTranscriptPath(sessionId, projectDir) {
30
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
31
+ if (!homeDir) {
32
+ throw new Error("Could not determine home directory");
33
+ }
34
+ const cwd = projectDir || process.cwd();
35
+ const projectSlug = cwd.replace(/\//g, "-").replace(/\\/g, "-");
36
+ const transcriptPath = path.join(
37
+ homeDir,
38
+ ".claude",
39
+ "projects",
40
+ projectSlug,
41
+ `${sessionId}.jsonl`
42
+ );
43
+ return transcriptPath;
44
+ }
45
+ async function parseTranscript(transcriptPath) {
46
+ if (!fs.existsSync(transcriptPath)) {
47
+ throw new Error(`Transcript file not found: ${transcriptPath}`);
48
+ }
49
+ const messages = [];
50
+ const fileStream = fs.createReadStream(transcriptPath);
51
+ const rl = readline.createInterface({
52
+ input: fileStream,
53
+ crlfDelay: Number.POSITIVE_INFINITY
54
+ });
55
+ for await (const line of rl) {
56
+ if (!line.trim()) continue;
57
+ try {
58
+ const message = JSON.parse(line);
59
+ messages.push(message);
60
+ } catch (error) {
61
+ console.error(`Failed to parse line: ${line.substring(0, 100)}...`);
62
+ throw error;
63
+ }
64
+ }
65
+ return messages;
66
+ }
67
+ async function loadSessionTranscript(sessionId, projectDir) {
68
+ const transcriptPath = getTranscriptPath(sessionId, projectDir);
69
+ return parseTranscript(transcriptPath);
70
+ }
71
+ function filterConversationMessages(messages) {
72
+ return messages.filter((msg) => {
73
+ if (msg.type === "file-history-snapshot") return false;
74
+ if (msg.isMeta) return false;
75
+ const content = msg.message?.content;
76
+ if (Array.isArray(content) && content.some((c) => c.type === "tool_result")) {
77
+ return false;
78
+ }
79
+ if (typeof content === "string") {
80
+ if (content.trim().match(/^<(command-name|local-command-stdout|system-reminder)/)) {
81
+ return false;
82
+ }
83
+ }
84
+ return msg.type === "user" || msg.type === "assistant";
85
+ });
86
+ }
87
+ function buildConversationTree(messages) {
88
+ const messageMap = /* @__PURE__ */ new Map();
89
+ const roots = [];
90
+ for (const message of messages) {
91
+ if (!message.uuid) continue;
92
+ const node = {
93
+ message,
94
+ children: []
95
+ };
96
+ messageMap.set(message.uuid, node);
97
+ }
98
+ for (const message of messages) {
99
+ if (!message.uuid) continue;
100
+ const node = messageMap.get(message.uuid);
101
+ if (!node) continue;
102
+ if (!message.parentUuid) {
103
+ roots.push(node);
104
+ } else {
105
+ const parent = messageMap.get(message.parentUuid);
106
+ if (parent) {
107
+ parent.children.push(node);
108
+ } else {
109
+ roots.push(node);
110
+ }
111
+ }
112
+ }
113
+ return roots;
114
+ }
115
+
116
+ // src/tools/claude/import/load-session.ts
117
+ async function loadClaudeSession(sessionId, projectDir) {
118
+ const transcriptPath = getTranscriptPath(sessionId, projectDir);
119
+ const messages = await parseTranscript(transcriptPath);
120
+ const cwdMessage = messages.find((msg) => msg.cwd);
121
+ const cwd = cwdMessage?.cwd || null;
122
+ return {
123
+ sessionId,
124
+ transcriptPath,
125
+ cwd,
126
+ messages
127
+ };
128
+ }
129
+
130
+ // src/tools/claude/import/message-converter.ts
131
+ function transcriptToMessage(transcript, sessionId, index) {
132
+ const content = transcript.message?.content || "";
133
+ const contentPreview = typeof content === "string" ? content.substring(0, 200) : JSON.stringify(content).substring(0, 200);
134
+ const role = transcript.message?.role || transcript.type;
135
+ let toolUses;
136
+ if (Array.isArray(content)) {
137
+ const tools = content.filter((c) => c.type === "tool_use");
138
+ if (tools.length > 0) {
139
+ toolUses = tools.map((tool) => ({
140
+ id: tool.id,
141
+ name: tool.name,
142
+ input: tool.input || {}
143
+ }));
144
+ }
145
+ }
146
+ return {
147
+ message_id: generateId(),
148
+ session_id: sessionId,
149
+ type: transcript.type,
150
+ role,
151
+ index,
152
+ timestamp: transcript.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
153
+ content_preview: contentPreview,
154
+ content,
155
+ tool_uses: toolUses,
156
+ metadata: {
157
+ original_id: transcript.uuid,
158
+ parent_id: transcript.parentUuid || void 0,
159
+ is_meta: transcript.isMeta
160
+ }
161
+ };
162
+ }
163
+ function transcriptsToMessages(transcripts, sessionId) {
164
+ return transcripts.map((transcript, index) => transcriptToMessage(transcript, sessionId, index));
165
+ }
166
+
167
+ // src/tools/claude/models.ts
168
+ var AVAILABLE_CLAUDE_MODEL_ALIASES = [
169
+ {
170
+ id: "claude-opus-4-1",
171
+ displayName: "Claude Opus 4.1",
172
+ family: "claude-4",
173
+ description: "Most capable model (latest)"
174
+ },
175
+ {
176
+ id: "claude-sonnet-4-5",
177
+ displayName: "Claude Sonnet 4.5",
178
+ family: "claude-4",
179
+ description: "Best for coding (latest)"
180
+ },
181
+ {
182
+ id: "claude-sonnet-4-0",
183
+ displayName: "Claude Sonnet 4.0",
184
+ family: "claude-4",
185
+ description: "Sonnet 4.0 (previous)"
186
+ },
187
+ {
188
+ id: "claude-3-7-sonnet-latest",
189
+ displayName: "Claude 3.7 Sonnet",
190
+ family: "claude-3.7",
191
+ description: "Fast & balanced"
192
+ },
193
+ {
194
+ id: "claude-haiku-4-5",
195
+ displayName: "Claude Haiku 4.5",
196
+ family: "claude-4",
197
+ description: "Fastest (latest)"
198
+ },
199
+ {
200
+ id: "claude-3-5-haiku-latest",
201
+ displayName: "Claude 3.5 Haiku",
202
+ family: "claude-3.5",
203
+ description: "Fastest (previous)"
204
+ }
205
+ ];
206
+ var DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-5";
207
+
208
+ // src/tools/claude/prompt-service.ts
209
+ import { execSync } from "child_process";
210
+ import * as fs4 from "fs/promises";
211
+ import * as path3 from "path";
212
+ import { query } from "@anthropic-ai/claude-agent-sdk";
213
+
214
+ // src/lib/validation.ts
215
+ import * as fs2 from "fs/promises";
216
+ async function validateDirectory(path7, context = "Directory") {
217
+ try {
218
+ const stats = await fs2.stat(path7);
219
+ if (!stats.isDirectory()) {
220
+ throw new Error(`${context} exists but is not a directory: ${path7}`);
221
+ }
222
+ } catch (error) {
223
+ if (error.code === "ENOENT") {
224
+ throw new Error(`${context} does not exist: ${path7}`);
225
+ }
226
+ throw new Error(`${context} is not accessible: ${path7} (${error})`);
227
+ }
228
+ }
229
+
230
+ // src/tools/claude/message-processor.ts
231
+ var SDKMessageProcessor = class {
232
+ state;
233
+ constructor(options) {
234
+ this.state = {
235
+ sessionId: options.sessionId,
236
+ existingSdkSessionId: options.existingSdkSessionId,
237
+ capturedAgentSessionId: void 0,
238
+ messageCount: 0,
239
+ lastActivityTime: Date.now(),
240
+ lastAssistantMessageTime: Date.now(),
241
+ enableTokenStreaming: options.enableTokenStreaming ?? true,
242
+ idleTimeoutMs: options.idleTimeoutMs ?? 3e4,
243
+ // 30s default
244
+ contentBlockStack: []
245
+ };
246
+ }
247
+ /**
248
+ * Process an SDK message and return 0 or more events
249
+ *
250
+ * @param msg - SDK message to process
251
+ * @returns Array of events to yield upstream
252
+ */
253
+ async process(msg) {
254
+ this.state.messageCount++;
255
+ this.state.lastActivityTime = Date.now();
256
+ if (this.state.messageCount % 10 === 0) {
257
+ console.debug(`\u{1F4E8} SDK message ${this.state.messageCount}: type=${msg.type}`);
258
+ }
259
+ if (process.env.DEBUG_SDK_MESSAGES === "true") {
260
+ console.log(`\u{1F50D} [DEBUG] Full SDK message ${this.state.messageCount}:`);
261
+ console.log(JSON.stringify(msg, null, 2));
262
+ }
263
+ if (!this.state.capturedAgentSessionId && "session_id" in msg && msg.session_id) {
264
+ const events = this.captureSessionId(msg.session_id);
265
+ const messageEvents = await this.routeMessage(msg);
266
+ return [...events, ...messageEvents];
267
+ }
268
+ return this.routeMessage(msg);
269
+ }
270
+ /**
271
+ * Check if processor has timed out due to inactivity
272
+ */
273
+ hasTimedOut() {
274
+ const timeSinceLastAssistant = Date.now() - this.state.lastAssistantMessageTime;
275
+ return timeSinceLastAssistant > this.state.idleTimeoutMs && this.state.messageCount > 5;
276
+ }
277
+ /**
278
+ * Get current processor state (for debugging/monitoring)
279
+ */
280
+ getState() {
281
+ return { ...this.state };
282
+ }
283
+ /**
284
+ * Route message to appropriate handler based on type
285
+ */
286
+ async routeMessage(msg) {
287
+ switch (msg.type) {
288
+ case "assistant":
289
+ return this.handleAssistant(msg);
290
+ case "user":
291
+ return this.handleUser(msg);
292
+ case "stream_event":
293
+ return this.handleStreamEvent(msg);
294
+ case "result":
295
+ return this.handleResult(msg);
296
+ case "system":
297
+ return this.handleSystem(msg);
298
+ default:
299
+ return this.handleUnknown(msg);
300
+ }
301
+ }
302
+ /**
303
+ * Capture SDK session ID for conversation continuity
304
+ */
305
+ captureSessionId(sessionId) {
306
+ if (sessionId === this.state.existingSdkSessionId) {
307
+ return [];
308
+ }
309
+ this.state.capturedAgentSessionId = sessionId;
310
+ console.log(`\u{1F511} New Agent SDK session_id`);
311
+ return [
312
+ {
313
+ type: "session_id_captured",
314
+ agentSessionId: sessionId
315
+ }
316
+ ];
317
+ }
318
+ /**
319
+ * Handle assistant messages (complete responses)
320
+ */
321
+ handleAssistant(msg) {
322
+ this.state.lastAssistantMessageTime = Date.now();
323
+ const contentBlocks = this.processContentBlocks(msg.message?.content);
324
+ const toolUses = this.extractToolUses(contentBlocks);
325
+ return [
326
+ {
327
+ type: "complete",
328
+ role: "assistant" /* ASSISTANT */,
329
+ content: contentBlocks,
330
+ toolUses: toolUses.length > 0 ? toolUses : void 0,
331
+ agentSessionId: this.state.capturedAgentSessionId,
332
+ resolvedModel: this.state.resolvedModel
333
+ }
334
+ ];
335
+ }
336
+ /**
337
+ * Handle user messages (including tool results)
338
+ */
339
+ handleUser(msg) {
340
+ if ("isReplay" in msg && msg.isReplay) {
341
+ console.debug(`\u{1F504} User message replay (uuid: ${msg.uuid?.substring(0, 8)})`);
342
+ return [];
343
+ }
344
+ const content = msg.message?.content;
345
+ const uuid = "uuid" in msg ? msg.uuid : void 0;
346
+ const hasToolResult = Array.isArray(content) && content.some((b) => b.type === "tool_result");
347
+ const hasText = Array.isArray(content) && content.some((b) => b.type === "text");
348
+ if (hasToolResult) {
349
+ const toolResults = content.filter((b) => b.type === "tool_result");
350
+ console.log(
351
+ `\u{1F527} SDK user message with ${toolResults.length} tool result(s) (uuid: ${uuid?.substring(0, 8)})`
352
+ );
353
+ toolResults.forEach((tr, i) => {
354
+ const preview = typeof tr.content === "string" ? tr.content.substring(0, 100) : JSON.stringify(tr.content).substring(0, 100);
355
+ console.log(` Result ${i + 1}: ${tr.is_error ? "\u274C ERROR" : "\u2705"} ${preview}`);
356
+ });
357
+ return [
358
+ {
359
+ type: "complete",
360
+ role: "user" /* USER */,
361
+ content,
362
+ // Tool result content
363
+ toolUses: void 0,
364
+ agentSessionId: this.state.capturedAgentSessionId,
365
+ resolvedModel: this.state.resolvedModel
366
+ }
367
+ ];
368
+ } else if (hasText) {
369
+ const textBlocks = content.filter((b) => b.type === "text");
370
+ const textPreview = textBlocks[0]?.text?.substring(0, 100) || "";
371
+ console.log(`\u{1F464} SDK user message (uuid: ${uuid?.substring(0, 8)}): "${textPreview}"`);
372
+ return [
373
+ {
374
+ type: "complete",
375
+ role: "user" /* USER */,
376
+ content,
377
+ toolUses: void 0,
378
+ agentSessionId: this.state.capturedAgentSessionId,
379
+ resolvedModel: this.state.resolvedModel
380
+ }
381
+ ];
382
+ } else {
383
+ console.log(`\u{1F464} SDK user message (uuid: ${uuid?.substring(0, 8)})`);
384
+ console.log(
385
+ ` Content types:`,
386
+ Array.isArray(content) ? content.map((b) => b.type) : "no content"
387
+ );
388
+ return [];
389
+ }
390
+ }
391
+ /**
392
+ * Handle streaming events (partial messages)
393
+ */
394
+ handleStreamEvent(msg) {
395
+ if (!this.state.enableTokenStreaming) {
396
+ return [];
397
+ }
398
+ const event = msg.event;
399
+ const events = [];
400
+ if (event?.type === "message_start") {
401
+ console.debug(`\u{1F3AC} Message start`);
402
+ events.push({
403
+ type: "message_start",
404
+ agentSessionId: this.state.capturedAgentSessionId
405
+ });
406
+ const message = event.message;
407
+ if (message?.model) {
408
+ this.state.resolvedModel = message.model;
409
+ }
410
+ }
411
+ if (event?.type === "content_block_start") {
412
+ const block = event.content_block;
413
+ const blockIndex = event.index;
414
+ if (block?.type === "tool_use") {
415
+ const toolName = block.name;
416
+ const toolId = block.id;
417
+ console.debug(`\u{1F527} Tool start: ${toolName} (${toolId})`);
418
+ this.state.contentBlockStack.push({
419
+ index: blockIndex,
420
+ type: "tool_use",
421
+ toolUseId: toolId,
422
+ toolName
423
+ });
424
+ events.push({
425
+ type: "tool_start",
426
+ toolName,
427
+ toolUseId: toolId,
428
+ agentSessionId: this.state.capturedAgentSessionId
429
+ });
430
+ } else if (block?.type === "text") {
431
+ this.state.contentBlockStack.push({
432
+ index: blockIndex,
433
+ type: "text"
434
+ });
435
+ }
436
+ }
437
+ if (event?.type === "content_block_delta") {
438
+ const delta = event.delta;
439
+ if (delta?.type === "text_delta") {
440
+ const textChunk = delta.text;
441
+ events.push({
442
+ type: "partial",
443
+ textChunk,
444
+ agentSessionId: this.state.capturedAgentSessionId,
445
+ resolvedModel: this.state.resolvedModel
446
+ });
447
+ } else if (delta?.type === "input_json_delta") {
448
+ const partialJson = delta.partial_json;
449
+ if (partialJson) {
450
+ console.debug(`\u{1F527} Tool input chunk: ${partialJson.substring(0, 50)}...`);
451
+ }
452
+ }
453
+ }
454
+ if (event?.type === "content_block_stop") {
455
+ const blockIndex = event.index;
456
+ const completedBlock = this.state.contentBlockStack.find((b) => b.index === blockIndex);
457
+ if (completedBlock?.type === "tool_use") {
458
+ console.debug(`\u{1F3C1} Tool complete: ${completedBlock.toolName} (${completedBlock.toolUseId})`);
459
+ events.push({
460
+ type: "tool_complete",
461
+ toolUseId: completedBlock.toolUseId,
462
+ agentSessionId: this.state.capturedAgentSessionId
463
+ });
464
+ } else {
465
+ console.debug(`\u{1F3C1} Content block ${blockIndex} complete`);
466
+ }
467
+ this.state.contentBlockStack = this.state.contentBlockStack.filter(
468
+ (b) => b.index !== blockIndex
469
+ );
470
+ }
471
+ if (event?.type === "message_stop") {
472
+ console.debug(`\u{1F3C1} Message complete`);
473
+ events.push({
474
+ type: "message_complete",
475
+ agentSessionId: this.state.capturedAgentSessionId
476
+ });
477
+ this.state.contentBlockStack = [];
478
+ }
479
+ return events;
480
+ }
481
+ /**
482
+ * Handle result messages (end of conversation)
483
+ */
484
+ handleResult(msg) {
485
+ const subtype = msg.subtype || "unknown";
486
+ const duration = msg.duration_ms;
487
+ const cost = msg.total_cost_usd;
488
+ console.log(
489
+ `\u2705 SDK result: ${subtype}${duration ? ` (${duration}ms)` : ""}${cost ? ` ($${cost})` : ""}`
490
+ );
491
+ if ("usage" in msg && msg.usage) {
492
+ console.log(` Token usage:`, msg.usage);
493
+ }
494
+ if ("modelUsage" in msg && msg.modelUsage) {
495
+ console.log(` Model usage (with contextWindow):`, JSON.stringify(msg.modelUsage, null, 2));
496
+ }
497
+ return [
498
+ {
499
+ type: "result",
500
+ subtype,
501
+ duration_ms: duration,
502
+ cost,
503
+ token_usage: "usage" in msg ? msg.usage : void 0,
504
+ model_usage: "modelUsage" in msg ? msg.modelUsage : void 0,
505
+ agentSessionId: this.state.capturedAgentSessionId
506
+ },
507
+ {
508
+ type: "end",
509
+ reason: "result"
510
+ }
511
+ ];
512
+ }
513
+ /**
514
+ * Handle system messages
515
+ */
516
+ handleSystem(msg) {
517
+ if ("subtype" in msg && msg.subtype === "compact_boundary") {
518
+ console.debug(`\u{1F4E6} SDK compact boundary (memory management)`);
519
+ return [];
520
+ }
521
+ if ("subtype" in msg && msg.subtype === "init") {
522
+ console.debug(`\u2139\uFE0F SDK system init:`, {
523
+ model: msg.model,
524
+ permissionMode: msg.permissionMode,
525
+ cwd: msg.cwd,
526
+ tools: msg.tools?.length,
527
+ mcp_servers: msg.mcp_servers?.length
528
+ });
529
+ if (msg.model) {
530
+ this.state.resolvedModel = msg.model;
531
+ }
532
+ return [];
533
+ }
534
+ console.debug(`\u2139\uFE0F SDK system message:`, msg);
535
+ return [];
536
+ }
537
+ /**
538
+ * Handle unknown message types
539
+ */
540
+ handleUnknown(msg) {
541
+ console.warn(`\u26A0\uFE0F Unknown SDK message type: ${msg.type}`, msg);
542
+ return [];
543
+ }
544
+ /**
545
+ * Process content blocks from SDK message
546
+ */
547
+ processContentBlocks(content) {
548
+ if (!Array.isArray(content)) {
549
+ return [];
550
+ }
551
+ return content.map((block) => {
552
+ if (block.type === "text") {
553
+ return {
554
+ type: "text",
555
+ text: block.text
556
+ };
557
+ } else if (block.type === "tool_use") {
558
+ return {
559
+ type: "tool_use",
560
+ id: block.id,
561
+ name: block.name,
562
+ input: block.input
563
+ };
564
+ } else {
565
+ return {
566
+ ...block,
567
+ type: block.type
568
+ };
569
+ }
570
+ });
571
+ }
572
+ /**
573
+ * Extract tool uses from content blocks
574
+ */
575
+ extractToolUses(contentBlocks) {
576
+ return contentBlocks.filter((block) => block.type === "tool_use" && block.id && block.name && block.input).map((block) => ({
577
+ id: block.id,
578
+ name: block.name,
579
+ input: block.input
580
+ }));
581
+ }
582
+ };
583
+
584
+ // src/tools/claude/session-context.ts
585
+ import * as fs3 from "fs/promises";
586
+ import * as path2 from "path";
587
+ function generateSessionContext(sessionId) {
588
+ const shortId = sessionId.substring(0, 8);
589
+ return `
590
+
591
+ ---
592
+
593
+ ## Agor Session Context
594
+
595
+ You are currently running within **Agor** (https://agor.live), a multiplayer canvas for orchestrating AI coding agents.
596
+
597
+ **Your current Agor session ID is: \`${sessionId}\`** (short: \`${shortId}\`)
598
+
599
+ When you see this ID referenced in prompts or tool calls, it refers to THIS session you're currently in.
600
+
601
+ For more information about Agor, visit https://agor.live
602
+ `;
603
+ }
604
+ async function appendSessionContextToCLAUDEmd(worktreePath, sessionId) {
605
+ const claudeMdPath = path2.join(worktreePath, "CLAUDE.md");
606
+ try {
607
+ let existingContent = "";
608
+ try {
609
+ existingContent = await fs3.readFile(claudeMdPath, "utf-8");
610
+ } catch (readError) {
611
+ console.log(`\u{1F4DD} CLAUDE.md doesn't exist at ${claudeMdPath}, will create it`);
612
+ }
613
+ if (existingContent.includes("## Agor Session Context")) {
614
+ console.log(`\u2705 Session context already in CLAUDE.md, skipping append`);
615
+ return;
616
+ }
617
+ const sessionContext = generateSessionContext(sessionId);
618
+ const newContent = existingContent + sessionContext;
619
+ await fs3.writeFile(claudeMdPath, newContent, "utf-8");
620
+ console.log(
621
+ `\u2705 Appended session context to CLAUDE.md for session ${sessionId.substring(0, 8)}`
622
+ );
623
+ } catch (error) {
624
+ console.error(`\u274C Failed to append session context to CLAUDE.md:`, error);
625
+ }
626
+ }
627
+ async function removeSessionContextFromCLAUDEmd(worktreePath) {
628
+ const claudeMdPath = path2.join(worktreePath, "CLAUDE.md");
629
+ try {
630
+ const content = await fs3.readFile(claudeMdPath, "utf-8");
631
+ const contextStart = content.indexOf("\n\n---\n\n## Agor Session Context");
632
+ if (contextStart === -1) {
633
+ return;
634
+ }
635
+ const cleanedContent = content.substring(0, contextStart);
636
+ await fs3.writeFile(claudeMdPath, cleanedContent, "utf-8");
637
+ console.log(`\u2705 Removed session context from CLAUDE.md`);
638
+ } catch (error) {
639
+ console.error(`\u274C Failed to remove session context from CLAUDE.md:`, error);
640
+ }
641
+ }
642
+
643
+ // src/tools/claude/prompt-service.ts
644
+ function getClaudeCodePath() {
645
+ try {
646
+ const path7 = execSync("which claude", { encoding: "utf-8" }).trim();
647
+ if (path7) return path7;
648
+ } catch {
649
+ }
650
+ const commonPaths = [
651
+ "/usr/local/bin/claude",
652
+ "/opt/homebrew/bin/claude",
653
+ `${process.env.HOME}/.nvm/versions/node/v20.19.4/bin/claude`
654
+ ];
655
+ for (const path7 of commonPaths) {
656
+ try {
657
+ execSync(`test -x "${path7}"`, { encoding: "utf-8" });
658
+ return path7;
659
+ } catch {
660
+ }
661
+ }
662
+ throw new Error(
663
+ "Claude Code executable not found. Install with: npm install -g @anthropic-ai/claude-code"
664
+ );
665
+ }
666
+ var ClaudePromptService = class _ClaudePromptService {
667
+ constructor(messagesRepo, sessionsRepo, apiKey, sessionMCPRepo, mcpServerRepo, permissionService, tasksService, sessionsService, worktreesRepo, messagesService) {
668
+ this.messagesRepo = messagesRepo;
669
+ this.sessionsRepo = sessionsRepo;
670
+ this.apiKey = apiKey;
671
+ this.sessionMCPRepo = sessionMCPRepo;
672
+ this.mcpServerRepo = mcpServerRepo;
673
+ this.permissionService = permissionService;
674
+ this.tasksService = tasksService;
675
+ this.sessionsService = sessionsService;
676
+ this.worktreesRepo = worktreesRepo;
677
+ this.messagesService = messagesService;
678
+ }
679
+ /** Enable token-level streaming from Claude Agent SDK */
680
+ static ENABLE_TOKEN_STREAMING = true;
681
+ /** Store active Query objects per session for interruption */
682
+ // biome-ignore lint/suspicious/noExplicitAny: Query type from SDK is complex
683
+ activeQueries = /* @__PURE__ */ new Map();
684
+ /** Track stop requests for immediate loop breaking */
685
+ stopRequested = /* @__PURE__ */ new Map();
686
+ /** Serialize permission checks per session to prevent duplicate prompts for concurrent tool calls */
687
+ permissionLocks = /* @__PURE__ */ new Map();
688
+ /**
689
+ * Create PreToolUse hook for permission handling
690
+ * @private
691
+ */
692
+ createPreToolUseHook(sessionId, taskId) {
693
+ return async (input, toolUseID, options) => {
694
+ if (!this.permissionService || !this.tasksService) {
695
+ return {};
696
+ }
697
+ let releaseLock;
698
+ try {
699
+ const existingLock = this.permissionLocks.get(sessionId);
700
+ if (existingLock) {
701
+ console.log(
702
+ `\u23F3 Waiting for pending permission check to complete (session ${sessionId.substring(0, 8)})`
703
+ );
704
+ await existingLock;
705
+ console.log(`\u2705 Permission check complete, rechecking DB...`);
706
+ }
707
+ const session = await this.sessionsRepo.findById(sessionId);
708
+ if (session?.permission_config?.allowedTools?.includes(input.tool_name)) {
709
+ console.log(`\u2705 Tool ${input.tool_name} allowed by session config (after queue wait)`);
710
+ return {
711
+ hookSpecificOutput: {
712
+ hookEventName: "PreToolUse",
713
+ permissionDecision: "allow",
714
+ permissionDecisionReason: "Allowed by session config"
715
+ }
716
+ };
717
+ }
718
+ console.log(
719
+ `\u{1F512} No permission found for ${input.tool_name}, creating lock and prompting user...`
720
+ );
721
+ const newLock = new Promise((resolve) => {
722
+ releaseLock = resolve;
723
+ });
724
+ this.permissionLocks.set(sessionId, newLock);
725
+ const requestId = generateId();
726
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
727
+ const existingMessages = await this.messagesRepo.findBySessionId(sessionId);
728
+ const nextIndex = existingMessages.length;
729
+ console.log(`\u{1F512} Creating permission request message for ${input.tool_name}`, {
730
+ request_id: requestId,
731
+ task_id: taskId,
732
+ index: nextIndex
733
+ });
734
+ const permissionMessage = {
735
+ message_id: generateId(),
736
+ session_id: sessionId,
737
+ task_id: taskId,
738
+ type: "permission_request",
739
+ role: "system" /* SYSTEM */,
740
+ index: nextIndex,
741
+ timestamp,
742
+ content_preview: `Permission required: ${input.tool_name}`,
743
+ content: {
744
+ request_id: requestId,
745
+ tool_name: input.tool_name,
746
+ tool_input: input.tool_input,
747
+ tool_use_id: toolUseID,
748
+ status: "pending" /* PENDING */
749
+ }
750
+ };
751
+ try {
752
+ if (this.messagesService) {
753
+ await this.messagesService.create(permissionMessage);
754
+ console.log(`\u2705 Permission request message created successfully`);
755
+ }
756
+ } catch (createError) {
757
+ console.error(`\u274C CRITICAL: Failed to create permission request message:`, createError);
758
+ throw createError;
759
+ }
760
+ try {
761
+ await this.tasksService.patch(taskId, {
762
+ status: TaskStatus.AWAITING_PERMISSION
763
+ });
764
+ console.log(`\u2705 Task ${taskId} updated to awaiting_permission`);
765
+ } catch (patchError) {
766
+ console.error(`\u274C CRITICAL: Failed to patch task ${taskId}:`, patchError);
767
+ throw patchError;
768
+ }
769
+ this.permissionService.emitRequest(sessionId, {
770
+ requestId,
771
+ taskId,
772
+ toolName: input.tool_name,
773
+ toolInput: input.tool_input,
774
+ toolUseID,
775
+ timestamp
776
+ });
777
+ const decision = await this.permissionService.waitForDecision(
778
+ requestId,
779
+ taskId,
780
+ options.signal
781
+ );
782
+ if (this.messagesService) {
783
+ const baseContent = typeof permissionMessage.content === "object" && !Array.isArray(permissionMessage.content) ? permissionMessage.content : {};
784
+ await this.messagesService.patch(permissionMessage.message_id, {
785
+ content: {
786
+ ...baseContent,
787
+ status: decision.allow ? "approved" /* APPROVED */ : "denied" /* DENIED */,
788
+ scope: decision.remember ? decision.scope : void 0,
789
+ approved_by: decision.decidedBy,
790
+ approved_at: (/* @__PURE__ */ new Date()).toISOString()
791
+ }
792
+ });
793
+ console.log(
794
+ `\u2705 Permission request message updated: ${decision.allow ? "approved" : "denied"}`
795
+ );
796
+ }
797
+ await this.tasksService.patch(taskId, {
798
+ status: decision.allow ? TaskStatus.RUNNING : TaskStatus.FAILED
799
+ });
800
+ if (decision.remember) {
801
+ const freshSession = await this.sessionsRepo.findById(sessionId);
802
+ if (!freshSession) {
803
+ return {
804
+ hookSpecificOutput: {
805
+ hookEventName: "PreToolUse",
806
+ permissionDecision: decision.allow ? "allow" : "deny",
807
+ permissionDecisionReason: decision.reason
808
+ }
809
+ };
810
+ }
811
+ if (decision.scope === "session") {
812
+ const currentAllowed = freshSession.permission_config?.allowedTools || [];
813
+ const newAllowedTools = [...currentAllowed, input.tool_name];
814
+ const updateData = {
815
+ permission_config: {
816
+ allowedTools: newAllowedTools
817
+ }
818
+ };
819
+ if (this.sessionsService) {
820
+ await this.sessionsService.patch(sessionId, updateData);
821
+ } else {
822
+ await this.sessionsRepo.update(sessionId, updateData);
823
+ }
824
+ } else if (decision.scope === "project") {
825
+ if (freshSession.worktree_id && this.worktreesRepo) {
826
+ const worktree = await this.worktreesRepo.findById(freshSession.worktree_id);
827
+ if (worktree) {
828
+ await this.updateProjectSettings(worktree.path, {
829
+ allowTools: [input.tool_name]
830
+ });
831
+ }
832
+ }
833
+ }
834
+ }
835
+ return {
836
+ hookSpecificOutput: {
837
+ hookEventName: "PreToolUse",
838
+ permissionDecision: decision.allow ? "allow" : "deny",
839
+ permissionDecisionReason: decision.reason
840
+ }
841
+ };
842
+ } catch (error) {
843
+ console.error("PreToolUse hook error:", error);
844
+ try {
845
+ const errorMessage = error instanceof Error ? error.message : String(error);
846
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
847
+ await this.tasksService.patch(taskId, {
848
+ status: TaskStatus.FAILED,
849
+ report: `Error: ${errorMessage}
850
+ Timestamp: ${timestamp}`
851
+ });
852
+ } catch (updateError) {
853
+ console.error("Failed to update task status:", updateError);
854
+ }
855
+ return {
856
+ hookSpecificOutput: {
857
+ hookEventName: "PreToolUse",
858
+ permissionDecision: "deny",
859
+ permissionDecisionReason: `Permission hook failed: ${error instanceof Error ? error.message : String(error)}`
860
+ }
861
+ };
862
+ } finally {
863
+ if (releaseLock) {
864
+ releaseLock();
865
+ this.permissionLocks.delete(sessionId);
866
+ console.log(`\u{1F513} Released permission lock for session ${sessionId.substring(0, 8)}`);
867
+ }
868
+ }
869
+ };
870
+ }
871
+ /**
872
+ * Update project-level permissions in .claude/settings.json
873
+ * @private
874
+ */
875
+ async updateProjectSettings(cwd, changes) {
876
+ const settingsPath = path3.join(cwd, ".claude", "settings.json");
877
+ let settings = {};
878
+ try {
879
+ const content = await fs4.readFile(settingsPath, "utf-8");
880
+ settings = JSON.parse(content);
881
+ } catch {
882
+ settings = { permissions: { allow: { tools: [] } } };
883
+ }
884
+ if (!settings.permissions) settings.permissions = {};
885
+ if (!settings.permissions.allow) settings.permissions.allow = {};
886
+ if (!settings.permissions.allow.tools) settings.permissions.allow.tools = [];
887
+ if (changes.allowTools) {
888
+ settings.permissions.allow.tools = [
889
+ .../* @__PURE__ */ new Set([...settings.permissions.allow.tools, ...changes.allowTools])
890
+ ];
891
+ }
892
+ if (changes.denyTools) {
893
+ if (!settings.permissions.deny) settings.permissions.deny = [];
894
+ settings.permissions.deny = [
895
+ .../* @__PURE__ */ new Set([...settings.permissions.deny, ...changes.denyTools])
896
+ ];
897
+ }
898
+ const claudeDir = path3.join(cwd, ".claude");
899
+ try {
900
+ await fs4.mkdir(claudeDir, { recursive: true });
901
+ } catch {
902
+ }
903
+ await fs4.writeFile(settingsPath, JSON.stringify(settings, null, 2));
904
+ }
905
+ /**
906
+ * Load session and initialize query
907
+ * @private
908
+ */
909
+ async setupQuery(sessionId, prompt, taskId, permissionMode, resume = true) {
910
+ const session = await this.sessionsRepo.findById(sessionId);
911
+ if (!session) {
912
+ throw new Error(`Session not found: ${sessionId}`);
913
+ }
914
+ const modelConfig = session.model_config;
915
+ const model = modelConfig?.model || DEFAULT_CLAUDE_MODEL;
916
+ let cwd = process.cwd();
917
+ if (session.worktree_id && this.worktreesRepo) {
918
+ try {
919
+ const worktree = await this.worktreesRepo.findById(session.worktree_id);
920
+ if (worktree) {
921
+ cwd = worktree.path;
922
+ console.log(`\u2705 Using worktree path as cwd: ${cwd}`);
923
+ } else {
924
+ console.warn(
925
+ `\u26A0\uFE0F Session ${sessionId} references non-existent worktree ${session.worktree_id}, using process.cwd(): ${cwd}`
926
+ );
927
+ }
928
+ } catch (error) {
929
+ console.error(`\u274C Failed to fetch worktree ${session.worktree_id}:`, error);
930
+ console.warn(` Falling back to process.cwd(): ${cwd}`);
931
+ }
932
+ } else {
933
+ console.warn(`\u26A0\uFE0F Session ${sessionId} has no worktree_id, using process.cwd(): ${cwd}`);
934
+ }
935
+ this.logPromptStart(sessionId, prompt, cwd, resume ? session.sdk_session_id : void 0);
936
+ try {
937
+ await validateDirectory(cwd, "Working directory");
938
+ try {
939
+ const files = await fs4.readdir(cwd);
940
+ const fileCount = files.length;
941
+ const hasGit = files.includes(".git");
942
+ const hasClaude = files.includes(".claude");
943
+ const hasCLAUDEmd = files.includes("CLAUDE.md");
944
+ console.log(
945
+ `\u2705 Working directory validated: ${cwd} (${fileCount} files/dirs${hasGit ? ", has .git" : ", NO .git!"}${hasClaude ? ", has .claude/" : ""}${hasCLAUDEmd ? ", has CLAUDE.md" : ""})`
946
+ );
947
+ if (fileCount === 0) {
948
+ console.warn(`\u26A0\uFE0F Working directory is EMPTY - worktree may be from bare repo!`);
949
+ } else if (!hasGit) {
950
+ console.warn(`\u26A0\uFE0F Working directory has no .git - not a valid worktree!`);
951
+ }
952
+ if (!hasCLAUDEmd && !hasClaude) {
953
+ console.warn(`\u26A0\uFE0F No CLAUDE.md or .claude/ directory found - SDK may not load properly`);
954
+ }
955
+ } catch (listError) {
956
+ console.warn(`\u26A0\uFE0F Could not list directory contents:`, listError);
957
+ }
958
+ await appendSessionContextToCLAUDEmd(cwd, sessionId);
959
+ } catch (error) {
960
+ const errorMessage = error instanceof Error ? error.message : String(error);
961
+ console.error(`\u274C Working directory validation failed: ${errorMessage}`);
962
+ throw new Error(
963
+ `${errorMessage}${session.worktree_id ? ` Session references worktree ${session.worktree_id} which may not be initialized.` : ""}`
964
+ );
965
+ }
966
+ const claudeCodePath = getClaudeCodePath();
967
+ let stderrBuffer = "";
968
+ const options = {
969
+ cwd,
970
+ systemPrompt: { type: "preset", preset: "claude_code" },
971
+ settingSources: ["user", "project"],
972
+ // Load user + project permissions, auto-loads CLAUDE.md
973
+ model,
974
+ // Use configured model or default
975
+ pathToClaudeCodeExecutable: claudeCodePath,
976
+ // Allow access to common directories outside CWD (e.g., /tmp)
977
+ additionalDirectories: ["/tmp", "/var/tmp"],
978
+ // Enable token-level streaming (yields partial messages as tokens arrive)
979
+ includePartialMessages: _ClaudePromptService.ENABLE_TOKEN_STREAMING,
980
+ // Enable debug logging to see what's happening
981
+ debug: true,
982
+ // Capture stderr to get actual error messages (not just "exit code 1")
983
+ stderr: (data) => {
984
+ stderrBuffer += data;
985
+ if (data.trim()) {
986
+ console.error(`[Claude stderr] ${data.trim()}`);
987
+ }
988
+ }
989
+ };
990
+ if (permissionMode) {
991
+ const isRoot = process.getuid?.() === 0;
992
+ if (isRoot && permissionMode === "bypassPermissions") {
993
+ console.warn(
994
+ `\u26A0\uFE0F Running as root - bypassPermissions not allowed. Falling back to 'default' mode.`
995
+ );
996
+ console.warn(` This is a security restriction from Claude Code SDK.`);
997
+ options.permissionMode = "default";
998
+ } else {
999
+ options.permissionMode = permissionMode;
1000
+ }
1001
+ console.log(`\u{1F510} Permission mode: ${options.permissionMode}`);
1002
+ }
1003
+ const sessionAllowedTools = session.permission_config?.allowedTools || [];
1004
+ if (sessionAllowedTools.length > 0) {
1005
+ options.allowedTools = sessionAllowedTools;
1006
+ }
1007
+ const effectivePermissionMode = options.permissionMode;
1008
+ if (this.permissionService && taskId && effectivePermissionMode !== "bypassPermissions") {
1009
+ options.hooks = {
1010
+ PreToolUse: [
1011
+ {
1012
+ hooks: [this.createPreToolUseHook(sessionId, taskId)]
1013
+ }
1014
+ ]
1015
+ };
1016
+ console.log(`\u{1FA9D} PreToolUse hook added (permission mode: ${effectivePermissionMode})`);
1017
+ }
1018
+ if (this.apiKey || process.env.ANTHROPIC_API_KEY) {
1019
+ options.apiKey = this.apiKey || process.env.ANTHROPIC_API_KEY;
1020
+ }
1021
+ if (resume) {
1022
+ const parentSessionId = session.genealogy?.forked_from_session_id || session.genealogy?.parent_session_id;
1023
+ if (parentSessionId && !session.sdk_session_id && this.sessionsRepo) {
1024
+ const parentSession = await this.sessionsRepo.findById(parentSessionId);
1025
+ if (parentSession?.sdk_session_id) {
1026
+ options.resume = parentSession.sdk_session_id;
1027
+ options.forkSession = true;
1028
+ console.log(
1029
+ `\u{1F374} Forking from parent session: ${parentSession.sdk_session_id.substring(0, 8)}`
1030
+ );
1031
+ console.log(` SDK will return new session ID for this fork`);
1032
+ } else {
1033
+ console.warn(
1034
+ `\u26A0\uFE0F Parent session ${parentSessionId.substring(0, 8)} has no sdk_session_id - starting fresh`
1035
+ );
1036
+ }
1037
+ } else if (session?.sdk_session_id) {
1038
+ const hoursSinceUpdate = session.last_updated ? (Date.now() - new Date(session.last_updated).getTime()) / (1e3 * 60 * 60) : 999;
1039
+ const isLikelyStale = hoursSinceUpdate > 24 || // Session older than 24 hours
1040
+ !session.worktree_id;
1041
+ if (isLikelyStale) {
1042
+ console.warn(
1043
+ `\u26A0\uFE0F Resume session ${session.sdk_session_id.substring(0, 8)} appears stale (${Math.round(hoursSinceUpdate)}h old) - starting fresh`
1044
+ );
1045
+ if (this.sessionsRepo) {
1046
+ await this.sessionsRepo.update(sessionId, { sdk_session_id: void 0 });
1047
+ }
1048
+ } else {
1049
+ options.resume = session.sdk_session_id;
1050
+ console.log(` Resuming SDK session: ${session.sdk_session_id.substring(0, 8)}`);
1051
+ }
1052
+ }
1053
+ }
1054
+ const mcpToken = session.mcp_token;
1055
+ console.log(`\u{1F50D} [MCP DEBUG] Checking for MCP token in session ${sessionId.substring(0, 8)}`);
1056
+ console.log(
1057
+ ` session.mcp_token: ${mcpToken ? `${mcpToken.substring(0, 16)}...` : "NOT FOUND"}`
1058
+ );
1059
+ if (mcpToken) {
1060
+ const daemonUrl = process.env.VITE_DAEMON_URL || "http://localhost:3030";
1061
+ console.log(`\u{1F50C} Configuring Agor MCP server (self-access to daemon)`);
1062
+ const mcpConfig = {
1063
+ agor: {
1064
+ name: "agor",
1065
+ type: "http",
1066
+ url: `${daemonUrl}/mcp?sessionToken=${mcpToken}`
1067
+ }
1068
+ };
1069
+ options.mcpServers = mcpConfig;
1070
+ console.log(` MCP server config:`, JSON.stringify(mcpConfig, null, 2));
1071
+ console.log(` Full URL: ${daemonUrl}/mcp?sessionToken=${mcpToken.substring(0, 16)}...`);
1072
+ } else {
1073
+ console.warn(`\u26A0\uFE0F No MCP token found for session ${sessionId.substring(0, 8)}`);
1074
+ console.warn(` Session will not have access to Agor MCP tools`);
1075
+ }
1076
+ if (false) {
1077
+ try {
1078
+ const allServers = [];
1079
+ console.log("\u{1F50C} Fetching MCP servers with hierarchical scoping...");
1080
+ const globalServers = await this.mcpServerRepo?.findAll({
1081
+ scope: "global",
1082
+ enabled: true
1083
+ });
1084
+ console.log(` \u{1F4CD} Global scope: ${globalServers?.length ?? 0} server(s)`);
1085
+ for (const server of globalServers ?? []) {
1086
+ allServers.push({ server, source: "global" });
1087
+ }
1088
+ let repoId;
1089
+ const worktreeId = session.worktree_id;
1090
+ if (worktreeId && this.worktreesRepo) {
1091
+ const worktree = await this.worktreesRepo.findById(worktreeId);
1092
+ repoId = worktree?.repo_id;
1093
+ }
1094
+ if (repoId) {
1095
+ const repoServers = await this.mcpServerRepo?.findAll({
1096
+ scope: "repo",
1097
+ scopeId: repoId,
1098
+ enabled: true
1099
+ });
1100
+ console.log(` \u{1F4CD} Repo scope: ${repoServers?.length ?? 0} server(s)`);
1101
+ for (const server of repoServers ?? []) {
1102
+ allServers.push({ server, source: "repo" });
1103
+ }
1104
+ }
1105
+ if (session && this.sessionMCPRepo) {
1106
+ const sessionServers = await this.sessionMCPRepo.listServers(sessionId, true);
1107
+ console.log(` \u{1F4CD} Session scope: ${sessionServers.length} server(s)`);
1108
+ for (const server of sessionServers) {
1109
+ allServers.push({ server, source: "session" });
1110
+ }
1111
+ } else {
1112
+ console.log(" \u{1F4CD} Session scope: 0 server(s)");
1113
+ }
1114
+ const serverMap = /* @__PURE__ */ new Map();
1115
+ for (const item of allServers) {
1116
+ serverMap.set(item.server.mcp_server_id, item);
1117
+ }
1118
+ const uniqueServers = Array.from(serverMap.values());
1119
+ console.log(
1120
+ ` \u2705 Total: ${uniqueServers.length} unique MCP server(s) after deduplication`
1121
+ );
1122
+ if (uniqueServers.length > 0) {
1123
+ const mcpConfig = {};
1124
+ const allowedTools = [];
1125
+ for (const { server, source } of uniqueServers) {
1126
+ console.log(` - ${server.name} (${server.transport}) [${source}]`);
1127
+ const serverConfig = {
1128
+ transport: server.transport
1129
+ };
1130
+ if (server.command) serverConfig.command = server.command;
1131
+ if (server.args) serverConfig.args = server.args;
1132
+ if (server.url) serverConfig.url = server.url;
1133
+ if (server.env) serverConfig.env = server.env;
1134
+ mcpConfig[server.name] = serverConfig;
1135
+ if (server.tools) {
1136
+ for (const tool of server.tools) {
1137
+ allowedTools.push(tool.name);
1138
+ }
1139
+ }
1140
+ }
1141
+ options.mcpServers = mcpConfig;
1142
+ console.log(` \u{1F527} MCP config being passed to SDK:`, JSON.stringify(mcpConfig, null, 2));
1143
+ if (allowedTools.length > 0) {
1144
+ options.allowedTools = allowedTools;
1145
+ console.log(` \u{1F527} Allowing ${allowedTools.length} MCP tools`);
1146
+ }
1147
+ }
1148
+ } catch (error) {
1149
+ console.warn("\u26A0\uFE0F Failed to fetch MCP servers for session:", error);
1150
+ }
1151
+ }
1152
+ console.log("\u{1F4E4} Calling query() with:");
1153
+ console.log(` prompt: "${prompt.substring(0, 100)}${prompt.length > 100 ? "..." : ""}"`);
1154
+ console.log(` options keys: ${Object.keys(options).join(", ")}`);
1155
+ console.log(
1156
+ ` \u{1F50D} [MCP DEBUG] options.mcpServers:`,
1157
+ options.mcpServers ? JSON.stringify(options.mcpServers, null, 2) : "NOT SET"
1158
+ );
1159
+ console.log(
1160
+ ` Full query call:`,
1161
+ JSON.stringify(
1162
+ {
1163
+ prompt,
1164
+ options
1165
+ },
1166
+ null,
1167
+ 2
1168
+ )
1169
+ );
1170
+ let result;
1171
+ try {
1172
+ result = query({
1173
+ prompt,
1174
+ // biome-ignore lint/suspicious/noExplicitAny: SDK Options type doesn't include all available fields
1175
+ options
1176
+ });
1177
+ console.log(`\u2705 query() returned AsyncGenerator successfully`);
1178
+ } catch (syncError) {
1179
+ console.error(`\u274C CRITICAL: query() threw synchronous error (very unusual):`, syncError);
1180
+ console.error(` Claude Code path: ${claudeCodePath}`);
1181
+ console.error(` CWD: ${cwd}`);
1182
+ console.error(
1183
+ ` API key set: ${this.apiKey ? "YES (custom)" : process.env.ANTHROPIC_API_KEY ? "YES (env)" : "NO"}`
1184
+ );
1185
+ console.error(` Resume session: ${options.resume || "none (fresh session)"}`);
1186
+ throw syncError;
1187
+ }
1188
+ this.activeQueries.set(sessionId, result);
1189
+ const getStderr = () => stderrBuffer;
1190
+ return { query: result, resolvedModel: model, getStderr };
1191
+ }
1192
+ /**
1193
+ * Log prompt start with context
1194
+ * @private
1195
+ */
1196
+ logPromptStart(sessionId, _prompt, _cwd, agentSessionId) {
1197
+ console.log(`\u{1F916} Prompting Claude for session ${sessionId.substring(0, 8)}...`);
1198
+ if (agentSessionId) {
1199
+ console.log(` Resuming session: ${agentSessionId}`);
1200
+ }
1201
+ }
1202
+ /**
1203
+ * Prompt a session using Claude Agent SDK (streaming version with text chunking)
1204
+ *
1205
+ * Yields both complete assistant messages AND text chunks as they're generated.
1206
+ * This enables real-time typewriter effect in the UI.
1207
+ *
1208
+ * @param sessionId - Session to prompt
1209
+ * @param prompt - User prompt
1210
+ * @param taskId - Optional task ID for permission tracking
1211
+ * @param permissionMode - Optional permission mode for SDK
1212
+ * @param chunkCallback - Optional callback for text chunks (3-10 words)
1213
+ * @returns Async generator yielding assistant messages with SDK session ID
1214
+ */
1215
+ async *promptSessionStreaming(sessionId, prompt, taskId, permissionMode, _chunkCallback) {
1216
+ const {
1217
+ query: result,
1218
+ resolvedModel,
1219
+ getStderr
1220
+ } = await this.setupQuery(sessionId, prompt, taskId, permissionMode, true);
1221
+ const session = await this.sessionsRepo?.findById(sessionId);
1222
+ const existingSdkSessionId = session?.sdk_session_id;
1223
+ const processor = new SDKMessageProcessor({
1224
+ sessionId,
1225
+ existingSdkSessionId,
1226
+ enableTokenStreaming: _ClaudePromptService.ENABLE_TOKEN_STREAMING,
1227
+ idleTimeoutMs: 12e4
1228
+ // 2 minutes - allows time for long operations (web search, file reads, etc.)
1229
+ });
1230
+ try {
1231
+ for await (const msg of result) {
1232
+ if (this.stopRequested.get(sessionId)) {
1233
+ console.log(
1234
+ `\u{1F6D1} Stop requested for session ${sessionId.substring(0, 8)}, breaking event loop`
1235
+ );
1236
+ this.stopRequested.delete(sessionId);
1237
+ break;
1238
+ }
1239
+ if (processor.hasTimedOut()) {
1240
+ const state = processor.getState();
1241
+ console.warn(
1242
+ `\u23F1\uFE0F No assistant messages for ${Math.round((Date.now() - state.lastAssistantMessageTime) / 1e3)}s - assuming conversation complete`
1243
+ );
1244
+ console.warn(
1245
+ ` SDK may not have sent 'result' message - breaking loop as safety measure`
1246
+ );
1247
+ break;
1248
+ }
1249
+ const events = await processor.process(msg);
1250
+ for (const event of events) {
1251
+ if (event.type === "session_id_captured") {
1252
+ if (this.sessionsRepo) {
1253
+ await this.sessionsRepo.update(sessionId, {
1254
+ sdk_session_id: event.agentSessionId
1255
+ });
1256
+ console.log(`\u{1F4BE} Stored Agent SDK session_id in database`);
1257
+ }
1258
+ continue;
1259
+ }
1260
+ if (event.type === "end") {
1261
+ console.log(`\u{1F3C1} Conversation ended: ${event.reason}`);
1262
+ break;
1263
+ }
1264
+ yield event;
1265
+ }
1266
+ if (events.some((e) => e.type === "end")) {
1267
+ break;
1268
+ }
1269
+ }
1270
+ } catch (error) {
1271
+ this.activeQueries.delete(sessionId);
1272
+ const state = processor.getState();
1273
+ const stderrOutput = getStderr();
1274
+ const errorContext = stderrOutput ? `
1275
+
1276
+ Claude Code stderr output:
1277
+ ${stderrOutput}` : "";
1278
+ const enhancedError = new Error(
1279
+ `Claude SDK error after ${state.messageCount} messages: ${error instanceof Error ? error.message : String(error)}${errorContext}`
1280
+ );
1281
+ if (error instanceof Error && error.stack) {
1282
+ enhancedError.stack = error.stack;
1283
+ }
1284
+ console.error(`\u274C SDK iteration failed:`, {
1285
+ sessionId: sessionId.substring(0, 8),
1286
+ messageCount: state.messageCount,
1287
+ error: error instanceof Error ? error.message : String(error),
1288
+ stderr: stderrOutput || "(no stderr output)"
1289
+ });
1290
+ throw enhancedError;
1291
+ }
1292
+ this.activeQueries.delete(sessionId);
1293
+ }
1294
+ /**
1295
+ * Prompt a session using Claude Agent SDK (non-streaming version)
1296
+ *
1297
+ * The Agent SDK automatically:
1298
+ * - Loads CLAUDE.md from the working directory
1299
+ * - Uses Claude Code preset system prompt
1300
+ * - Handles streaming via async generators
1301
+ *
1302
+ * @param sessionId - Session to prompt
1303
+ * @param prompt - User prompt
1304
+ * @returns Complete assistant response with metadata
1305
+ */
1306
+ async promptSession(sessionId, prompt) {
1307
+ const { query: result, getStderr } = await this.setupQuery(
1308
+ sessionId,
1309
+ prompt,
1310
+ void 0,
1311
+ void 0,
1312
+ false
1313
+ );
1314
+ const session = await this.sessionsRepo?.findById(sessionId);
1315
+ const existingSdkSessionId = session?.sdk_session_id;
1316
+ const processor = new SDKMessageProcessor({
1317
+ sessionId,
1318
+ existingSdkSessionId,
1319
+ enableTokenStreaming: false,
1320
+ // Non-streaming mode
1321
+ idleTimeoutMs: 12e4
1322
+ // 2 minutes - allows time for long operations
1323
+ });
1324
+ const assistantMessages = [];
1325
+ let tokenUsage;
1326
+ for await (const msg of result) {
1327
+ const events = await processor.process(msg);
1328
+ for (const event of events) {
1329
+ if (event.type === "complete" && event.role === "assistant" /* ASSISTANT */) {
1330
+ assistantMessages.push({
1331
+ content: event.content,
1332
+ toolUses: event.toolUses
1333
+ });
1334
+ }
1335
+ if (event.type === "result" && event.token_usage) {
1336
+ tokenUsage = event.token_usage;
1337
+ }
1338
+ if (event.type === "end") {
1339
+ break;
1340
+ }
1341
+ }
1342
+ }
1343
+ this.activeQueries.delete(sessionId);
1344
+ return {
1345
+ messages: assistantMessages,
1346
+ inputTokens: tokenUsage?.input_tokens || 0,
1347
+ outputTokens: tokenUsage?.output_tokens || 0
1348
+ };
1349
+ }
1350
+ /**
1351
+ * Stop currently executing task
1352
+ *
1353
+ * Uses Claude Agent SDK's native interrupt() method to gracefully stop execution.
1354
+ * This is the same mechanism used by the Escape key in Claude Code CLI.
1355
+ *
1356
+ * @param sessionId - Session identifier
1357
+ * @returns Success status
1358
+ */
1359
+ async stopTask(sessionId) {
1360
+ console.log(`\u{1F6D1} Stopping task for session ${sessionId.substring(0, 8)}`);
1361
+ const queryObj = this.activeQueries.get(sessionId);
1362
+ if (!queryObj) {
1363
+ return {
1364
+ success: false,
1365
+ reason: "No active task found for this session"
1366
+ };
1367
+ }
1368
+ try {
1369
+ this.stopRequested.set(sessionId, true);
1370
+ await queryObj.interrupt();
1371
+ this.activeQueries.delete(sessionId);
1372
+ console.log(`\u2705 Stopped Claude execution for session ${sessionId.substring(0, 8)}`);
1373
+ return { success: true };
1374
+ } catch (error) {
1375
+ console.error("Failed to interrupt Claude execution:", error);
1376
+ this.stopRequested.delete(sessionId);
1377
+ return {
1378
+ success: false,
1379
+ reason: error instanceof Error ? error.message : "Unknown error"
1380
+ };
1381
+ }
1382
+ }
1383
+ };
1384
+
1385
+ // src/tools/claude/claude-tool.ts
1386
+ function extractTokenUsage(raw) {
1387
+ if (!raw || typeof raw !== "object") return void 0;
1388
+ const obj = raw;
1389
+ return {
1390
+ input_tokens: typeof obj.input_tokens === "number" ? obj.input_tokens : void 0,
1391
+ output_tokens: typeof obj.output_tokens === "number" ? obj.output_tokens : void 0,
1392
+ total_tokens: typeof obj.total_tokens === "number" ? obj.total_tokens : void 0,
1393
+ cache_read_tokens: typeof obj.cache_read_input_tokens === "number" ? obj.cache_read_input_tokens : void 0,
1394
+ cache_creation_tokens: typeof obj.cache_creation_input_tokens === "number" ? obj.cache_creation_input_tokens : void 0
1395
+ };
1396
+ }
1397
+ var ClaudeTool = class {
1398
+ constructor(messagesRepo, sessionsRepo, apiKey, messagesService, sessionMCPRepo, mcpServerRepo, permissionService, tasksService, sessionsService, worktreesRepo) {
1399
+ this.messagesRepo = messagesRepo;
1400
+ this.sessionsRepo = sessionsRepo;
1401
+ this.messagesService = messagesService;
1402
+ this.tasksService = tasksService;
1403
+ if (messagesRepo && sessionsRepo) {
1404
+ this.promptService = new ClaudePromptService(
1405
+ messagesRepo,
1406
+ sessionsRepo,
1407
+ apiKey,
1408
+ sessionMCPRepo,
1409
+ mcpServerRepo,
1410
+ permissionService,
1411
+ tasksService,
1412
+ sessionsService,
1413
+ worktreesRepo,
1414
+ messagesService
1415
+ );
1416
+ }
1417
+ }
1418
+ toolType = "claude-code";
1419
+ name = "Claude Code";
1420
+ promptService;
1421
+ getCapabilities() {
1422
+ return {
1423
+ supportsSessionImport: true,
1424
+ // ✅ We have transcript parsing
1425
+ supportsSessionCreate: false,
1426
+ // ❌ Waiting for SDK
1427
+ supportsLiveExecution: true,
1428
+ // ✅ Now supported via Anthropic SDK
1429
+ supportsSessionFork: false,
1430
+ supportsChildSpawn: false,
1431
+ supportsGitState: true,
1432
+ // Transcripts contain git state
1433
+ supportsStreaming: true
1434
+ // ✅ Streaming via callbacks during message generation
1435
+ };
1436
+ }
1437
+ async checkInstalled() {
1438
+ try {
1439
+ const claudeDir = path4.join(os.homedir(), ".claude");
1440
+ const stats = await fs5.stat(claudeDir);
1441
+ return stats.isDirectory();
1442
+ } catch {
1443
+ return false;
1444
+ }
1445
+ }
1446
+ async importSession(sessionId, options) {
1447
+ const session = await loadClaudeSession(sessionId, options?.projectDir);
1448
+ const messages = transcriptsToMessages(session.messages, session.sessionId);
1449
+ const metadata = {
1450
+ sessionId: session.sessionId,
1451
+ toolType: this.toolType,
1452
+ status: TaskStatus.COMPLETED,
1453
+ // Historical sessions are always completed
1454
+ createdAt: new Date(session.messages[0]?.timestamp || Date.now()),
1455
+ lastUpdatedAt: new Date(
1456
+ session.messages[session.messages.length - 1]?.timestamp || Date.now()
1457
+ ),
1458
+ workingDirectory: session.cwd || void 0,
1459
+ messageCount: session.messages.length
1460
+ };
1461
+ return {
1462
+ sessionId: session.sessionId,
1463
+ toolType: this.toolType,
1464
+ messages,
1465
+ metadata,
1466
+ workingDirectory: session.cwd || void 0
1467
+ };
1468
+ }
1469
+ /**
1470
+ * Execute a prompt against a session WITH real-time streaming
1471
+ *
1472
+ * Creates user message, streams response chunks from Claude, then creates complete assistant messages.
1473
+ * Calls streamingCallbacks during message generation for real-time UI updates.
1474
+ * Agent SDK may return multiple assistant messages (e.g., tool invocation, then response).
1475
+ *
1476
+ * @param sessionId - Session to execute prompt in
1477
+ * @param prompt - User prompt text
1478
+ * @param taskId - Optional task ID for linking messages
1479
+ * @param permissionMode - Optional permission mode for SDK
1480
+ * @param streamingCallbacks - Optional callbacks for real-time streaming (enables typewriter effect)
1481
+ * @returns User message ID and array of assistant message IDs
1482
+ */
1483
+ async executePromptWithStreaming(sessionId, prompt, taskId, permissionMode, streamingCallbacks) {
1484
+ if (!this.promptService || !this.messagesRepo) {
1485
+ throw new Error("ClaudeTool not initialized with repositories for live execution");
1486
+ }
1487
+ if (!this.messagesService) {
1488
+ throw new Error("ClaudeTool not initialized with messagesService for live execution");
1489
+ }
1490
+ const existingMessages = await this.messagesRepo.findBySessionId(sessionId);
1491
+ let nextIndex = existingMessages.length;
1492
+ const userMessage = await this.createUserMessage(sessionId, prompt, taskId, nextIndex++);
1493
+ const assistantMessageIds = [];
1494
+ let capturedAgentSessionId;
1495
+ let resolvedModel;
1496
+ let currentMessageId = null;
1497
+ let streamStartTime = Date.now();
1498
+ let firstTokenTime = null;
1499
+ let tokenUsage;
1500
+ let durationMs;
1501
+ let contextWindow;
1502
+ let contextWindowLimit;
1503
+ for await (const event of this.promptService.promptSessionStreaming(
1504
+ sessionId,
1505
+ prompt,
1506
+ taskId,
1507
+ permissionMode
1508
+ )) {
1509
+ if (!resolvedModel && "resolvedModel" in event && event.resolvedModel) {
1510
+ resolvedModel = event.resolvedModel;
1511
+ }
1512
+ if (!capturedAgentSessionId && event.agentSessionId) {
1513
+ capturedAgentSessionId = event.agentSessionId;
1514
+ await this.captureAgentSessionId(sessionId, capturedAgentSessionId);
1515
+ }
1516
+ if (event.type === "tool_start") {
1517
+ if (this.tasksService && taskId) {
1518
+ this.tasksService.emit("tool:start", {
1519
+ task_id: taskId,
1520
+ session_id: sessionId,
1521
+ tool_use_id: event.toolUseId,
1522
+ tool_name: event.toolName
1523
+ });
1524
+ }
1525
+ }
1526
+ if (event.type === "tool_complete") {
1527
+ if (this.tasksService && taskId) {
1528
+ this.tasksService.emit("tool:complete", {
1529
+ task_id: taskId,
1530
+ session_id: sessionId,
1531
+ tool_use_id: event.toolUseId
1532
+ });
1533
+ }
1534
+ }
1535
+ if ("token_usage" in event && event.token_usage) {
1536
+ tokenUsage = extractTokenUsage(event.token_usage);
1537
+ }
1538
+ if ("duration_ms" in event && typeof event.duration_ms === "number") {
1539
+ durationMs = event.duration_ms;
1540
+ }
1541
+ if ("model_usage" in event && event.model_usage) {
1542
+ const modelUsage = event.model_usage;
1543
+ let maxUsage = 0;
1544
+ let maxLimit = 0;
1545
+ for (const modelData of Object.values(modelUsage)) {
1546
+ const usage = (modelData.inputTokens || 0) + (modelData.outputTokens || 0) + (modelData.cacheReadInputTokens || 0) + (modelData.cacheCreationInputTokens || 0);
1547
+ const limit = modelData.contextWindow || 0;
1548
+ if (usage > maxUsage) {
1549
+ maxUsage = usage;
1550
+ maxLimit = limit;
1551
+ }
1552
+ }
1553
+ contextWindow = maxUsage;
1554
+ contextWindowLimit = maxLimit;
1555
+ console.log(
1556
+ `\u{1F50D} [ClaudeTool] Context window: ${contextWindow}/${contextWindowLimit} (${(contextWindow / contextWindowLimit * 100).toFixed(1)}%)`
1557
+ );
1558
+ }
1559
+ if (event.type === "partial" && event.textChunk) {
1560
+ if (!currentMessageId) {
1561
+ currentMessageId = generateId();
1562
+ firstTokenTime = Date.now();
1563
+ const ttfb = firstTokenTime - streamStartTime;
1564
+ console.debug(`\u23F1\uFE0F [SDK] TTFB: ${ttfb}ms`);
1565
+ if (streamingCallbacks) {
1566
+ streamingCallbacks.onStreamStart(currentMessageId, {
1567
+ session_id: sessionId,
1568
+ task_id: taskId,
1569
+ role: "assistant" /* ASSISTANT */,
1570
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1571
+ });
1572
+ }
1573
+ }
1574
+ if (streamingCallbacks) {
1575
+ streamingCallbacks.onStreamChunk(currentMessageId, event.textChunk);
1576
+ }
1577
+ } else if (event.type === "complete" && event.content) {
1578
+ if (currentMessageId && streamingCallbacks && "role" in event && event.role === "assistant" /* ASSISTANT */) {
1579
+ const streamEndTime = Date.now();
1580
+ streamingCallbacks.onStreamEnd(currentMessageId);
1581
+ const totalTime = streamEndTime - streamStartTime;
1582
+ const streamingTime = firstTokenTime ? streamEndTime - firstTokenTime : 0;
1583
+ console.debug(
1584
+ `\u23F1\uFE0F [Streaming] Complete - TTFB: ${firstTokenTime ? firstTokenTime - streamStartTime : 0}ms, streaming: ${streamingTime}ms, total: ${totalTime}ms`
1585
+ );
1586
+ }
1587
+ if ("role" in event && event.role === "assistant" /* ASSISTANT */) {
1588
+ const assistantMessageId = currentMessageId || generateId();
1589
+ await this.createAssistantMessage(
1590
+ sessionId,
1591
+ assistantMessageId,
1592
+ event.content,
1593
+ event.toolUses,
1594
+ taskId,
1595
+ nextIndex++,
1596
+ resolvedModel
1597
+ );
1598
+ assistantMessageIds.push(assistantMessageId);
1599
+ currentMessageId = null;
1600
+ streamStartTime = Date.now();
1601
+ firstTokenTime = null;
1602
+ } else if ("role" in event && event.role === "user" /* USER */) {
1603
+ const userMessageId = generateId();
1604
+ await this.createUserMessageFromContent(
1605
+ sessionId,
1606
+ userMessageId,
1607
+ event.content,
1608
+ taskId,
1609
+ nextIndex++
1610
+ );
1611
+ }
1612
+ }
1613
+ }
1614
+ return {
1615
+ userMessageId: userMessage.message_id,
1616
+ assistantMessageIds,
1617
+ tokenUsage,
1618
+ durationMs,
1619
+ agentSessionId: capturedAgentSessionId,
1620
+ contextWindow,
1621
+ contextWindowLimit
1622
+ };
1623
+ }
1624
+ /**
1625
+ * Create user message in database (from text prompt)
1626
+ * @private
1627
+ */
1628
+ async createUserMessage(sessionId, prompt, taskId, nextIndex) {
1629
+ const userMessage = {
1630
+ message_id: generateId(),
1631
+ session_id: sessionId,
1632
+ type: "user",
1633
+ role: "user" /* USER */,
1634
+ index: nextIndex,
1635
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1636
+ content_preview: prompt.substring(0, 200),
1637
+ content: prompt,
1638
+ task_id: taskId
1639
+ };
1640
+ await this.messagesService?.create(userMessage);
1641
+ return userMessage;
1642
+ }
1643
+ /**
1644
+ * Create user message from SDK content (tool results, etc.)
1645
+ * @private
1646
+ */
1647
+ async createUserMessageFromContent(sessionId, messageId, content, taskId, nextIndex) {
1648
+ let contentPreview = "";
1649
+ for (const block of content) {
1650
+ if (block.type === "text" && block.text) {
1651
+ contentPreview = block.text.substring(0, 200);
1652
+ break;
1653
+ } else if (block.type === "tool_result" && block.content) {
1654
+ const resultText = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
1655
+ contentPreview = `Tool result: ${resultText.substring(0, 180)}`;
1656
+ break;
1657
+ }
1658
+ }
1659
+ const userMessage = {
1660
+ message_id: messageId,
1661
+ session_id: sessionId,
1662
+ type: "user",
1663
+ role: "user" /* USER */,
1664
+ index: nextIndex,
1665
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1666
+ content_preview: contentPreview,
1667
+ content,
1668
+ // Tool result blocks
1669
+ task_id: taskId
1670
+ };
1671
+ await this.messagesService?.create(userMessage);
1672
+ return userMessage;
1673
+ }
1674
+ /**
1675
+ * Capture and store Agent SDK session_id for conversation continuity
1676
+ * @private
1677
+ */
1678
+ async captureAgentSessionId(sessionId, agentSessionId) {
1679
+ console.log(
1680
+ `\u{1F511} Captured Agent SDK session_id for Agor session ${sessionId}: ${agentSessionId}`
1681
+ );
1682
+ if (this.sessionsRepo) {
1683
+ console.log(
1684
+ `\u{1F4DD} About to update session with: ${JSON.stringify({ sdk_session_id: agentSessionId })}`
1685
+ );
1686
+ const updated = await this.sessionsRepo.update(sessionId, {
1687
+ sdk_session_id: agentSessionId
1688
+ });
1689
+ console.log(`\u{1F4BE} Stored Agent SDK session_id in Agor session`);
1690
+ console.log(`\u{1F50D} Verify: updated.sdk_session_id = ${updated.sdk_session_id}`);
1691
+ }
1692
+ }
1693
+ /**
1694
+ * Create complete assistant message in database
1695
+ * @private
1696
+ */
1697
+ async createAssistantMessage(sessionId, messageId, content, toolUses, taskId, nextIndex, resolvedModel) {
1698
+ const textBlocks = content.filter((b) => b.type === "text").map((b) => b.text || "");
1699
+ const fullTextContent = textBlocks.join("");
1700
+ const contentPreview = fullTextContent.substring(0, 200);
1701
+ const message = {
1702
+ message_id: messageId,
1703
+ session_id: sessionId,
1704
+ type: "assistant",
1705
+ role: "assistant" /* ASSISTANT */,
1706
+ index: nextIndex,
1707
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1708
+ content_preview: contentPreview,
1709
+ content,
1710
+ tool_uses: toolUses,
1711
+ task_id: taskId,
1712
+ metadata: {
1713
+ model: resolvedModel || DEFAULT_CLAUDE_MODEL,
1714
+ tokens: {
1715
+ input: 0,
1716
+ // TODO: Extract from SDK
1717
+ output: 0
1718
+ }
1719
+ }
1720
+ };
1721
+ await this.messagesService?.create(message);
1722
+ if (taskId && resolvedModel && this.tasksService) {
1723
+ await this.tasksService.patch(taskId, { model: resolvedModel });
1724
+ }
1725
+ return message;
1726
+ }
1727
+ /**
1728
+ * Execute a prompt against a session (non-streaming version)
1729
+ *
1730
+ * Creates user message, streams response from Claude, creates assistant messages.
1731
+ * Agent SDK may return multiple assistant messages (e.g., tool invocation, then response).
1732
+ * Returns user message ID and array of assistant message IDs.
1733
+ *
1734
+ * Also captures and stores the Agent SDK session_id for conversation continuity.
1735
+ */
1736
+ async executePrompt(sessionId, prompt, taskId, permissionMode) {
1737
+ if (!this.promptService || !this.messagesRepo) {
1738
+ throw new Error("ClaudeTool not initialized with repositories for live execution");
1739
+ }
1740
+ if (!this.messagesService) {
1741
+ throw new Error("ClaudeTool not initialized with messagesService for live execution");
1742
+ }
1743
+ const existingMessages = await this.messagesRepo.findBySessionId(sessionId);
1744
+ let nextIndex = existingMessages.length;
1745
+ const userMessage = await this.createUserMessage(sessionId, prompt, taskId, nextIndex++);
1746
+ const assistantMessageIds = [];
1747
+ let capturedAgentSessionId;
1748
+ let resolvedModel;
1749
+ let tokenUsage;
1750
+ let durationMs;
1751
+ let contextWindow;
1752
+ let contextWindowLimit;
1753
+ for await (const event of this.promptService.promptSessionStreaming(
1754
+ sessionId,
1755
+ prompt,
1756
+ taskId,
1757
+ permissionMode
1758
+ )) {
1759
+ if (!resolvedModel && "resolvedModel" in event && event.resolvedModel) {
1760
+ resolvedModel = event.resolvedModel;
1761
+ }
1762
+ if (!capturedAgentSessionId && event.agentSessionId) {
1763
+ capturedAgentSessionId = event.agentSessionId;
1764
+ await this.captureAgentSessionId(sessionId, capturedAgentSessionId);
1765
+ }
1766
+ if ("token_usage" in event && event.token_usage) {
1767
+ tokenUsage = extractTokenUsage(event.token_usage);
1768
+ }
1769
+ if ("duration_ms" in event && typeof event.duration_ms === "number") {
1770
+ durationMs = event.duration_ms;
1771
+ }
1772
+ if ("model_usage" in event && event.model_usage) {
1773
+ const modelUsage = event.model_usage;
1774
+ let maxUsage = 0;
1775
+ let maxLimit = 0;
1776
+ for (const modelData of Object.values(modelUsage)) {
1777
+ const usage = (modelData.inputTokens || 0) + (modelData.outputTokens || 0) + (modelData.cacheReadInputTokens || 0) + (modelData.cacheCreationInputTokens || 0);
1778
+ const limit = modelData.contextWindow || 0;
1779
+ if (usage > maxUsage) {
1780
+ maxUsage = usage;
1781
+ maxLimit = limit;
1782
+ }
1783
+ }
1784
+ contextWindow = maxUsage;
1785
+ contextWindowLimit = maxLimit;
1786
+ console.log(
1787
+ `\u{1F50D} [ClaudeTool] Context window: ${contextWindow}/${contextWindowLimit} (${(contextWindow / contextWindowLimit * 100).toFixed(1)}%)`
1788
+ );
1789
+ }
1790
+ if (event.type === "partial") {
1791
+ continue;
1792
+ }
1793
+ if (event.type === "complete" && event.content) {
1794
+ const messageId = generateId();
1795
+ await this.createAssistantMessage(
1796
+ sessionId,
1797
+ messageId,
1798
+ event.content,
1799
+ event.toolUses,
1800
+ taskId,
1801
+ nextIndex++,
1802
+ resolvedModel
1803
+ );
1804
+ assistantMessageIds.push(messageId);
1805
+ }
1806
+ }
1807
+ return {
1808
+ userMessageId: userMessage.message_id,
1809
+ assistantMessageIds,
1810
+ tokenUsage,
1811
+ durationMs,
1812
+ agentSessionId: capturedAgentSessionId,
1813
+ contextWindow,
1814
+ contextWindowLimit
1815
+ };
1816
+ }
1817
+ /**
1818
+ * Stop currently executing task in session
1819
+ *
1820
+ * Uses Claude Agent SDK's native interrupt() method to gracefully stop execution.
1821
+ *
1822
+ * @param sessionId - Session identifier
1823
+ * @param taskId - Optional task ID (not used for Claude, session-level stop)
1824
+ * @returns Success status and reason if failed
1825
+ */
1826
+ async stopTask(sessionId, taskId) {
1827
+ if (!this.promptService) {
1828
+ return {
1829
+ success: false,
1830
+ reason: "ClaudeTool not initialized with prompt service"
1831
+ };
1832
+ }
1833
+ const result = await this.promptService.stopTask(sessionId);
1834
+ if (result.success) {
1835
+ return {
1836
+ success: true,
1837
+ partialResult: {
1838
+ taskId: taskId || "unknown",
1839
+ status: "cancelled"
1840
+ }
1841
+ };
1842
+ }
1843
+ return result;
1844
+ }
1845
+ };
1846
+
1847
+ // src/tools/claude/import/task-extractor.ts
1848
+ function extractTasksFromMessages(messages, sessionId) {
1849
+ const tasks = [];
1850
+ const userMessageIndices = messages.map((msg, idx) => msg.type === "user" ? idx : -1).filter((idx) => idx !== -1);
1851
+ for (let i = 0; i < userMessageIndices.length; i++) {
1852
+ const startIndex = userMessageIndices[i];
1853
+ const userMessage = messages[startIndex];
1854
+ const endIndex = i < userMessageIndices.length - 1 ? userMessageIndices[i + 1] - 1 : messages.length - 1;
1855
+ const messagesInRange = messages.slice(startIndex, endIndex + 1);
1856
+ const toolUseCount = messagesInRange.reduce((count, msg) => {
1857
+ return count + (msg.tool_uses?.length ?? 0);
1858
+ }, 0);
1859
+ let fullPrompt = "";
1860
+ if (typeof userMessage.content === "string") {
1861
+ fullPrompt = userMessage.content;
1862
+ } else if (Array.isArray(userMessage.content)) {
1863
+ const textContent = userMessage.content.filter((c) => c.type === "text").map((c) => c.text || "").join("\n");
1864
+ fullPrompt = textContent || JSON.stringify(userMessage.content);
1865
+ } else {
1866
+ fullPrompt = JSON.stringify(userMessage.content);
1867
+ }
1868
+ const cleanPrompt = fullPrompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
1869
+ const description = cleanPrompt.substring(0, 120) + (cleanPrompt.length > 120 ? "..." : "");
1870
+ const startTimestamp = userMessage.timestamp;
1871
+ const endMessage = messages[endIndex];
1872
+ const endTimestamp = endMessage?.timestamp;
1873
+ tasks.push({
1874
+ task_id: generateId(),
1875
+ session_id: sessionId,
1876
+ full_prompt: fullPrompt,
1877
+ description,
1878
+ status: TaskStatus.COMPLETED,
1879
+ // Imported sessions are historical
1880
+ message_range: {
1881
+ start_index: startIndex,
1882
+ end_index: endIndex,
1883
+ start_timestamp: startTimestamp,
1884
+ end_timestamp: endTimestamp
1885
+ },
1886
+ git_state: {
1887
+ ref_at_start: "unknown",
1888
+ // No git tracking in Claude Code transcripts
1889
+ sha_at_start: "unknown"
1890
+ // No git tracking in Claude Code transcripts
1891
+ },
1892
+ model: userMessage.metadata?.model || "claude-sonnet-4-5",
1893
+ tool_use_count: toolUseCount,
1894
+ created_at: startTimestamp,
1895
+ completed_at: endTimestamp
1896
+ });
1897
+ }
1898
+ return tasks;
1899
+ }
1900
+
1901
+ // src/tools/codex/codex-tool.ts
1902
+ import { execSync as execSync2 } from "child_process";
1903
+
1904
+ // src/tools/codex/models.ts
1905
+ var DEFAULT_CODEX_MODEL = "gpt-5-codex";
1906
+ var CODEX_MINI_MODEL = "codex-mini-latest";
1907
+ var CODEX_MODELS = {
1908
+ "gpt-5-codex": "gpt-5-codex",
1909
+ "codex-mini": "codex-mini-latest",
1910
+ "gpt-4o": "gpt-4o",
1911
+ "gpt-4o-mini": "gpt-4o-mini"
1912
+ };
1913
+
1914
+ // src/tools/codex/prompt-service.ts
1915
+ import * as fs6 from "fs/promises";
1916
+ import * as path5 from "path";
1917
+ import { Codex } from "@openai/codex-sdk";
1918
+ var CodexPromptService = class {
1919
+ constructor(_messagesRepo, sessionsRepo, apiKey) {
1920
+ this.sessionsRepo = sessionsRepo;
1921
+ this.codex = new Codex({
1922
+ apiKey: apiKey || process.env.OPENAI_API_KEY
1923
+ });
1924
+ }
1925
+ codex;
1926
+ lastApprovalPolicy = null;
1927
+ stopRequested = /* @__PURE__ */ new Map();
1928
+ /**
1929
+ * Generate ~/.codex/config.toml for approval_policy setting
1930
+ *
1931
+ * NOTE: approval_policy cannot be passed via ThreadOptions, so we must use config.toml.
1932
+ * We minimize file writes by tracking the last set value and only updating when it changes.
1933
+ */
1934
+ async ensureApprovalPolicy(permissionMode) {
1935
+ const approvalPolicyMap = {
1936
+ ask: "untrusted",
1937
+ // Ask before running any command
1938
+ auto: "on-request",
1939
+ // Model decides when to ask (recommended)
1940
+ "on-failure": "on-failure",
1941
+ // Ask only when commands fail
1942
+ "allow-all": "never"
1943
+ // Never ask, auto-approve all operations
1944
+ };
1945
+ const approvalPolicy = approvalPolicyMap[permissionMode];
1946
+ if (this.lastApprovalPolicy === approvalPolicy) {
1947
+ return;
1948
+ }
1949
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
1950
+ if (!homeDir) {
1951
+ console.warn("\u26A0\uFE0F Could not determine home directory, skipping approval_policy config");
1952
+ return;
1953
+ }
1954
+ const codexConfigDir = path5.join(homeDir, ".codex");
1955
+ const configPath = path5.join(codexConfigDir, "config.toml");
1956
+ const configContent = `# Codex configuration (approval_policy only - sandboxMode passed via SDK)
1957
+ # Generated by Agor - ${(/* @__PURE__ */ new Date()).toISOString()}
1958
+
1959
+ # Approval policy controls when Codex asks before running commands
1960
+ # Options: "untrusted", "on-request", "on-failure", "never"
1961
+ approval_policy = "${approvalPolicy}"
1962
+ `;
1963
+ await fs6.mkdir(codexConfigDir, { recursive: true });
1964
+ await fs6.writeFile(configPath, configContent, "utf-8");
1965
+ this.lastApprovalPolicy = approvalPolicy;
1966
+ console.log(`\u{1F4DD} [Codex] Set approval_policy = "${approvalPolicy}" in ~/.codex/config.toml`);
1967
+ }
1968
+ /**
1969
+ * Convert Codex item to ToolUse format
1970
+ * Maps different Codex item types to Agor tool use schema
1971
+ */
1972
+ itemToToolUse(item, status) {
1973
+ switch (item.type) {
1974
+ case "command_execution":
1975
+ return {
1976
+ id: item.id,
1977
+ name: "bash",
1978
+ input: { command: item.command },
1979
+ ...status === "completed" && {
1980
+ output: item.aggregated_output || "",
1981
+ status: item.status
1982
+ }
1983
+ };
1984
+ case "file_change":
1985
+ return {
1986
+ id: item.id,
1987
+ name: "edit_files",
1988
+ input: {
1989
+ changes: item.changes || []
1990
+ },
1991
+ ...status === "completed" && {
1992
+ status: item.status
1993
+ }
1994
+ };
1995
+ case "mcp_tool_call":
1996
+ return {
1997
+ id: item.id,
1998
+ name: `${item.server}.${item.tool}`,
1999
+ input: {},
2000
+ ...status === "completed" && {
2001
+ status: item.status
2002
+ }
2003
+ };
2004
+ case "web_search":
2005
+ return {
2006
+ id: item.id,
2007
+ name: "web_search",
2008
+ input: { query: item.query }
2009
+ };
2010
+ case "reasoning":
2011
+ return null;
2012
+ case "todo_list":
2013
+ return null;
2014
+ case "agent_message":
2015
+ return null;
2016
+ default:
2017
+ return null;
2018
+ }
2019
+ }
2020
+ /**
2021
+ * Execute prompt with streaming support
2022
+ *
2023
+ * Uses Codex SDK's runStreamed() method for real-time event streaming.
2024
+ * Yields partial text chunks and complete messages.
2025
+ *
2026
+ * @param sessionId - Agor session ID
2027
+ * @param prompt - User prompt
2028
+ * @param taskId - Optional task ID
2029
+ * @param permissionMode - Permission mode for tool execution ('ask' | 'auto' | 'allow-all')
2030
+ * @returns Async generator of streaming events
2031
+ */
2032
+ async *promptSessionStreaming(sessionId, prompt, _taskId, permissionMode) {
2033
+ const session = await this.sessionsRepo.findById(sessionId);
2034
+ if (!session) {
2035
+ throw new Error(`Session ${sessionId} not found`);
2036
+ }
2037
+ console.log(`\u{1F50D} [Codex] Starting prompt execution for session ${sessionId.substring(0, 8)}`);
2038
+ console.log(` Permission mode: ${permissionMode || "not specified (will use default)"}`);
2039
+ console.log(` Existing thread ID: ${session.sdk_session_id || "none (will create new)"}`);
2040
+ const effectivePermissionMode = permissionMode || session.permission_config?.mode || "auto";
2041
+ const sandboxModeMap = {
2042
+ ask: "read-only",
2043
+ auto: "workspace-write",
2044
+ "on-failure": "workspace-write",
2045
+ "allow-all": "workspace-write"
2046
+ };
2047
+ const sandboxMode = sandboxModeMap[effectivePermissionMode];
2048
+ await this.ensureApprovalPolicy(effectivePermissionMode);
2049
+ console.log(` Configured: sandboxMode=${sandboxMode}, approval_policy via config.toml`);
2050
+ const threadOptions = {
2051
+ workingDirectory: process.cwd(),
2052
+ // Temporary fallback
2053
+ skipGitRepoCheck: false,
2054
+ sandboxMode
2055
+ };
2056
+ const sessionPermissionMode = session.permission_config?.mode || "auto";
2057
+ const permissionModeChanged = effectivePermissionMode !== sessionPermissionMode;
2058
+ let thread;
2059
+ if (session.sdk_session_id) {
2060
+ console.log(`\u{1F504} [Codex] Resuming thread: ${session.sdk_session_id}`);
2061
+ thread = this.codex.resumeThread(session.sdk_session_id, threadOptions);
2062
+ if (permissionModeChanged) {
2063
+ console.log(
2064
+ `\u2699\uFE0F [Codex] Permission mode changed: ${sessionPermissionMode} \u2192 ${effectivePermissionMode}`
2065
+ );
2066
+ console.log(` Sending slash commands to update thread settings...`);
2067
+ const approvalModeMap = {
2068
+ ask: "untrusted",
2069
+ auto: "on-request",
2070
+ "on-failure": "on-failure",
2071
+ "allow-all": "never"
2072
+ };
2073
+ const approvalMode = approvalModeMap[effectivePermissionMode];
2074
+ const slashCommand = `/approvals ${approvalMode}`;
2075
+ console.log(` Executing: ${slashCommand}`);
2076
+ try {
2077
+ await thread.run(slashCommand);
2078
+ console.log(`\u2705 [Codex] Thread settings updated successfully`);
2079
+ } catch (error) {
2080
+ console.error(`\u274C [Codex] Failed to update thread settings:`, error);
2081
+ }
2082
+ }
2083
+ } else {
2084
+ console.log(`\u{1F195} [Codex] Creating new thread`);
2085
+ thread = this.codex.startThread(threadOptions);
2086
+ }
2087
+ try {
2088
+ console.log(
2089
+ `\u25B6\uFE0F [Codex] Running prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? "..." : ""}"`
2090
+ );
2091
+ const { events } = await thread.runStreamed(prompt);
2092
+ let currentMessage = [];
2093
+ let threadId = session.sdk_session_id || "";
2094
+ let resolvedModel;
2095
+ let allToolUses = [];
2096
+ for await (const event of events) {
2097
+ if (this.stopRequested.get(sessionId)) {
2098
+ console.log(`\u{1F6D1} Stop requested for session ${sessionId}, breaking event loop`);
2099
+ this.stopRequested.delete(sessionId);
2100
+ break;
2101
+ }
2102
+ switch (event.type) {
2103
+ case "turn.started":
2104
+ allToolUses = [];
2105
+ break;
2106
+ case "item.started":
2107
+ if (event.item) {
2108
+ const toolUseStart = this.itemToToolUse(event.item, "started");
2109
+ if (toolUseStart) {
2110
+ yield {
2111
+ type: "tool_start",
2112
+ toolUse: toolUseStart,
2113
+ threadId: thread.id || void 0
2114
+ };
2115
+ }
2116
+ }
2117
+ break;
2118
+ case "item.updated":
2119
+ break;
2120
+ case "item.completed":
2121
+ if (event.item) {
2122
+ const toolUseComplete = this.itemToToolUse(event.item, "completed");
2123
+ if (toolUseComplete) {
2124
+ allToolUses.push({
2125
+ id: toolUseComplete.id,
2126
+ name: toolUseComplete.name,
2127
+ input: toolUseComplete.input
2128
+ });
2129
+ currentMessage.push({
2130
+ type: "tool_use",
2131
+ id: toolUseComplete.id,
2132
+ name: toolUseComplete.name,
2133
+ input: toolUseComplete.input
2134
+ });
2135
+ if (toolUseComplete.output !== void 0 || toolUseComplete.status) {
2136
+ const isError = toolUseComplete.status === "failed" || toolUseComplete.status === "error";
2137
+ let content = toolUseComplete.output || "";
2138
+ if (!content && toolUseComplete.status) {
2139
+ content = `[${toolUseComplete.status}]`;
2140
+ }
2141
+ currentMessage.push({
2142
+ type: "tool_result",
2143
+ tool_use_id: toolUseComplete.id,
2144
+ content,
2145
+ is_error: isError
2146
+ });
2147
+ }
2148
+ yield {
2149
+ type: "tool_complete",
2150
+ toolUse: toolUseComplete,
2151
+ threadId: thread.id || void 0
2152
+ };
2153
+ }
2154
+ if ("text" in event.item && event.item.type === "agent_message") {
2155
+ currentMessage.push({
2156
+ type: "text",
2157
+ text: event.item.text
2158
+ });
2159
+ }
2160
+ }
2161
+ break;
2162
+ case "turn.completed": {
2163
+ threadId = thread.id || "";
2164
+ yield {
2165
+ type: "complete",
2166
+ content: currentMessage,
2167
+ toolUses: allToolUses.length > 0 ? allToolUses : void 0,
2168
+ threadId,
2169
+ resolvedModel: resolvedModel || DEFAULT_CODEX_MODEL
2170
+ };
2171
+ currentMessage = [];
2172
+ allToolUses = [];
2173
+ break;
2174
+ }
2175
+ case "turn.failed":
2176
+ console.error("\u274C Codex turn failed:", event.error);
2177
+ throw new Error(`Codex execution failed: ${event.error}`);
2178
+ default:
2179
+ break;
2180
+ }
2181
+ }
2182
+ } catch (error) {
2183
+ console.error("\u274C Codex streaming error:", error);
2184
+ throw error;
2185
+ }
2186
+ }
2187
+ /**
2188
+ * Execute prompt (non-streaming version)
2189
+ *
2190
+ * Collects all streaming events and returns complete result.
2191
+ *
2192
+ * @param sessionId - Agor session ID
2193
+ * @param prompt - User prompt
2194
+ * @param taskId - Optional task ID
2195
+ * @param permissionMode - Permission mode for tool execution ('ask' | 'auto' | 'allow-all')
2196
+ * @returns Complete prompt result
2197
+ */
2198
+ async promptSession(sessionId, prompt, taskId, permissionMode) {
2199
+ const messages = [];
2200
+ let threadId = "";
2201
+ const inputTokens = 0;
2202
+ const outputTokens = 0;
2203
+ for await (const event of this.promptSessionStreaming(
2204
+ sessionId,
2205
+ prompt,
2206
+ taskId,
2207
+ permissionMode
2208
+ )) {
2209
+ if (event.type === "complete") {
2210
+ messages.push({
2211
+ content: event.content,
2212
+ toolUses: event.toolUses
2213
+ });
2214
+ threadId = event.threadId;
2215
+ }
2216
+ }
2217
+ return {
2218
+ messages,
2219
+ inputTokens,
2220
+ outputTokens,
2221
+ threadId
2222
+ };
2223
+ }
2224
+ /**
2225
+ * Stop currently executing task
2226
+ *
2227
+ * Sets a stop flag that is checked in the event loop.
2228
+ * The loop will break on the next iteration, stopping execution gracefully.
2229
+ *
2230
+ * @param sessionId - Session identifier
2231
+ * @returns Success status
2232
+ */
2233
+ stopTask(sessionId) {
2234
+ this.stopRequested.set(sessionId, true);
2235
+ console.log(`\u{1F6D1} Stop requested for Codex session ${sessionId}`);
2236
+ return { success: true };
2237
+ }
2238
+ };
2239
+
2240
+ // src/tools/codex/codex-tool.ts
2241
+ var CodexTool = class {
2242
+ constructor(messagesRepo, sessionsRepo, apiKey, messagesService, tasksService) {
2243
+ this.messagesRepo = messagesRepo;
2244
+ this.sessionsRepo = sessionsRepo;
2245
+ this.messagesService = messagesService;
2246
+ this.tasksService = tasksService;
2247
+ if (messagesRepo && sessionsRepo) {
2248
+ this.promptService = new CodexPromptService(messagesRepo, sessionsRepo, apiKey);
2249
+ }
2250
+ }
2251
+ toolType = "codex";
2252
+ name = "OpenAI Codex";
2253
+ promptService;
2254
+ getCapabilities() {
2255
+ return {
2256
+ supportsSessionImport: false,
2257
+ // ❌ Deferred until we have real JSONL format
2258
+ supportsSessionCreate: false,
2259
+ // ❌ Not exposed (handled via executeTask)
2260
+ supportsLiveExecution: true,
2261
+ // ✅ Via Codex SDK
2262
+ supportsSessionFork: false,
2263
+ supportsChildSpawn: false,
2264
+ supportsGitState: false,
2265
+ // Agor manages git state
2266
+ supportsStreaming: true
2267
+ // ✅ Via runStreamed()
2268
+ };
2269
+ }
2270
+ async checkInstalled() {
2271
+ try {
2272
+ execSync2("which codex", { encoding: "utf-8" });
2273
+ return true;
2274
+ } catch {
2275
+ return false;
2276
+ }
2277
+ }
2278
+ /**
2279
+ * Execute a prompt against a session WITH real-time streaming
2280
+ *
2281
+ * Creates user message, streams response chunks from Codex, then creates complete assistant messages.
2282
+ * Calls streamingCallbacks during message generation for real-time UI updates.
2283
+ *
2284
+ * @param sessionId - Session to execute prompt in
2285
+ * @param prompt - User prompt text
2286
+ * @param taskId - Optional task ID for linking messages
2287
+ * @param permissionMode - Permission mode for tool execution ('ask' | 'auto' | 'allow-all')
2288
+ * @param streamingCallbacks - Optional callbacks for real-time streaming (enables typewriter effect)
2289
+ * @returns User message ID and array of assistant message IDs
2290
+ */
2291
+ async executePromptWithStreaming(sessionId, prompt, taskId, permissionMode, streamingCallbacks) {
2292
+ if (!this.promptService || !this.messagesRepo) {
2293
+ throw new Error("CodexTool not initialized with repositories for live execution");
2294
+ }
2295
+ if (!this.messagesService) {
2296
+ throw new Error("CodexTool not initialized with messagesService for live execution");
2297
+ }
2298
+ const existingMessages = await this.messagesRepo.findBySessionId(sessionId);
2299
+ let nextIndex = existingMessages.length;
2300
+ const userMessage = await this.createUserMessage(sessionId, prompt, taskId, nextIndex++);
2301
+ const assistantMessageIds = [];
2302
+ let capturedThreadId;
2303
+ let resolvedModel;
2304
+ let currentMessageId = null;
2305
+ let _streamStartTime = Date.now();
2306
+ let _firstTokenTime = null;
2307
+ for await (const event of this.promptService.promptSessionStreaming(
2308
+ sessionId,
2309
+ prompt,
2310
+ taskId,
2311
+ permissionMode
2312
+ )) {
2313
+ if (!resolvedModel) {
2314
+ if (event.type === "partial") {
2315
+ resolvedModel = event.resolvedModel;
2316
+ } else if (event.type === "complete") {
2317
+ resolvedModel = event.resolvedModel;
2318
+ }
2319
+ }
2320
+ if (!capturedThreadId && event.threadId) {
2321
+ capturedThreadId = event.threadId;
2322
+ await this.captureThreadId(sessionId, capturedThreadId);
2323
+ }
2324
+ if (event.type === "partial" && event.textChunk) {
2325
+ if (!currentMessageId) {
2326
+ currentMessageId = generateId();
2327
+ _firstTokenTime = Date.now();
2328
+ if (streamingCallbacks) {
2329
+ streamingCallbacks.onStreamStart(currentMessageId, {
2330
+ session_id: sessionId,
2331
+ task_id: taskId,
2332
+ role: "assistant" /* ASSISTANT */,
2333
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2334
+ });
2335
+ }
2336
+ }
2337
+ if (streamingCallbacks) {
2338
+ streamingCallbacks.onStreamChunk(currentMessageId, event.textChunk);
2339
+ }
2340
+ } else if (event.type === "tool_complete") {
2341
+ const toolMessageId = generateId();
2342
+ const toolContent = [
2343
+ {
2344
+ type: "tool_use",
2345
+ id: event.toolUse.id,
2346
+ name: event.toolUse.name,
2347
+ input: event.toolUse.input
2348
+ },
2349
+ ...event.toolUse.output !== void 0 || event.toolUse.status ? [
2350
+ {
2351
+ type: "tool_result",
2352
+ tool_use_id: event.toolUse.id,
2353
+ content: event.toolUse.output || `[${event.toolUse.status}]`,
2354
+ is_error: event.toolUse.status === "failed" || event.toolUse.status === "error"
2355
+ }
2356
+ ] : []
2357
+ ];
2358
+ await this.createAssistantMessage(
2359
+ sessionId,
2360
+ toolMessageId,
2361
+ toolContent,
2362
+ [
2363
+ {
2364
+ id: event.toolUse.id,
2365
+ name: event.toolUse.name,
2366
+ input: event.toolUse.input
2367
+ }
2368
+ ],
2369
+ taskId,
2370
+ nextIndex++,
2371
+ resolvedModel
2372
+ );
2373
+ assistantMessageIds.push(toolMessageId);
2374
+ } else if (event.type === "complete" && event.content) {
2375
+ const textOnlyContent = event.content.filter(
2376
+ (block) => block.type === "text"
2377
+ // Only keep text blocks
2378
+ );
2379
+ if (textOnlyContent.length > 0) {
2380
+ const _fullText = textOnlyContent.map((block) => block.text || "").join("");
2381
+ const assistantMessageId = currentMessageId || generateId();
2382
+ await this.createAssistantMessage(
2383
+ sessionId,
2384
+ assistantMessageId,
2385
+ textOnlyContent,
2386
+ void 0,
2387
+ // No tool uses in this message (already saved separately)
2388
+ taskId,
2389
+ nextIndex++,
2390
+ resolvedModel
2391
+ );
2392
+ assistantMessageIds.push(assistantMessageId);
2393
+ currentMessageId = null;
2394
+ }
2395
+ _streamStartTime = Date.now();
2396
+ _firstTokenTime = null;
2397
+ }
2398
+ }
2399
+ return {
2400
+ userMessageId: userMessage.message_id,
2401
+ assistantMessageIds
2402
+ };
2403
+ }
2404
+ /**
2405
+ * Create user message in database
2406
+ * @private
2407
+ */
2408
+ async createUserMessage(sessionId, prompt, taskId, nextIndex) {
2409
+ const userMessage = {
2410
+ message_id: generateId(),
2411
+ session_id: sessionId,
2412
+ type: "user",
2413
+ role: "user" /* USER */,
2414
+ index: nextIndex,
2415
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2416
+ content_preview: prompt.substring(0, 200),
2417
+ content: prompt,
2418
+ task_id: taskId
2419
+ };
2420
+ await this.messagesService?.create(userMessage);
2421
+ return userMessage;
2422
+ }
2423
+ /**
2424
+ * Capture and store Codex thread ID for conversation continuity
2425
+ * @private
2426
+ */
2427
+ async captureThreadId(sessionId, threadId) {
2428
+ console.log(`\u{1F511} Captured Codex thread ID for Agor session ${sessionId}: ${threadId}`);
2429
+ if (this.sessionsRepo) {
2430
+ await this.sessionsRepo.update(sessionId, { sdk_session_id: threadId });
2431
+ console.log(`\u{1F4BE} Stored Codex thread ID in Agor session`);
2432
+ }
2433
+ }
2434
+ /**
2435
+ * Create complete assistant message in database
2436
+ * @private
2437
+ */
2438
+ async createAssistantMessage(sessionId, messageId, content, toolUses, taskId, nextIndex, resolvedModel) {
2439
+ const textBlocks = content.filter((b) => b.type === "text").map((b) => b.text || "");
2440
+ const fullTextContent = textBlocks.join("");
2441
+ const contentPreview = fullTextContent.substring(0, 200);
2442
+ const message = {
2443
+ message_id: messageId,
2444
+ session_id: sessionId,
2445
+ type: "assistant",
2446
+ role: "assistant" /* ASSISTANT */,
2447
+ index: nextIndex,
2448
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2449
+ content_preview: contentPreview,
2450
+ content,
2451
+ tool_uses: toolUses,
2452
+ task_id: taskId,
2453
+ metadata: {
2454
+ model: resolvedModel || DEFAULT_CODEX_MODEL,
2455
+ tokens: {
2456
+ input: 0,
2457
+ // TODO: Extract from Codex SDK
2458
+ output: 0
2459
+ }
2460
+ }
2461
+ };
2462
+ await this.messagesService?.create(message);
2463
+ if (taskId && resolvedModel && this.tasksService) {
2464
+ await this.tasksService.patch(taskId, { model: resolvedModel });
2465
+ }
2466
+ return message;
2467
+ }
2468
+ /**
2469
+ * Execute a prompt against a session (non-streaming version)
2470
+ *
2471
+ * Creates user message, collects response from Codex, creates assistant messages.
2472
+ * Returns user message ID and array of assistant message IDs.
2473
+ *
2474
+ * @param sessionId - Session to execute prompt in
2475
+ * @param prompt - User prompt text
2476
+ * @param taskId - Optional task ID for linking messages
2477
+ * @param permissionMode - Permission mode for tool execution ('ask' | 'auto' | 'allow-all')
2478
+ */
2479
+ async executePrompt(sessionId, prompt, taskId, permissionMode) {
2480
+ if (!this.promptService || !this.messagesRepo) {
2481
+ throw new Error("CodexTool not initialized with repositories for live execution");
2482
+ }
2483
+ if (!this.messagesService) {
2484
+ throw new Error("CodexTool not initialized with messagesService for live execution");
2485
+ }
2486
+ const existingMessages = await this.messagesRepo.findBySessionId(sessionId);
2487
+ let nextIndex = existingMessages.length;
2488
+ const userMessage = await this.createUserMessage(sessionId, prompt, taskId, nextIndex++);
2489
+ const assistantMessageIds = [];
2490
+ let capturedThreadId;
2491
+ let resolvedModel;
2492
+ for await (const event of this.promptService.promptSessionStreaming(
2493
+ sessionId,
2494
+ prompt,
2495
+ taskId,
2496
+ permissionMode
2497
+ )) {
2498
+ if (!resolvedModel) {
2499
+ if (event.type === "partial") {
2500
+ resolvedModel = event.resolvedModel;
2501
+ } else if (event.type === "complete") {
2502
+ resolvedModel = event.resolvedModel;
2503
+ }
2504
+ }
2505
+ if (!capturedThreadId && event.threadId) {
2506
+ capturedThreadId = event.threadId;
2507
+ await this.captureThreadId(sessionId, capturedThreadId);
2508
+ }
2509
+ if (event.type === "partial" || event.type === "tool_start" || event.type === "tool_complete") {
2510
+ continue;
2511
+ }
2512
+ if (event.type === "complete" && event.content) {
2513
+ const messageId = generateId();
2514
+ await this.createAssistantMessage(
2515
+ sessionId,
2516
+ messageId,
2517
+ event.content,
2518
+ event.toolUses,
2519
+ taskId,
2520
+ nextIndex++,
2521
+ resolvedModel
2522
+ );
2523
+ assistantMessageIds.push(messageId);
2524
+ }
2525
+ }
2526
+ return {
2527
+ userMessageId: userMessage.message_id,
2528
+ assistantMessageIds
2529
+ };
2530
+ }
2531
+ /**
2532
+ * Stop currently executing task in session
2533
+ *
2534
+ * Uses a flag-based approach to break the event loop on the next iteration.
2535
+ *
2536
+ * @param sessionId - Session identifier
2537
+ * @param taskId - Optional task ID (not used for Codex, session-level stop)
2538
+ * @returns Success status and reason if failed
2539
+ */
2540
+ async stopTask(sessionId, taskId) {
2541
+ if (!this.promptService) {
2542
+ return {
2543
+ success: false,
2544
+ reason: "CodexTool not initialized with prompt service"
2545
+ };
2546
+ }
2547
+ const result = this.promptService.stopTask(sessionId);
2548
+ if (result.success) {
2549
+ return {
2550
+ success: true,
2551
+ partialResult: {
2552
+ taskId: taskId || "unknown",
2553
+ status: "cancelled"
2554
+ }
2555
+ };
2556
+ }
2557
+ return result;
2558
+ }
2559
+ };
2560
+
2561
+ // src/tools/gemini/gemini-tool.ts
2562
+ import { execSync as execSync3 } from "child_process";
2563
+
2564
+ // src/tools/gemini/models.ts
2565
+ var DEFAULT_GEMINI_MODEL = "gemini-2.5-flash";
2566
+ var GEMINI_MODELS = {
2567
+ "gemini-2.5-pro": {
2568
+ name: "Gemini 2.5 Pro",
2569
+ description: "Most capable model for complex reasoning and multi-step tasks",
2570
+ inputPrice: "Higher",
2571
+ // Pricing not publicly disclosed yet
2572
+ outputPrice: "Higher",
2573
+ useCase: "Complex refactoring, architecture decisions, advanced debugging"
2574
+ },
2575
+ "gemini-2.5-flash": {
2576
+ name: "Gemini 2.5 Flash",
2577
+ description: "Balanced performance and cost for most agentic coding tasks",
2578
+ inputPrice: "$0.30",
2579
+ outputPrice: "$2.50",
2580
+ useCase: "Feature development, bug fixes, code reviews, testing"
2581
+ },
2582
+ "gemini-2.5-flash-lite": {
2583
+ name: "Gemini 2.5 Flash-Lite",
2584
+ description: "Ultra-fast, low-cost model for simple tasks",
2585
+ inputPrice: "$0.10",
2586
+ outputPrice: "$0.40",
2587
+ useCase: "File search, summaries, simple edits, code formatting"
2588
+ }
2589
+ };
2590
+
2591
+ // src/tools/gemini/prompt-service.ts
2592
+ import * as crypto from "crypto";
2593
+ import * as fs7 from "fs/promises";
2594
+ import * as os2 from "os";
2595
+ import * as path6 from "path";
2596
+ import {
2597
+ ApprovalMode,
2598
+ AuthType,
2599
+ Config,
2600
+ executeToolCall,
2601
+ GeminiClient,
2602
+ GeminiEventType
2603
+ } from "@google/gemini-cli-core";
2604
+ var GeminiPromptService = class {
2605
+ constructor(_messagesRepo, sessionsRepo, _apiKey) {
2606
+ this.sessionsRepo = sessionsRepo;
2607
+ }
2608
+ sessionClients = /* @__PURE__ */ new Map();
2609
+ activeControllers = /* @__PURE__ */ new Map();
2610
+ /**
2611
+ * Execute prompt with streaming via @google/gemini-cli-core SDK
2612
+ *
2613
+ * @param sessionId - Agor session ID
2614
+ * @param prompt - User prompt text
2615
+ * @param taskId - Optional task ID for message linking
2616
+ * @param permissionMode - Agor permission mode ('ask' | 'auto' | 'allow-all')
2617
+ * @yields Streaming events (partial chunks and complete messages)
2618
+ */
2619
+ async *promptSessionStreaming(sessionId, prompt, _taskId, permissionMode) {
2620
+ const client = await this.getOrCreateClient(sessionId, permissionMode);
2621
+ const session = await this.sessionsRepo.findById(sessionId);
2622
+ if (!session) {
2623
+ throw new Error(`Session ${sessionId} not found`);
2624
+ }
2625
+ const model = session.model_config?.model || DEFAULT_GEMINI_MODEL;
2626
+ let parts = [{ text: prompt }];
2627
+ const abortController = new AbortController();
2628
+ this.activeControllers.set(sessionId, abortController);
2629
+ const promptId = `${sessionId}-${Date.now()}`;
2630
+ try {
2631
+ let loopCount = 0;
2632
+ const MAX_LOOPS = 50;
2633
+ while (loopCount < MAX_LOOPS) {
2634
+ loopCount++;
2635
+ console.debug(`[Gemini Loop ${loopCount}] Starting turn with ${parts.length} parts`);
2636
+ const stream = client.sendMessageStream(parts, abortController.signal, promptId);
2637
+ let fullTextContent = "";
2638
+ const toolUses = [];
2639
+ const pendingToolCalls = [];
2640
+ for await (const event of stream) {
2641
+ const eventValue = "value" in event ? event.value : void 0;
2642
+ console.debug(
2643
+ `[Gemini Event] ${event.type}:`,
2644
+ eventValue ? JSON.stringify(eventValue).slice(0, 100) : "(no value)"
2645
+ );
2646
+ switch (event.type) {
2647
+ case GeminiEventType.Content: {
2648
+ const textChunk = event.value || "";
2649
+ fullTextContent += textChunk;
2650
+ yield {
2651
+ type: "partial",
2652
+ textChunk,
2653
+ resolvedModel: model,
2654
+ sessionId
2655
+ };
2656
+ break;
2657
+ }
2658
+ case GeminiEventType.ToolCallRequest: {
2659
+ const { name, args, callId } = event.value;
2660
+ toolUses.push({
2661
+ id: callId,
2662
+ name,
2663
+ input: args
2664
+ });
2665
+ pendingToolCalls.push({
2666
+ callId,
2667
+ name,
2668
+ args
2669
+ });
2670
+ yield {
2671
+ type: "tool_start",
2672
+ toolName: name,
2673
+ toolInput: args
2674
+ };
2675
+ break;
2676
+ }
2677
+ case GeminiEventType.ToolCallResponse: {
2678
+ const toolResponse = event.value;
2679
+ yield {
2680
+ type: "tool_complete",
2681
+ toolName: toolResponse.name || "unknown",
2682
+ result: toolResponse.response || toolResponse
2683
+ };
2684
+ break;
2685
+ }
2686
+ case GeminiEventType.Finished: {
2687
+ console.debug(
2688
+ `[Gemini Turn Finished] Text: ${fullTextContent.length} chars, Tools: ${toolUses.length}`
2689
+ );
2690
+ const content = [];
2691
+ if (fullTextContent) {
2692
+ content.push({
2693
+ type: "text",
2694
+ text: fullTextContent
2695
+ });
2696
+ }
2697
+ for (const toolUse of toolUses) {
2698
+ content.push({
2699
+ type: "tool_use",
2700
+ id: toolUse.id,
2701
+ name: toolUse.name,
2702
+ input: toolUse.input
2703
+ });
2704
+ }
2705
+ if (content.length > 0) {
2706
+ yield {
2707
+ type: "complete",
2708
+ content,
2709
+ toolUses: toolUses.length > 0 ? toolUses : void 0,
2710
+ resolvedModel: model,
2711
+ sessionId
2712
+ };
2713
+ }
2714
+ await this.updateSessionHistory(sessionId, client);
2715
+ break;
2716
+ }
2717
+ case GeminiEventType.Error: {
2718
+ const errorValue = "value" in event ? event.value : "Unknown error";
2719
+ console.error(`Gemini SDK error: ${JSON.stringify(errorValue)}`);
2720
+ let errorMessage = "Unknown error";
2721
+ if (typeof errorValue === "object" && errorValue !== null) {
2722
+ if ("error" in errorValue && typeof errorValue.error === "object" && errorValue.error !== null) {
2723
+ const errorObj = errorValue.error;
2724
+ errorMessage = errorObj.message || JSON.stringify(errorValue);
2725
+ } else {
2726
+ errorMessage = JSON.stringify(errorValue);
2727
+ }
2728
+ } else if (typeof errorValue === "string") {
2729
+ errorMessage = errorValue;
2730
+ }
2731
+ throw new Error(`Gemini execution failed: ${errorMessage}`);
2732
+ }
2733
+ case GeminiEventType.Thought: {
2734
+ const thoughtValue = "value" in event ? event.value : "";
2735
+ console.debug(`[Gemini Thought] ${thoughtValue}`);
2736
+ break;
2737
+ }
2738
+ case GeminiEventType.ToolCallConfirmation: {
2739
+ console.warn(
2740
+ "[Gemini] Tool call needs confirmation - this should not happen in AUTO_EDIT/YOLO mode!"
2741
+ );
2742
+ console.warn("[Gemini] Confirmation details:", JSON.stringify(event.value, null, 2));
2743
+ break;
2744
+ }
2745
+ default: {
2746
+ const debugValue = "value" in event ? event.value : "";
2747
+ console.debug(`[Gemini Event] ${event.type}:`, debugValue);
2748
+ break;
2749
+ }
2750
+ }
2751
+ }
2752
+ if (pendingToolCalls.length === 0) {
2753
+ console.debug("[Gemini Loop] No pending tool calls - conversation complete!");
2754
+ break;
2755
+ }
2756
+ console.debug(`[Gemini Loop] Found ${pendingToolCalls.length} pending tool calls`);
2757
+ const config = client.config;
2758
+ const functionResponseParts = [];
2759
+ for (const toolCall of pendingToolCalls) {
2760
+ try {
2761
+ console.debug(
2762
+ `[Gemini Loop] Executing tool: ${toolCall.name} with args:`,
2763
+ JSON.stringify(toolCall.args).slice(0, 100)
2764
+ );
2765
+ const response = await executeToolCall(
2766
+ config,
2767
+ {
2768
+ callId: toolCall.callId,
2769
+ name: toolCall.name,
2770
+ args: toolCall.args,
2771
+ isClientInitiated: false,
2772
+ prompt_id: promptId
2773
+ },
2774
+ abortController.signal
2775
+ );
2776
+ console.debug(`[Gemini Loop] Tool ${toolCall.name} executed successfully`);
2777
+ functionResponseParts.push(...response.responseParts);
2778
+ } catch (error) {
2779
+ console.error(`[Gemini Loop] Error executing tool ${toolCall.name}:`, error);
2780
+ functionResponseParts.push({
2781
+ functionResponse: {
2782
+ name: toolCall.name,
2783
+ response: { error: String(error) }
2784
+ }
2785
+ });
2786
+ }
2787
+ }
2788
+ parts = functionResponseParts;
2789
+ console.debug(
2790
+ `[Gemini Loop] Sending ${functionResponseParts.length} tool result parts back to model...`
2791
+ );
2792
+ }
2793
+ if (loopCount >= MAX_LOOPS) {
2794
+ console.warn(
2795
+ `[Gemini Loop] Hit maximum loop count (${MAX_LOOPS}) - stopping to prevent infinite loop`
2796
+ );
2797
+ }
2798
+ } catch (error) {
2799
+ if (error instanceof Error && error.name === "AbortError") {
2800
+ console.log(`\u{1F6D1} Gemini execution stopped for session ${sessionId}`);
2801
+ return;
2802
+ }
2803
+ console.error("Gemini streaming error:", error);
2804
+ throw error;
2805
+ } finally {
2806
+ this.activeControllers.delete(sessionId);
2807
+ }
2808
+ }
2809
+ /**
2810
+ * Load session file from SDK's filesystem storage
2811
+ *
2812
+ * Searches for session file in ~/.gemini/tmp/{projectHash}/chats/
2813
+ * matching pattern: session-*-{sessionId-first8}.json
2814
+ */
2815
+ async loadSessionFile(sessionId, projectRoot) {
2816
+ try {
2817
+ const projectHash = crypto.createHash("sha256").update(projectRoot).digest("hex");
2818
+ const chatsDir = path6.join(os2.homedir(), ".gemini", "tmp", projectHash, "chats");
2819
+ try {
2820
+ await fs7.access(chatsDir);
2821
+ } catch {
2822
+ console.debug(`No chats directory found for project ${projectRoot}`);
2823
+ return null;
2824
+ }
2825
+ const sessionIdShort = sessionId.slice(0, 8);
2826
+ const files = await fs7.readdir(chatsDir);
2827
+ const sessionFile = files.find((f) => f.includes(sessionIdShort) && f.endsWith(".json"));
2828
+ if (!sessionFile) {
2829
+ console.debug(`No session file found for ${sessionId} (looking for *${sessionIdShort}*)`);
2830
+ return null;
2831
+ }
2832
+ const filePath = path6.join(chatsDir, sessionFile);
2833
+ const fileContent = await fs7.readFile(filePath, "utf-8");
2834
+ const conversation = JSON.parse(fileContent);
2835
+ console.log(`\u{1F4C2} Found session file: ${sessionFile}`);
2836
+ return { conversation, filePath };
2837
+ } catch (error) {
2838
+ console.error("Error loading session file:", error);
2839
+ return null;
2840
+ }
2841
+ }
2842
+ /**
2843
+ * Get or create GeminiClient for a session
2844
+ *
2845
+ * Manages client lifecycle and session continuity via history restoration.
2846
+ */
2847
+ async getOrCreateClient(sessionId, permissionMode) {
2848
+ const approvalMode = this.mapPermissionMode(permissionMode || "ask");
2849
+ if (this.sessionClients.has(sessionId)) {
2850
+ const existingClient = this.sessionClients.get(sessionId);
2851
+ const config2 = existingClient.config;
2852
+ if (config2 && typeof config2.setApprovalMode === "function") {
2853
+ config2.setApprovalMode(approvalMode);
2854
+ console.log(`\u{1F504} [Gemini] Updated approval mode for existing client: ${approvalMode}`);
2855
+ }
2856
+ return existingClient;
2857
+ }
2858
+ const session = await this.sessionsRepo.findById(sessionId);
2859
+ if (!session) {
2860
+ throw new Error(`Session ${sessionId} not found`);
2861
+ }
2862
+ const workingDirectory = process.cwd();
2863
+ const model = session.model_config?.model || DEFAULT_GEMINI_MODEL;
2864
+ console.log(
2865
+ `\u{1F527} [Gemini] Creating new client with approval mode: ${permissionMode || "ask"} \u2192 ${approvalMode}`
2866
+ );
2867
+ const claudeMdPath = path6.join(workingDirectory, "CLAUDE.md");
2868
+ let systemPrompt;
2869
+ try {
2870
+ const claudeMdContent = await fs7.readFile(claudeMdPath, "utf-8");
2871
+ systemPrompt = `# Project Context
2872
+
2873
+ ${claudeMdContent}`;
2874
+ console.log(`\u{1F4D6} Loaded CLAUDE.md from ${claudeMdPath}`);
2875
+ } catch {
2876
+ }
2877
+ const config = new Config({
2878
+ sessionId,
2879
+ // Use Agor session ID
2880
+ targetDir: workingDirectory,
2881
+ cwd: workingDirectory,
2882
+ model,
2883
+ interactive: false,
2884
+ // Use non-interactive mode (we'll handle tool execution ourselves)
2885
+ approvalMode,
2886
+ debugMode: true,
2887
+ // Enable debug logging to see what's happening
2888
+ folderTrust: true,
2889
+ // CRITICAL: Trust folder to allow YOLO/AUTO_EDIT modes
2890
+ trustedFolder: true,
2891
+ // CRITICAL: Mark folder as trusted
2892
+ fileFiltering: {
2893
+ respectGitIgnore: true,
2894
+ respectGeminiIgnore: true
2895
+ }
2896
+ // output: { format: 'stream-json' }, // Streaming JSON events (omitting for now - may not be needed)
2897
+ // System prompt will be added via first message if provided
2898
+ });
2899
+ await config.initialize();
2900
+ await config.refreshAuth(AuthType.USE_GEMINI);
2901
+ const resumedSessionData = await this.loadSessionFile(sessionId, workingDirectory);
2902
+ const client = new GeminiClient(config);
2903
+ await client.initialize();
2904
+ let hasExistingHistory = false;
2905
+ if (resumedSessionData) {
2906
+ const recordingService = client.getChatRecordingService();
2907
+ if (recordingService) {
2908
+ recordingService.initialize(resumedSessionData);
2909
+ console.log(
2910
+ `\u{1F504} Resumed session from file: ${resumedSessionData.conversation.messages.length} messages`
2911
+ );
2912
+ hasExistingHistory = true;
2913
+ const history = this.convertConversationToHistory(resumedSessionData.conversation);
2914
+ client.setHistory(history);
2915
+ }
2916
+ }
2917
+ if (systemPrompt && !hasExistingHistory) {
2918
+ }
2919
+ this.sessionClients.set(sessionId, client);
2920
+ return client;
2921
+ }
2922
+ /**
2923
+ * Map Agor permission mode to Gemini ApprovalMode
2924
+ *
2925
+ * Gemini SDK supports 3 modes:
2926
+ * - DEFAULT: Prompt for each tool use
2927
+ * - AUTO_EDIT: Auto-approve file edits, prompt for shell/web commands
2928
+ * - YOLO: Auto-approve all operations
2929
+ */
2930
+ mapPermissionMode(permissionMode) {
2931
+ switch (permissionMode) {
2932
+ case "default":
2933
+ case "ask":
2934
+ return ApprovalMode.DEFAULT;
2935
+ // Prompt for each tool use
2936
+ case "acceptEdits":
2937
+ case "auto":
2938
+ return ApprovalMode.YOLO;
2939
+ // Auto-approve all operations (was: AUTO_EDIT)
2940
+ case "bypassPermissions":
2941
+ case "allow-all":
2942
+ return ApprovalMode.YOLO;
2943
+ // Auto-approve all operations
2944
+ default:
2945
+ return ApprovalMode.DEFAULT;
2946
+ }
2947
+ }
2948
+ /**
2949
+ * Convert SDK's ConversationRecord to Gemini Content[] format
2950
+ *
2951
+ * This converts the SDK's session file format into the API format needed for setHistory()
2952
+ */
2953
+ convertConversationToHistory(conversation) {
2954
+ const history = [];
2955
+ for (const msg of conversation.messages) {
2956
+ const role = msg.type === "user" ? "user" : "model";
2957
+ const parts = [];
2958
+ const content = msg.content;
2959
+ if (Array.isArray(content)) {
2960
+ parts.push(...content);
2961
+ } else if (content && typeof content === "object" && "text" in content) {
2962
+ parts.push(content);
2963
+ }
2964
+ if (parts.length > 0) {
2965
+ history.push({ role, parts });
2966
+ }
2967
+ }
2968
+ return history;
2969
+ }
2970
+ /**
2971
+ * Update session history after turn completion
2972
+ *
2973
+ * The SDK's ChatRecordingService automatically persists to filesystem,
2974
+ * so we just log for debugging purposes.
2975
+ */
2976
+ async updateSessionHistory(sessionId, client) {
2977
+ const history = client.getHistory();
2978
+ const recordingService = client.getChatRecordingService();
2979
+ if (recordingService) {
2980
+ console.debug(
2981
+ `\u{1F4DD} Session ${sessionId} history updated: ${history.length} turns (auto-saved to filesystem)`
2982
+ );
2983
+ } else {
2984
+ console.warn(
2985
+ `\u26A0\uFE0F No ChatRecordingService found for session ${sessionId} - history not persisted`
2986
+ );
2987
+ }
2988
+ }
2989
+ /**
2990
+ * Stop currently executing task
2991
+ *
2992
+ * Calls abort() on the AbortController to gracefully stop streaming.
2993
+ *
2994
+ * @param sessionId - Session identifier
2995
+ * @returns Success status
2996
+ */
2997
+ stopTask(sessionId) {
2998
+ const controller = this.activeControllers.get(sessionId);
2999
+ if (!controller) {
3000
+ return {
3001
+ success: false,
3002
+ reason: "No active task found for this session"
3003
+ };
3004
+ }
3005
+ controller.abort();
3006
+ console.log(`\u{1F6D1} Stopping Gemini task for session ${sessionId}`);
3007
+ return { success: true };
3008
+ }
3009
+ /**
3010
+ * Clean up client for a session (e.g., on session close)
3011
+ */
3012
+ async closeSession(sessionId) {
3013
+ const client = this.sessionClients.get(sessionId);
3014
+ if (client) {
3015
+ await client.resetChat();
3016
+ this.sessionClients.delete(sessionId);
3017
+ console.log(`\u{1F5D1}\uFE0F Closed Gemini client for session ${sessionId}`);
3018
+ }
3019
+ }
3020
+ };
3021
+
3022
+ // src/tools/gemini/gemini-tool.ts
3023
+ var GeminiTool = class {
3024
+ constructor(messagesRepo, sessionsRepo, apiKey, messagesService, tasksService) {
3025
+ this.messagesRepo = messagesRepo;
3026
+ this.messagesService = messagesService;
3027
+ this.tasksService = tasksService;
3028
+ if (messagesRepo && sessionsRepo) {
3029
+ this.promptService = new GeminiPromptService(messagesRepo, sessionsRepo, apiKey);
3030
+ }
3031
+ }
3032
+ toolType = "gemini";
3033
+ name = "Google Gemini";
3034
+ promptService;
3035
+ getCapabilities() {
3036
+ return {
3037
+ supportsSessionImport: false,
3038
+ // ❌ Deferred until checkpoint format is documented
3039
+ supportsSessionCreate: false,
3040
+ // ❌ Not exposed (handled via executeTask)
3041
+ supportsLiveExecution: true,
3042
+ // ✅ Via @google/gemini-cli-core SDK
3043
+ supportsSessionFork: false,
3044
+ supportsChildSpawn: false,
3045
+ supportsGitState: false,
3046
+ // Agor manages git state
3047
+ supportsStreaming: true
3048
+ // ✅ Via sendMessageStream()
3049
+ };
3050
+ }
3051
+ async checkInstalled() {
3052
+ try {
3053
+ execSync3("which gemini", { encoding: "utf-8" });
3054
+ return true;
3055
+ } catch {
3056
+ return false;
3057
+ }
3058
+ }
3059
+ /**
3060
+ * Execute a prompt against a session WITH real-time streaming
3061
+ *
3062
+ * Creates user message, streams response chunks from Gemini, then creates complete assistant messages.
3063
+ * Calls streamingCallbacks during message generation for real-time UI updates.
3064
+ *
3065
+ * @param sessionId - Session to execute prompt in
3066
+ * @param prompt - User prompt text
3067
+ * @param taskId - Optional task ID for linking messages
3068
+ * @param permissionMode - Permission mode for tool execution ('ask' | 'auto' | 'allow-all')
3069
+ * @param streamingCallbacks - Optional callbacks for real-time streaming (enables typewriter effect)
3070
+ * @returns User message ID and array of assistant message IDs
3071
+ */
3072
+ async executePromptWithStreaming(sessionId, prompt, taskId, permissionMode, streamingCallbacks) {
3073
+ if (!this.promptService || !this.messagesRepo) {
3074
+ throw new Error("GeminiTool not initialized with repositories for live execution");
3075
+ }
3076
+ if (!this.messagesService) {
3077
+ throw new Error("GeminiTool not initialized with messagesService for live execution");
3078
+ }
3079
+ const existingMessages = await this.messagesRepo.findBySessionId(sessionId);
3080
+ let nextIndex = existingMessages.length;
3081
+ const userMessage = await this.createUserMessage(sessionId, prompt, taskId, nextIndex++);
3082
+ const assistantMessageIds = [];
3083
+ let resolvedModel;
3084
+ let currentMessageId = null;
3085
+ let streamStartTime = Date.now();
3086
+ let firstTokenTime = null;
3087
+ for await (const event of this.promptService.promptSessionStreaming(
3088
+ sessionId,
3089
+ prompt,
3090
+ taskId,
3091
+ permissionMode
3092
+ )) {
3093
+ if (!resolvedModel) {
3094
+ if (event.type === "partial") {
3095
+ resolvedModel = event.resolvedModel;
3096
+ } else if (event.type === "complete") {
3097
+ resolvedModel = event.resolvedModel;
3098
+ }
3099
+ }
3100
+ if (event.type === "partial" && event.textChunk) {
3101
+ if (!currentMessageId) {
3102
+ currentMessageId = generateId();
3103
+ firstTokenTime = Date.now();
3104
+ const ttfb = firstTokenTime - streamStartTime;
3105
+ console.debug(`\u23F1\uFE0F [Gemini] TTFB: ${ttfb}ms`);
3106
+ if (streamingCallbacks) {
3107
+ streamingCallbacks.onStreamStart(currentMessageId, {
3108
+ session_id: sessionId,
3109
+ task_id: taskId,
3110
+ role: "assistant" /* ASSISTANT */,
3111
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3112
+ });
3113
+ }
3114
+ }
3115
+ if (streamingCallbacks) {
3116
+ streamingCallbacks.onStreamChunk(currentMessageId, event.textChunk);
3117
+ }
3118
+ } else if (event.type === "complete" && event.content) {
3119
+ if (currentMessageId && streamingCallbacks) {
3120
+ const streamEndTime = Date.now();
3121
+ streamingCallbacks.onStreamEnd(currentMessageId);
3122
+ const totalTime = streamEndTime - streamStartTime;
3123
+ const streamingTime = firstTokenTime ? streamEndTime - firstTokenTime : 0;
3124
+ console.debug(
3125
+ `\u23F1\uFE0F [Streaming] Complete - TTFB: ${firstTokenTime ? firstTokenTime - streamStartTime : 0}ms, streaming: ${streamingTime}ms, total: ${totalTime}ms`
3126
+ );
3127
+ }
3128
+ const assistantMessageId = currentMessageId || generateId();
3129
+ await this.createAssistantMessage(
3130
+ sessionId,
3131
+ assistantMessageId,
3132
+ event.content,
3133
+ event.toolUses,
3134
+ taskId,
3135
+ nextIndex++,
3136
+ resolvedModel
3137
+ );
3138
+ assistantMessageIds.push(assistantMessageId);
3139
+ currentMessageId = null;
3140
+ streamStartTime = Date.now();
3141
+ firstTokenTime = null;
3142
+ }
3143
+ }
3144
+ return {
3145
+ userMessageId: userMessage.message_id,
3146
+ assistantMessageIds
3147
+ };
3148
+ }
3149
+ /**
3150
+ * Create user message in database
3151
+ * @private
3152
+ */
3153
+ async createUserMessage(sessionId, prompt, taskId, nextIndex) {
3154
+ const userMessage = {
3155
+ message_id: generateId(),
3156
+ session_id: sessionId,
3157
+ type: "user",
3158
+ role: "user" /* USER */,
3159
+ index: nextIndex,
3160
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3161
+ content_preview: prompt.substring(0, 200),
3162
+ content: prompt,
3163
+ task_id: taskId
3164
+ };
3165
+ await this.messagesService?.create(userMessage);
3166
+ return userMessage;
3167
+ }
3168
+ /**
3169
+ * Create complete assistant message in database
3170
+ * @private
3171
+ */
3172
+ async createAssistantMessage(sessionId, messageId, content, toolUses, taskId, nextIndex, resolvedModel) {
3173
+ const textBlocks = content.filter((b) => b.type === "text").map((b) => b.text || "");
3174
+ const fullTextContent = textBlocks.join("");
3175
+ const contentPreview = fullTextContent.substring(0, 200);
3176
+ const message = {
3177
+ message_id: messageId,
3178
+ session_id: sessionId,
3179
+ type: "assistant",
3180
+ role: "assistant" /* ASSISTANT */,
3181
+ index: nextIndex,
3182
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3183
+ content_preview: contentPreview,
3184
+ content,
3185
+ tool_uses: toolUses,
3186
+ task_id: taskId,
3187
+ metadata: {
3188
+ model: resolvedModel || DEFAULT_GEMINI_MODEL,
3189
+ tokens: {
3190
+ input: 0,
3191
+ // TODO: Extract from Gemini SDK usage metadata
3192
+ output: 0
3193
+ }
3194
+ }
3195
+ };
3196
+ await this.messagesService?.create(message);
3197
+ if (taskId && resolvedModel && this.tasksService) {
3198
+ await this.tasksService.patch(taskId, { model: resolvedModel });
3199
+ }
3200
+ return message;
3201
+ }
3202
+ /**
3203
+ * Execute a prompt against a session (non-streaming version)
3204
+ *
3205
+ * Creates user message, collects response from Gemini, creates assistant messages.
3206
+ * Returns user message ID and array of assistant message IDs.
3207
+ *
3208
+ * @param sessionId - Session to execute prompt in
3209
+ * @param prompt - User prompt text
3210
+ * @param taskId - Optional task ID for linking messages
3211
+ * @param permissionMode - Permission mode for tool execution ('ask' | 'auto' | 'allow-all')
3212
+ */
3213
+ async executePrompt(sessionId, prompt, taskId, permissionMode) {
3214
+ if (!this.promptService || !this.messagesRepo) {
3215
+ throw new Error("GeminiTool not initialized with repositories for live execution");
3216
+ }
3217
+ if (!this.messagesService) {
3218
+ throw new Error("GeminiTool not initialized with messagesService for live execution");
3219
+ }
3220
+ const existingMessages = await this.messagesRepo.findBySessionId(sessionId);
3221
+ let nextIndex = existingMessages.length;
3222
+ const userMessage = await this.createUserMessage(sessionId, prompt, taskId, nextIndex++);
3223
+ const assistantMessageIds = [];
3224
+ let resolvedModel;
3225
+ for await (const event of this.promptService.promptSessionStreaming(
3226
+ sessionId,
3227
+ prompt,
3228
+ taskId,
3229
+ permissionMode
3230
+ )) {
3231
+ if (!resolvedModel) {
3232
+ if (event.type === "partial") {
3233
+ resolvedModel = event.resolvedModel;
3234
+ } else if (event.type === "complete") {
3235
+ resolvedModel = event.resolvedModel;
3236
+ }
3237
+ }
3238
+ if (event.type === "partial" || event.type === "tool_start" || event.type === "tool_complete") {
3239
+ continue;
3240
+ }
3241
+ if (event.type === "complete" && event.content) {
3242
+ const messageId = generateId();
3243
+ await this.createAssistantMessage(
3244
+ sessionId,
3245
+ messageId,
3246
+ event.content,
3247
+ event.toolUses,
3248
+ taskId,
3249
+ nextIndex++,
3250
+ resolvedModel
3251
+ );
3252
+ assistantMessageIds.push(messageId);
3253
+ }
3254
+ }
3255
+ return {
3256
+ userMessageId: userMessage.message_id,
3257
+ assistantMessageIds
3258
+ };
3259
+ }
3260
+ /**
3261
+ * Stop currently executing task in session
3262
+ *
3263
+ * Uses AbortController to gracefully cancel the streaming request.
3264
+ *
3265
+ * @param sessionId - Session identifier
3266
+ * @param taskId - Optional task ID (not used for Gemini, session-level stop)
3267
+ * @returns Success status and reason if failed
3268
+ */
3269
+ async stopTask(sessionId, taskId) {
3270
+ if (!this.promptService) {
3271
+ return {
3272
+ success: false,
3273
+ reason: "GeminiTool not initialized with prompt service"
3274
+ };
3275
+ }
3276
+ const result = this.promptService.stopTask(sessionId);
3277
+ if (result.success) {
3278
+ return {
3279
+ success: true,
3280
+ partialResult: {
3281
+ taskId: taskId || "unknown",
3282
+ status: "cancelled"
3283
+ }
3284
+ };
3285
+ }
3286
+ return result;
3287
+ }
3288
+ };
3289
+ export {
3290
+ AVAILABLE_CLAUDE_MODEL_ALIASES,
3291
+ CODEX_MINI_MODEL,
3292
+ CODEX_MODELS,
3293
+ ClaudeTool,
3294
+ CodexPromptService,
3295
+ CodexTool,
3296
+ DEFAULT_CLAUDE_MODEL,
3297
+ DEFAULT_CODEX_MODEL,
3298
+ DEFAULT_GEMINI_MODEL,
3299
+ GEMINI_MODELS,
3300
+ GeminiPromptService,
3301
+ GeminiTool,
3302
+ appendSessionContextToCLAUDEmd,
3303
+ buildConversationTree,
3304
+ extractTasksFromMessages,
3305
+ filterConversationMessages,
3306
+ generateSessionContext,
3307
+ getTranscriptPath,
3308
+ loadClaudeSession,
3309
+ loadSessionTranscript,
3310
+ parseTranscript,
3311
+ removeSessionContextFromCLAUDEmd,
3312
+ transcriptToMessage,
3313
+ transcriptsToMessages
3314
+ };