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.
- package/LICENSE +94 -0
- package/README.md +163 -0
- package/bin/agor-daemon.js +20 -0
- package/bin/agor.js +14 -0
- package/dist/cli/base-command.d.ts +29 -0
- package/dist/cli/base-command.js +41 -0
- package/dist/cli/commands/board/add-session.d.ts +15 -0
- package/dist/cli/commands/board/add-session.js +102 -0
- package/dist/cli/commands/board/list.d.ts +14 -0
- package/dist/cli/commands/board/list.js +74 -0
- package/dist/cli/commands/config/clear.d.ts +13 -0
- package/dist/cli/commands/config/clear.js +21 -0
- package/dist/cli/commands/config/get.d.ts +13 -0
- package/dist/cli/commands/config/get.js +41 -0
- package/dist/cli/commands/config/index.d.ts +13 -0
- package/dist/cli/commands/config/index.js +118 -0
- package/dist/cli/commands/config/set.d.ts +14 -0
- package/dist/cli/commands/config/set.js +50 -0
- package/dist/cli/commands/config/unset.d.ts +13 -0
- package/dist/cli/commands/config/unset.js +35 -0
- package/dist/cli/commands/daemon/index.d.ts +13 -0
- package/dist/cli/commands/daemon/index.js +65 -0
- package/dist/cli/commands/daemon/logs.d.ts +13 -0
- package/dist/cli/commands/daemon/logs.js +78 -0
- package/dist/cli/commands/daemon/restart.d.ts +13 -0
- package/dist/cli/commands/daemon/restart.js +177 -0
- package/dist/cli/commands/daemon/start.d.ts +13 -0
- package/dist/cli/commands/daemon/start.js +193 -0
- package/dist/cli/commands/daemon/status.d.ts +13 -0
- package/dist/cli/commands/daemon/status.js +93 -0
- package/dist/cli/commands/daemon/stop.d.ts +13 -0
- package/dist/cli/commands/daemon/stop.js +108 -0
- package/dist/cli/commands/init.d.ts +44 -0
- package/dist/cli/commands/init.js +459 -0
- package/dist/cli/commands/mcp/add.d.ts +26 -0
- package/dist/cli/commands/mcp/add.js +162 -0
- package/dist/cli/commands/mcp/list.d.ts +16 -0
- package/dist/cli/commands/mcp/list.js +89 -0
- package/dist/cli/commands/mcp/remove.d.ts +17 -0
- package/dist/cli/commands/mcp/remove.js +86 -0
- package/dist/cli/commands/mcp/show.d.ts +14 -0
- package/dist/cli/commands/mcp/show.js +131 -0
- package/dist/cli/commands/repo/add.d.ts +16 -0
- package/dist/cli/commands/repo/add.js +105 -0
- package/dist/cli/commands/repo/list.d.ts +17 -0
- package/dist/cli/commands/repo/list.js +99 -0
- package/dist/cli/commands/repo/rm.d.ts +17 -0
- package/dist/cli/commands/repo/rm.js +126 -0
- package/dist/cli/commands/repo/worktree/add.d.ts +21 -0
- package/dist/cli/commands/repo/worktree/add.js +145 -0
- package/dist/cli/commands/repo/worktree/list.d.ts +21 -0
- package/dist/cli/commands/repo/worktree/list.js +136 -0
- package/dist/cli/commands/session/list.d.ts +30 -0
- package/dist/cli/commands/session/list.js +204 -0
- package/dist/cli/commands/session/load-claude.d.ts +16 -0
- package/dist/cli/commands/session/load-claude.js +211 -0
- package/dist/cli/commands/user/create-admin.d.ts +13 -0
- package/dist/cli/commands/user/create-admin.js +65 -0
- package/dist/cli/commands/user/create.d.ts +16 -0
- package/dist/cli/commands/user/create.js +126 -0
- package/dist/cli/commands/user/delete.d.ts +16 -0
- package/dist/cli/commands/user/delete.js +77 -0
- package/dist/cli/commands/user/list.d.ts +13 -0
- package/dist/cli/commands/user/list.js +78 -0
- package/dist/cli/commands/user/update.d.ts +19 -0
- package/dist/cli/commands/user/update.js +149 -0
- package/dist/cli/hooks/command-not-found.d.ts +9 -0
- package/dist/cli/hooks/command-not-found.js +14 -0
- package/dist/cli/lib/banner.d.ts +25 -0
- package/dist/cli/lib/banner.js +25 -0
- package/dist/cli/lib/context.d.ts +27 -0
- package/dist/cli/lib/context.js +32 -0
- package/dist/cli/lib/daemon-manager.d.ts +48 -0
- package/dist/cli/lib/daemon-manager.js +109 -0
- package/dist/cli/lib/help.d.ts +13 -0
- package/dist/cli/lib/help.js +46 -0
- package/dist/core/agentic-tool-B_gFNpk5.d.ts +33 -0
- package/dist/core/agentic-tool-DsyX8diw.d.cts +33 -0
- package/dist/core/api/index.cjs +98 -0
- package/dist/core/api/index.d.cts +174 -0
- package/dist/core/api/index.d.ts +174 -0
- package/dist/core/api/index.js +62 -0
- package/dist/core/board-comment-BUm0fpmD.d.cts +134 -0
- package/dist/core/board-comment-gC_-twPx.d.ts +134 -0
- package/dist/core/claude/index.cjs +673 -0
- package/dist/core/claude/index.d.cts +124 -0
- package/dist/core/claude/index.d.ts +124 -0
- package/dist/core/claude/index.js +629 -0
- package/dist/core/config/browser.cjs +165 -0
- package/dist/core/config/browser.d.cts +289 -0
- package/dist/core/config/browser.d.ts +289 -0
- package/dist/core/config/browser.js +131 -0
- package/dist/core/config/index.cjs +518 -0
- package/dist/core/config/index.d.cts +246 -0
- package/dist/core/config/index.d.ts +246 -0
- package/dist/core/config/index.js +451 -0
- package/dist/core/db/index.cjs +3726 -0
- package/dist/core/db/index.d.cts +631 -0
- package/dist/core/db/index.d.ts +631 -0
- package/dist/core/db/index.js +3649 -0
- package/dist/core/dist/agentic-tool-B_gFNpk5.d.ts +33 -0
- package/dist/core/dist/agentic-tool-DsyX8diw.d.cts +33 -0
- package/dist/core/dist/api/index.cjs +98 -0
- package/dist/core/dist/api/index.d.cts +174 -0
- package/dist/core/dist/api/index.d.ts +174 -0
- package/dist/core/dist/api/index.js +62 -0
- package/dist/core/dist/board-comment-BUm0fpmD.d.cts +134 -0
- package/dist/core/dist/board-comment-gC_-twPx.d.ts +134 -0
- package/dist/core/dist/claude/index.cjs +673 -0
- package/dist/core/dist/claude/index.d.cts +124 -0
- package/dist/core/dist/claude/index.d.ts +124 -0
- package/dist/core/dist/claude/index.js +629 -0
- package/dist/core/dist/config/browser.cjs +165 -0
- package/dist/core/dist/config/browser.d.cts +289 -0
- package/dist/core/dist/config/browser.d.ts +289 -0
- package/dist/core/dist/config/browser.js +131 -0
- package/dist/core/dist/config/index.cjs +518 -0
- package/dist/core/dist/config/index.d.cts +246 -0
- package/dist/core/dist/config/index.d.ts +246 -0
- package/dist/core/dist/config/index.js +451 -0
- package/dist/core/dist/db/index.cjs +3726 -0
- package/dist/core/dist/db/index.d.cts +631 -0
- package/dist/core/dist/db/index.d.ts +631 -0
- package/dist/core/dist/db/index.js +3649 -0
- package/dist/core/dist/environment/variable-resolver.cjs +92 -0
- package/dist/core/dist/environment/variable-resolver.d.cts +52 -0
- package/dist/core/dist/environment/variable-resolver.d.ts +52 -0
- package/dist/core/dist/environment/variable-resolver.js +53 -0
- package/dist/core/dist/feathers/index.cjs +66 -0
- package/dist/core/dist/feathers/index.d.cts +7 -0
- package/dist/core/dist/feathers/index.d.ts +7 -0
- package/dist/core/dist/feathers/index.js +25 -0
- package/dist/core/dist/feathers-BzHEPnpl.d.cts +228 -0
- package/dist/core/dist/feathers-BzHEPnpl.d.ts +228 -0
- package/dist/core/dist/git/index.cjs +302 -0
- package/dist/core/dist/git/index.d.cts +137 -0
- package/dist/core/dist/git/index.d.ts +137 -0
- package/dist/core/dist/git/index.js +260 -0
- package/dist/core/dist/id-DMqyogFB.d.cts +131 -0
- package/dist/core/dist/id-DMqyogFB.d.ts +131 -0
- package/dist/core/dist/index.cjs +4653 -0
- package/dist/core/dist/index.d.cts +23 -0
- package/dist/core/dist/index.d.ts +23 -0
- package/dist/core/dist/index.js +4509 -0
- package/dist/core/dist/message-BoxZISHg.d.cts +120 -0
- package/dist/core/dist/message-DvBzHu7V.d.ts +120 -0
- package/dist/core/dist/permissions/index.cjs +112 -0
- package/dist/core/dist/permissions/index.d.cts +81 -0
- package/dist/core/dist/permissions/index.d.ts +81 -0
- package/dist/core/dist/permissions/index.js +85 -0
- package/dist/core/dist/repo-3CUrCRbq.d.cts +405 -0
- package/dist/core/dist/repo-CnvJ0B6-.d.ts +405 -0
- package/dist/core/dist/session-BPjJlVdZ.d.cts +429 -0
- package/dist/core/dist/session-wAzjHatv.d.ts +429 -0
- package/dist/core/dist/task-BIEgT1DK.d.cts +163 -0
- package/dist/core/dist/task-DuIfiUbW.d.ts +163 -0
- package/dist/core/dist/templates/handlebars-helpers.cjs +156 -0
- package/dist/core/dist/templates/handlebars-helpers.d.cts +45 -0
- package/dist/core/dist/templates/handlebars-helpers.d.ts +45 -0
- package/dist/core/dist/templates/handlebars-helpers.js +119 -0
- package/dist/core/dist/tools/claude/models.cjs +70 -0
- package/dist/core/dist/tools/claude/models.d.cts +27 -0
- package/dist/core/dist/tools/claude/models.d.ts +27 -0
- package/dist/core/dist/tools/claude/models.js +44 -0
- package/dist/core/dist/tools/index.cjs +3367 -0
- package/dist/core/dist/tools/index.d.cts +967 -0
- package/dist/core/dist/tools/index.d.ts +967 -0
- package/dist/core/dist/tools/index.js +3314 -0
- package/dist/core/dist/tools/models.cjs +119 -0
- package/dist/core/dist/tools/models.d.cts +47 -0
- package/dist/core/dist/tools/models.d.ts +47 -0
- package/dist/core/dist/tools/models.js +86 -0
- package/dist/core/dist/types/index.cjs +152 -0
- package/dist/core/dist/types/index.d.cts +214 -0
- package/dist/core/dist/types/index.d.ts +214 -0
- package/dist/core/dist/types/index.js +112 -0
- package/dist/core/dist/user-BmL3kFol.d.ts +50 -0
- package/dist/core/dist/user-eUuKj7yM.d.cts +50 -0
- package/dist/core/dist/utils/pricing.cjs +102 -0
- package/dist/core/dist/utils/pricing.d.cts +43 -0
- package/dist/core/dist/utils/pricing.d.ts +43 -0
- package/dist/core/dist/utils/pricing.js +75 -0
- package/dist/core/dist/worktrees-BzIxB1U6.d.cts +2745 -0
- package/dist/core/dist/worktrees-CYem1ya2.d.ts +2745 -0
- package/dist/core/environment/variable-resolver.cjs +92 -0
- package/dist/core/environment/variable-resolver.d.cts +52 -0
- package/dist/core/environment/variable-resolver.d.ts +52 -0
- package/dist/core/environment/variable-resolver.js +53 -0
- package/dist/core/feathers/index.cjs +66 -0
- package/dist/core/feathers/index.d.cts +7 -0
- package/dist/core/feathers/index.d.ts +7 -0
- package/dist/core/feathers/index.js +25 -0
- package/dist/core/feathers-BzHEPnpl.d.cts +228 -0
- package/dist/core/feathers-BzHEPnpl.d.ts +228 -0
- package/dist/core/git/index.cjs +302 -0
- package/dist/core/git/index.d.cts +137 -0
- package/dist/core/git/index.d.ts +137 -0
- package/dist/core/git/index.js +260 -0
- package/dist/core/id-DMqyogFB.d.cts +131 -0
- package/dist/core/id-DMqyogFB.d.ts +131 -0
- package/dist/core/index.cjs +4653 -0
- package/dist/core/index.d.cts +23 -0
- package/dist/core/index.d.ts +23 -0
- package/dist/core/index.js +4509 -0
- package/dist/core/message-BoxZISHg.d.cts +120 -0
- package/dist/core/message-DvBzHu7V.d.ts +120 -0
- package/dist/core/package.json +133 -0
- package/dist/core/permissions/index.cjs +112 -0
- package/dist/core/permissions/index.d.cts +81 -0
- package/dist/core/permissions/index.d.ts +81 -0
- package/dist/core/permissions/index.js +85 -0
- package/dist/core/repo-3CUrCRbq.d.cts +405 -0
- package/dist/core/repo-CnvJ0B6-.d.ts +405 -0
- package/dist/core/session-BPjJlVdZ.d.cts +429 -0
- package/dist/core/session-wAzjHatv.d.ts +429 -0
- package/dist/core/task-BIEgT1DK.d.cts +163 -0
- package/dist/core/task-DuIfiUbW.d.ts +163 -0
- package/dist/core/templates/handlebars-helpers.cjs +156 -0
- package/dist/core/templates/handlebars-helpers.d.cts +45 -0
- package/dist/core/templates/handlebars-helpers.d.ts +45 -0
- package/dist/core/templates/handlebars-helpers.js +119 -0
- package/dist/core/tools/claude/models.cjs +70 -0
- package/dist/core/tools/claude/models.d.cts +27 -0
- package/dist/core/tools/claude/models.d.ts +27 -0
- package/dist/core/tools/claude/models.js +44 -0
- package/dist/core/tools/index.cjs +3367 -0
- package/dist/core/tools/index.d.cts +967 -0
- package/dist/core/tools/index.d.ts +967 -0
- package/dist/core/tools/index.js +3314 -0
- package/dist/core/tools/models.cjs +119 -0
- package/dist/core/tools/models.d.cts +47 -0
- package/dist/core/tools/models.d.ts +47 -0
- package/dist/core/tools/models.js +86 -0
- package/dist/core/types/index.cjs +152 -0
- package/dist/core/types/index.d.cts +214 -0
- package/dist/core/types/index.d.ts +214 -0
- package/dist/core/types/index.js +112 -0
- package/dist/core/user-BmL3kFol.d.ts +50 -0
- package/dist/core/user-eUuKj7yM.d.cts +50 -0
- package/dist/core/utils/pricing.cjs +102 -0
- package/dist/core/utils/pricing.d.cts +43 -0
- package/dist/core/utils/pricing.d.ts +43 -0
- package/dist/core/utils/pricing.js +75 -0
- package/dist/core/worktrees-BzIxB1U6.d.cts +2745 -0
- package/dist/core/worktrees-CYem1ya2.d.ts +2745 -0
- package/dist/daemon/adapters/drizzle.d.ts +114 -0
- package/dist/daemon/adapters/drizzle.js +219 -0
- package/dist/daemon/declarations.d.ts +101 -0
- package/dist/daemon/declarations.js +0 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +4093 -0
- package/dist/daemon/mcp/routes.d.ts +15 -0
- package/dist/daemon/mcp/routes.js +641 -0
- package/dist/daemon/mcp/tokens.d.ts +50 -0
- package/dist/daemon/mcp/tokens.js +85 -0
- package/dist/daemon/services/board-comments.d.ts +97 -0
- package/dist/daemon/services/board-comments.js +326 -0
- package/dist/daemon/services/board-objects.d.ts +71 -0
- package/dist/daemon/services/board-objects.js +117 -0
- package/dist/daemon/services/boards.d.ts +64 -0
- package/dist/daemon/services/boards.js +286 -0
- package/dist/daemon/services/config.d.ts +35 -0
- package/dist/daemon/services/config.js +68 -0
- package/dist/daemon/services/context.d.ts +55 -0
- package/dist/daemon/services/context.js +113 -0
- package/dist/daemon/services/health-monitor.d.ts +58 -0
- package/dist/daemon/services/health-monitor.js +158 -0
- package/dist/daemon/services/mcp-servers.d.ts +42 -0
- package/dist/daemon/services/mcp-servers.js +275 -0
- package/dist/daemon/services/messages.d.ts +49 -0
- package/dist/daemon/services/messages.js +269 -0
- package/dist/daemon/services/repos.d.ts +61 -0
- package/dist/daemon/services/repos.js +350 -0
- package/dist/daemon/services/session-mcp-servers.d.ts +56 -0
- package/dist/daemon/services/session-mcp-servers.js +51 -0
- package/dist/daemon/services/sessions.d.ts +64 -0
- package/dist/daemon/services/sessions.js +398 -0
- package/dist/daemon/services/tasks.d.ts +55 -0
- package/dist/daemon/services/tasks.js +318 -0
- package/dist/daemon/services/terminals.d.ts +75 -0
- package/dist/daemon/services/terminals.js +110 -0
- package/dist/daemon/services/users.d.ts +98 -0
- package/dist/daemon/services/users.js +177 -0
- package/dist/daemon/services/worktrees.d.ts +98 -0
- package/dist/daemon/services/worktrees.js +719 -0
- package/dist/daemon/strategies/anonymous.d.ts +20 -0
- package/dist/daemon/strategies/anonymous.js +32 -0
- package/dist/ui/assets/cc-CYmbalCD.png +0 -0
- package/dist/ui/assets/codex-4sLD1mVS.png +0 -0
- package/dist/ui/assets/cursor-BUy5pFVL.png +0 -0
- package/dist/ui/assets/gemini-ajOb7iAl.png +0 -0
- package/dist/ui/assets/index-Dc4ELxry.css +32 -0
- package/dist/ui/assets/index-KfIu8v4V.js +578 -0
- package/dist/ui/favicon.png +0 -0
- package/dist/ui/index.html +26 -0
- package/dist/ui/vite.svg +1 -0
- 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
|
+
};
|