@vibe80/vibe80 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/bin/vibe80.js +176 -0
  4. package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
  5. package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
  6. package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
  7. package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
  8. package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
  9. package/client/dist/assets/browser-e3WgtMs-.js +8 -0
  10. package/client/dist/assets/index-CgqGyssr.css +32 -0
  11. package/client/dist/assets/index-DnwKjoj7.js +706 -0
  12. package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
  13. package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
  14. package/client/dist/favicon.ico +0 -0
  15. package/client/dist/favicon.png +0 -0
  16. package/client/dist/favicon.svg +35 -0
  17. package/client/dist/index.html +14 -0
  18. package/client/index.html +16 -0
  19. package/client/package.json +34 -0
  20. package/client/public/favicon.ico +0 -0
  21. package/client/public/favicon.png +0 -0
  22. package/client/public/favicon.svg +35 -0
  23. package/client/public/pwa-192x192.png +0 -0
  24. package/client/public/pwa-512x512.png +0 -0
  25. package/client/src/App.jsx +3131 -0
  26. package/client/src/assets/logo_small.png +0 -0
  27. package/client/src/assets/vibe80_dark.svg +51 -0
  28. package/client/src/assets/vibe80_light.svg +50 -0
  29. package/client/src/components/Chat/ChatComposer.jsx +228 -0
  30. package/client/src/components/Chat/ChatMessages.jsx +811 -0
  31. package/client/src/components/Chat/ChatToolbar.jsx +109 -0
  32. package/client/src/components/Chat/useChatComposer.js +462 -0
  33. package/client/src/components/Diff/DiffPanel.jsx +129 -0
  34. package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
  35. package/client/src/components/Logs/LogsPanel.jsx +80 -0
  36. package/client/src/components/SessionGate/SessionGate.jsx +874 -0
  37. package/client/src/components/Settings/SettingsPanel.jsx +212 -0
  38. package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
  39. package/client/src/components/Topbar/Topbar.jsx +101 -0
  40. package/client/src/components/WorktreeTabs.css +419 -0
  41. package/client/src/components/WorktreeTabs.jsx +604 -0
  42. package/client/src/hooks/useAttachments.jsx +125 -0
  43. package/client/src/hooks/useBacklog.js +254 -0
  44. package/client/src/hooks/useChatClear.js +90 -0
  45. package/client/src/hooks/useChatCollapse.js +42 -0
  46. package/client/src/hooks/useChatCommands.js +294 -0
  47. package/client/src/hooks/useChatExport.js +144 -0
  48. package/client/src/hooks/useChatMessagesState.js +69 -0
  49. package/client/src/hooks/useChatSend.js +158 -0
  50. package/client/src/hooks/useChatSocket.js +1239 -0
  51. package/client/src/hooks/useDiffNavigation.js +19 -0
  52. package/client/src/hooks/useExplorerActions.js +1184 -0
  53. package/client/src/hooks/useGitIdentity.js +114 -0
  54. package/client/src/hooks/useLayoutMode.js +31 -0
  55. package/client/src/hooks/useLocalPreferences.js +131 -0
  56. package/client/src/hooks/useMessageSync.js +30 -0
  57. package/client/src/hooks/useNotifications.js +132 -0
  58. package/client/src/hooks/usePaneNavigation.js +67 -0
  59. package/client/src/hooks/usePanelState.js +13 -0
  60. package/client/src/hooks/useProviderSelection.js +70 -0
  61. package/client/src/hooks/useRepoBranchesModels.js +218 -0
  62. package/client/src/hooks/useRepoStatus.js +350 -0
  63. package/client/src/hooks/useRpcLogActions.js +19 -0
  64. package/client/src/hooks/useRpcLogView.js +58 -0
  65. package/client/src/hooks/useSessionHandoff.js +97 -0
  66. package/client/src/hooks/useSessionLifecycle.js +287 -0
  67. package/client/src/hooks/useSessionReset.js +63 -0
  68. package/client/src/hooks/useSessionResync.js +77 -0
  69. package/client/src/hooks/useTerminalSession.js +328 -0
  70. package/client/src/hooks/useToolbarExport.js +27 -0
  71. package/client/src/hooks/useTurnInterrupt.js +43 -0
  72. package/client/src/hooks/useVibe80Forms.js +128 -0
  73. package/client/src/hooks/useWorkspaceAuth.js +932 -0
  74. package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
  75. package/client/src/hooks/useWorktrees.js +396 -0
  76. package/client/src/i18n.jsx +87 -0
  77. package/client/src/index.css +5147 -0
  78. package/client/src/locales/en.json +37 -0
  79. package/client/src/locales/fr.json +321 -0
  80. package/client/src/main.jsx +16 -0
  81. package/client/vite.config.js +62 -0
  82. package/docs/api/asyncapi.json +1511 -0
  83. package/docs/api/openapi.json +3242 -0
  84. package/git_hooks/prepare-commit-msg +35 -0
  85. package/package.json +36 -0
  86. package/server/package.json +29 -0
  87. package/server/scripts/rotate-workspace-secret.js +101 -0
  88. package/server/src/claudeClient.js +454 -0
  89. package/server/src/clientEvents.js +594 -0
  90. package/server/src/clientFactory.js +164 -0
  91. package/server/src/codexClient.js +468 -0
  92. package/server/src/config.js +27 -0
  93. package/server/src/helpers.js +138 -0
  94. package/server/src/index.js +1641 -0
  95. package/server/src/middleware/auth.js +93 -0
  96. package/server/src/middleware/debug.js +89 -0
  97. package/server/src/middleware/errorTypes.js +60 -0
  98. package/server/src/providerLogger.js +60 -0
  99. package/server/src/routes/files.js +114 -0
  100. package/server/src/routes/git.js +183 -0
  101. package/server/src/routes/health.js +13 -0
  102. package/server/src/routes/sessions.js +407 -0
  103. package/server/src/routes/workspaces.js +296 -0
  104. package/server/src/routes/worktrees.js +993 -0
  105. package/server/src/runAs.js +458 -0
  106. package/server/src/runtimeStore.js +32 -0
  107. package/server/src/services/auth.js +157 -0
  108. package/server/src/services/claudeThreadDirectory.js +33 -0
  109. package/server/src/services/session.js +918 -0
  110. package/server/src/services/workspace.js +858 -0
  111. package/server/src/storage/index.js +17 -0
  112. package/server/src/storage/redis.js +412 -0
  113. package/server/src/storage/sqlite.js +649 -0
  114. package/server/src/worktreeManager.js +717 -0
  115. package/server/tests/README.md +13 -0
  116. package/server/tests/factories/workspaceFactory.js +13 -0
  117. package/server/tests/fixtures/workspaceCredentials.json +4 -0
  118. package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
  119. package/server/tests/setup/env.js +9 -0
  120. package/server/tests/unit/helpers.test.js +95 -0
  121. package/server/tests/unit/services/auth.test.js +181 -0
  122. package/server/tests/unit/services/workspace.test.js +115 -0
  123. package/server/vitest.config.js +23 -0
@@ -0,0 +1,35 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ MSG_FILE="$1"
5
+ COMMIT_SOURCE="${2-}"
6
+ SHA1="${3-}"
7
+
8
+ # Ne pas modifier les messages par défaut lors d'un merge ou d'un squash
9
+ case "$COMMIT_SOURCE" in
10
+ merge|squash) exit 0 ;;
11
+ esac
12
+
13
+ # Récupération des valeurs depuis la config du worktree
14
+ sessionId="$(git config --worktree --get vibe80.sessionId || true)"
15
+ worktreeId="$(git config --worktree --get vibe80.worktreeId || true)"
16
+
17
+ # Flag d’activation (stdout uniquement)
18
+ includeMetadata="$(git config --get vibe80.includeCommitMetadata || true)"
19
+
20
+ # Fonction: ajouter "Key: value" si absent
21
+ add_if_missing() {
22
+ key="$1"
23
+ value="$2"
24
+ # Si déjà présent (même vide), on ne touche pas
25
+ if grep -q "^$key:" "$MSG_FILE"; then
26
+ return 0
27
+ fi
28
+
29
+ printf "\n%s: %s\n" "$key" "$value" >> "$MSG_FILE"
30
+ }
31
+
32
+ # Ajout conditionnel
33
+ if [ "$includeMetadata" = "1" ]; then
34
+ add_if_missing "VIBE80-Tag" "${sessionId}/${worktreeId}"
35
+ fi
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@vibe80/vibe80",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "workspaces": [
6
+ "server",
7
+ "client"
8
+ ],
9
+ "bin": {
10
+ "vibe80": "bin/vibe80.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "git_hooks",
15
+ "server",
16
+ "client",
17
+ "package.json",
18
+ "package-lock.json",
19
+ "README.md",
20
+ "docs"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "dev": "concurrently -n server,client -c blue,green \"npm:dev:server\" \"npm:dev:client\"",
27
+ "dev:server": "npm --workspace server run dev",
28
+ "dev:client": "npm --workspace client run dev",
29
+ "build": "npm --workspace client run build",
30
+ "start": "npm --workspace server run start"
31
+ },
32
+ "dependencies": {
33
+ "concurrently": "^8.2.2"
34
+ },
35
+ "license": "Apache-2.0"
36
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "vibe80-server",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "node --watch src/index.js",
7
+ "start": "node src/index.js",
8
+ "workspace:rotate-secret": "node scripts/rotate-workspace-secret.js",
9
+ "test": "vitest run",
10
+ "test:watch": "vitest",
11
+ "test:coverage": "vitest run --coverage"
12
+ },
13
+ "dependencies": {
14
+ "docker-names": "^1.2.1",
15
+ "express": "^4.19.2",
16
+ "express-rate-limit": "^7.4.0",
17
+ "jsonwebtoken": "^9.0.3",
18
+ "multer": "^1.4.5-lts.1",
19
+ "node-pty": "^1.0.0",
20
+ "redis": "^4.7.1",
21
+ "sqlite3": "^5.1.7",
22
+ "ws": "^8.17.1"
23
+ },
24
+ "devDependencies": {
25
+ "@vitest/coverage-v8": "^4.0.18",
26
+ "supertest": "^7.2.2",
27
+ "vitest": "^4.0.18"
28
+ }
29
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+
3
+ import storage from "../src/storage/index.js";
4
+ import {
5
+ workspaceIdPattern,
6
+ rotateWorkspaceSecret,
7
+ } from "../src/services/workspace.js";
8
+
9
+ const printUsage = () => {
10
+ console.error(
11
+ [
12
+ "Usage:",
13
+ " node server/scripts/rotate-workspace-secret.js --workspace-id <workspaceId> [--workspace-secret <secret>] [--json]",
14
+ "",
15
+ "Requirements:",
16
+ " - SERVER env vars must be set (e.g. STORAGE_BACKEND, DEPLOYMENT_MODE, etc.)",
17
+ ].join("\n")
18
+ );
19
+ };
20
+
21
+ const parseArgs = (argv) => {
22
+ const args = {
23
+ workspaceId: "",
24
+ workspaceSecret: "",
25
+ json: false,
26
+ };
27
+
28
+ for (let i = 0; i < argv.length; i += 1) {
29
+ const token = argv[i];
30
+ if (token === "--workspace-id") {
31
+ args.workspaceId = String(argv[i + 1] || "").trim();
32
+ i += 1;
33
+ continue;
34
+ }
35
+ if (token === "--workspace-secret") {
36
+ args.workspaceSecret = String(argv[i + 1] || "");
37
+ i += 1;
38
+ continue;
39
+ }
40
+ if (token === "--json") {
41
+ args.json = true;
42
+ continue;
43
+ }
44
+ if (token === "-h" || token === "--help") {
45
+ args.help = true;
46
+ continue;
47
+ }
48
+ throw new Error(`Unknown argument: ${token}`);
49
+ }
50
+
51
+ return args;
52
+ };
53
+
54
+ const main = async () => {
55
+ const args = parseArgs(process.argv.slice(2));
56
+ if (args.help) {
57
+ printUsage();
58
+ process.exit(0);
59
+ }
60
+
61
+ if (!args.workspaceId) {
62
+ throw new Error("--workspace-id is required.");
63
+ }
64
+
65
+ if (!workspaceIdPattern.test(args.workspaceId)) {
66
+ throw new Error(`Invalid workspaceId: ${args.workspaceId}`);
67
+ }
68
+
69
+ if (args.workspaceSecret.trim() === "") {
70
+ args.workspaceSecret = "";
71
+ }
72
+
73
+ await storage.init();
74
+ try {
75
+ const result = await rotateWorkspaceSecret(args.workspaceId, {
76
+ workspaceSecret: args.workspaceSecret || undefined,
77
+ actor: "cli",
78
+ });
79
+
80
+ if (args.json) {
81
+ process.stdout.write(`${JSON.stringify(result)}\n`);
82
+ return;
83
+ }
84
+
85
+ process.stdout.write(
86
+ [
87
+ "Workspace secret rotated successfully.",
88
+ `workspaceId=${result.workspaceId}`,
89
+ `workspaceSecret=${result.workspaceSecret}`,
90
+ ].join("\n") + "\n"
91
+ );
92
+ } finally {
93
+ await storage.close();
94
+ }
95
+ };
96
+
97
+ main().catch((error) => {
98
+ console.error(error?.message || error);
99
+ printUsage();
100
+ process.exit(1);
101
+ });
@@ -0,0 +1,454 @@
1
+ import { spawn } from "child_process";
2
+ import { EventEmitter } from "events";
3
+ import crypto from "crypto";
4
+ import path from "path";
5
+ import { SYSTEM_PROMPT } from "./config.js";
6
+ import { createProviderLogger } from "./providerLogger.js";
7
+ import { buildSandboxArgs, getWorkspaceHome, runAsCommand } from "./runAs.js";
8
+
9
+ const RUN_AS_HELPER = process.env.VIBE80_RUN_AS_HELPER || "/usr/local/bin/vibe80-run-as";
10
+ const SUDO_PATH = process.env.VIBE80_SUDO_PATH || "sudo";
11
+ const isMonoUser = process.env.DEPLOYMENT_MODE === "mono_user";
12
+
13
+ const createTurnId = () =>
14
+ typeof crypto.randomUUID === "function"
15
+ ? crypto.randomUUID()
16
+ : crypto.randomBytes(16).toString("hex");
17
+
18
+ export class ClaudeCliClient extends EventEmitter {
19
+ constructor({
20
+ cwd,
21
+ attachmentsDir,
22
+ repoDir,
23
+ internetAccess,
24
+ denyGitCredentialsAccess,
25
+ gitDir,
26
+ env,
27
+ workspaceId,
28
+ tmpDir,
29
+ sessionId,
30
+ worktreeId,
31
+ threadId,
32
+ forkFromThreadId,
33
+ }) {
34
+ super();
35
+ this.cwd = cwd;
36
+ this.attachmentsDir = attachmentsDir;
37
+ this.repoDir = repoDir || cwd;
38
+ this.internetAccess = internetAccess ?? true;
39
+ this.denyGitCredentialsAccess = denyGitCredentialsAccess ?? true;
40
+ if (this.internetAccess === false && this.denyGitCredentialsAccess) {
41
+ throw new Error(
42
+ "Invalid Claude configuration: denyGitCredentialsAccess must be false when internetAccess is false."
43
+ );
44
+ }
45
+ this.gitDir = gitDir || null;
46
+ this.env = env || process.env;
47
+ this.workspaceId = workspaceId;
48
+ this.tmpDir = tmpDir || null;
49
+ this.sessionId = sessionId || null;
50
+ this.worktreeId = worktreeId || "main";
51
+ this.ready = false;
52
+ this.threadId = threadId || null;
53
+ this.pendingForkFromThreadId = forkFromThreadId || null;
54
+ this.modelInfo = null;
55
+ this.defaultModel = null;
56
+ this.toolUses = new Map();
57
+ this.buffer = "";
58
+ this.stdoutLogBuffer = "";
59
+ this.stderrLogBuffer = "";
60
+ this.providerLogger = createProviderLogger({
61
+ provider: "claude",
62
+ sessionId: this.sessionId,
63
+ worktreeId: this.worktreeId,
64
+ });
65
+ this.systemPrompt = SYSTEM_PROMPT;
66
+ this.activeProcess = null;
67
+ }
68
+
69
+ async start() {
70
+ this.ready = true;
71
+ this.emit("ready", { threadId: this.threadId });
72
+ }
73
+
74
+ async stop() {
75
+ const proc = this.activeProcess;
76
+ if (!proc) {
77
+ return;
78
+ }
79
+ this.activeProcess = null;
80
+ this.ready = false;
81
+ const exitPromise = new Promise((resolve) => {
82
+ proc.once("exit", resolve);
83
+ proc.once("close", resolve);
84
+ });
85
+ try {
86
+ proc.kill("SIGTERM");
87
+ } catch {
88
+ return;
89
+ }
90
+ await Promise.race([
91
+ exitPromise,
92
+ new Promise((resolve) => setTimeout(resolve, 5000)),
93
+ ]);
94
+ if (!proc.killed) {
95
+ try {
96
+ proc.kill("SIGKILL");
97
+ } catch {
98
+ return;
99
+ }
100
+ await exitPromise;
101
+ }
102
+ }
103
+
104
+ getStatus() {
105
+ if (this.activeProcess) {
106
+ return "busy";
107
+ }
108
+ if (!this.ready) {
109
+ return "starting";
110
+ }
111
+ return "idle";
112
+ }
113
+
114
+ requestRestart() {
115
+ return;
116
+ }
117
+
118
+ async restart() {
119
+ return;
120
+ }
121
+
122
+ async sendTurn(text) {
123
+ const turnId = createTurnId();
124
+ const workspaceHome = getWorkspaceHome(this.workspaceId);
125
+ const claudeConfigPath = path.join(workspaceHome, ".claude.json");
126
+ try {
127
+ await runAsCommand(
128
+ this.workspaceId,
129
+ "/bin/sh",
130
+ [
131
+ "-c",
132
+ 'if [ ! -f "$1" ]; then printf "{}" > "$1"; fi',
133
+ "sh",
134
+ claudeConfigPath,
135
+ ],
136
+ { cwd: workspaceHome }
137
+ );
138
+ } catch (error) {
139
+ this.emit(
140
+ "log",
141
+ `Unable to ensure Claude config exists: ${error?.message || error}`
142
+ );
143
+ }
144
+ const shareGitCredentials = !this.denyGitCredentialsAccess;
145
+ const sshDir = path.join(getWorkspaceHome(this.workspaceId), ".ssh");
146
+ const allowedDirs = [
147
+ this.cwd,
148
+ this.repoDir,
149
+ this.attachmentsDir,
150
+ shareGitCredentials ? this.gitDir : null,
151
+ shareGitCredentials ? sshDir : null,
152
+ ]
153
+ .filter(Boolean)
154
+ .filter((value, index, self) => self.indexOf(value) === index);
155
+ const allowedTools = ["Bash"];
156
+ if (this.internetAccess) {
157
+ allowedTools.push("WebSearch");
158
+ allowedTools.push("WebFetch");
159
+ }
160
+ const shouldForkSession =
161
+ !this.threadId &&
162
+ typeof this.pendingForkFromThreadId === "string" &&
163
+ this.pendingForkFromThreadId.trim();
164
+ const args = [
165
+ ...(shouldForkSession
166
+ ? ["--fork-session", "--resume", this.pendingForkFromThreadId.trim()]
167
+ : ["--continue"]),
168
+ "--verbose",
169
+ "-p",
170
+ ...(this.defaultModel ? ["--model", this.defaultModel] : []),
171
+ "--output-format",
172
+ "stream-json",
173
+ "--input-format",
174
+ "stream-json",
175
+ "--permission-mode",
176
+ "acceptEdits",
177
+ "--allowed-tools",
178
+ allowedTools.join(" "),
179
+ "--append-system-prompt",
180
+ this.systemPrompt,
181
+ ];
182
+ for (const dir of allowedDirs) {
183
+ args.push("--add-dir", dir);
184
+ }
185
+
186
+ const command = "claude";
187
+ const spawnCommand = isMonoUser ? command : SUDO_PATH;
188
+ const spawnArgs = isMonoUser
189
+ ? args
190
+ : [
191
+ "-n",
192
+ RUN_AS_HELPER,
193
+ "--workspace-id",
194
+ this.workspaceId,
195
+ "--cwd",
196
+ this.cwd,
197
+ ...buildSandboxArgs({
198
+ cwd: this.cwd,
199
+ repoDir: this.repoDir,
200
+ attachmentsDir: this.attachmentsDir,
201
+ tmpDir: this.tmpDir,
202
+ workspaceId: this.workspaceId,
203
+ internetAccess: this.internetAccess,
204
+ netMode: "tcp:22,53,443",
205
+ extraAllowRw: [
206
+ path.join(getWorkspaceHome(this.workspaceId), ".claude"),
207
+ ...(shareGitCredentials
208
+ ? [sshDir, ...(this.gitDir ? [this.gitDir] : [])]
209
+ : []),
210
+ ],
211
+ extraAllowRwFiles: [
212
+ path.join(getWorkspaceHome(this.workspaceId), ".claude.json"),
213
+ ],
214
+ }),
215
+ "--",
216
+ command,
217
+ ...args,
218
+ ];
219
+
220
+ const proc = spawn(spawnCommand, spawnArgs, {
221
+ stdio: ["pipe", "pipe", "pipe"],
222
+ env: this.env,
223
+ cwd: isMonoUser ? this.cwd : undefined,
224
+ });
225
+ this.activeProcess = proc;
226
+
227
+ proc.stdout.setEncoding("utf8");
228
+ proc.stdout.on("data", (chunk) => {
229
+ this.#logStreamChunk("OUT", "stdoutLogBuffer", chunk);
230
+ this.#handleStdout(turnId, chunk);
231
+ });
232
+
233
+ proc.stderr.setEncoding("utf8");
234
+ proc.stderr.on("data", (chunk) => {
235
+ this.#logStreamChunk("ERR", "stderrLogBuffer", chunk);
236
+ const message = chunk.toString().trim();
237
+ if (message) {
238
+ this.emit("log", message);
239
+ }
240
+ });
241
+
242
+ this.emit("turn_started", { turnId, status: "processing" });
243
+
244
+ proc.on("error", (error) => {
245
+ const details = [
246
+ "Claude spawn failed",
247
+ `mode=${isMonoUser ? "mono_user" : "multi_user"}`,
248
+ isMonoUser ? `cmd=${command}` : `sudo=${SUDO_PATH}`,
249
+ isMonoUser ? null : `helper=${RUN_AS_HELPER}`,
250
+ `workspace=${this.workspaceId}`,
251
+ `cwd=${this.cwd}`,
252
+ `error=${error?.message || error}`,
253
+ ]
254
+ .filter(Boolean)
255
+ .join(" ");
256
+ this.emit("log", details);
257
+ this.emit("turn_error", {
258
+ turnId,
259
+ message: details,
260
+ });
261
+ });
262
+
263
+ proc.on("close", (code) => {
264
+ if (this.activeProcess === proc) {
265
+ this.activeProcess = null;
266
+ }
267
+ this.#flushLogBuffer("OUT", "stdoutLogBuffer");
268
+ this.#flushLogBuffer("ERR", "stderrLogBuffer");
269
+ this.providerLogger?.close?.();
270
+ if (code === 0) {
271
+ this.emit("turn_completed", { turnId, status: "success" });
272
+ } else {
273
+ this.emit("turn_error", {
274
+ turnId,
275
+ message: `Claude process exited with code ${code}`,
276
+ });
277
+ }
278
+ });
279
+
280
+ const payload = {
281
+ type: "user",
282
+ message: {
283
+ role: "user",
284
+ content: [{ type: "text", text }],
285
+ },
286
+ };
287
+ const line = JSON.stringify(payload);
288
+ this.providerLogger?.writeLine("IN", line);
289
+ proc.stdin.write(`${line}\n`);
290
+ proc.stdin.end();
291
+
292
+ return { turn: { id: turnId } };
293
+ }
294
+
295
+ async interruptTurn() {
296
+ if (this.activeProcess && !this.activeProcess.killed) {
297
+ this.activeProcess.kill("SIGTERM");
298
+ return;
299
+ }
300
+ throw new Error("Claude CLI does not have an active process to interrupt.");
301
+ }
302
+
303
+ async listModels() {
304
+ const model = this.modelInfo?.model || "default";
305
+ return {
306
+ data: [
307
+ {
308
+ id: "default",
309
+ model: "default",
310
+ displayName: "default",
311
+ isDefault: true,
312
+ },
313
+ { id: "sonnet", model: "sonnet", displayName: "sonnet" },
314
+ { id: "opus", model: "opus", displayName: "opus" },
315
+ { id: "haiku", model: "haiku", displayName: "haiku" },
316
+ { id: "opusplan", model: "opusplan", displayName: "opusplan" },
317
+ ],
318
+ nextCursor: null,
319
+ };
320
+ }
321
+
322
+ async setDefaultModel(model) {
323
+ if (typeof model === "string" && model.trim()) {
324
+ this.defaultModel = model.trim();
325
+ this.modelInfo = { model: this.defaultModel };
326
+ return { ok: true };
327
+ }
328
+ this.defaultModel = null;
329
+ return { ok: true };
330
+ }
331
+
332
+ async startAccountLogin() {
333
+ throw new Error("Claude CLI does not support account login.");
334
+ }
335
+
336
+ #handleStdout(turnId, chunk) {
337
+ this.buffer += chunk;
338
+ let newlineIndex;
339
+
340
+ while ((newlineIndex = this.buffer.indexOf("\n")) !== -1) {
341
+ const raw = this.buffer.slice(0, newlineIndex).trim();
342
+ this.buffer = this.buffer.slice(newlineIndex + 1);
343
+ if (!raw) {
344
+ continue;
345
+ }
346
+ let message;
347
+ try {
348
+ message = JSON.parse(raw);
349
+ } catch (error) {
350
+ this.emit("log", `Failed to parse Claude JSON: ${raw}`);
351
+ continue;
352
+ }
353
+ this.emit("stdout_json", { turnId, message });
354
+ this.#handleMessage(turnId, message);
355
+ }
356
+ }
357
+
358
+ #handleMessage(turnId, message) {
359
+ if (message?.type === "system" && message.subtype === "init") {
360
+ this.modelInfo = { model: message.model || null };
361
+ if (typeof message.session_id === "string" && message.session_id.trim()) {
362
+ const nextThreadId = message.session_id.trim();
363
+ this.pendingForkFromThreadId = null;
364
+ if (this.threadId !== nextThreadId) {
365
+ this.threadId = nextThreadId;
366
+ this.emit("ready", { threadId: this.threadId });
367
+ }
368
+ }
369
+ return;
370
+ }
371
+
372
+ if (message?.type === "assistant" && message.message?.content) {
373
+ const { content, id } = message.message;
374
+ let textBlocks = [];
375
+ for (const part of content) {
376
+ if (part?.type === "text" && typeof part.text === "string") {
377
+ textBlocks.push(part.text);
378
+ }
379
+ if (part?.type === "tool_use") {
380
+ this.toolUses.set(part.id, { name: part.name });
381
+ }
382
+ }
383
+ const text = textBlocks.join("");
384
+ if (text) {
385
+ this.emit("assistant_message", {
386
+ id: id || createTurnId(),
387
+ text,
388
+ turnId,
389
+ });
390
+ }
391
+ return;
392
+ }
393
+
394
+ if (message?.type === "user" && message.message?.content) {
395
+ for (const part of message.message.content) {
396
+ if (part?.type !== "tool_result") {
397
+ continue;
398
+ }
399
+ const tool = this.toolUses.get(part.tool_use_id) || {};
400
+ const output = typeof part.content === "string" ? part.content : "";
401
+ this.emit("command_execution_completed", {
402
+ item: {
403
+ id: part.tool_use_id,
404
+ type: "commandExecution",
405
+ command: tool.name || "Tool",
406
+ aggregatedOutput: output,
407
+ status: part.is_error ? "error" : "completed",
408
+ },
409
+ itemId: part.tool_use_id,
410
+ turnId,
411
+ });
412
+ this.toolUses.delete(part.tool_use_id);
413
+ }
414
+ return;
415
+ }
416
+
417
+ if (message?.type === "result") {
418
+ if (message.is_error) {
419
+ this.emit("turn_error", {
420
+ turnId,
421
+ message: message.result || "Claude returned an error.",
422
+ });
423
+ }
424
+ }
425
+ }
426
+
427
+ #logStreamChunk(prefix, bufferKey, chunk) {
428
+ if (!this.providerLogger) {
429
+ return;
430
+ }
431
+ const text = chunk == null ? "" : String(chunk);
432
+ this[bufferKey] += text;
433
+ let newlineIndex;
434
+ while ((newlineIndex = this[bufferKey].indexOf("\n")) !== -1) {
435
+ let line = this[bufferKey].slice(0, newlineIndex);
436
+ if (line.endsWith("\r")) {
437
+ line = line.slice(0, -1);
438
+ }
439
+ this.providerLogger.writeLine(prefix, line);
440
+ this[bufferKey] = this[bufferKey].slice(newlineIndex + 1);
441
+ }
442
+ }
443
+
444
+ #flushLogBuffer(prefix, bufferKey) {
445
+ if (!this.providerLogger) {
446
+ return;
447
+ }
448
+ const leftover = this[bufferKey];
449
+ if (leftover) {
450
+ this.providerLogger.writeLine(prefix, leftover);
451
+ this[bufferKey] = "";
452
+ }
453
+ }
454
+ }