@sna-sdk/core 0.9.9 → 0.9.10
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/dist/core/providers/claude-code.d.ts +32 -1
- package/dist/core/providers/claude-code.js +72 -22
- package/dist/electron/index.cjs +494 -13
- package/dist/electron/index.d.ts +2 -0
- package/dist/electron/index.js +6 -1
- package/dist/node/index.cjs +409 -9
- package/dist/node/index.d.ts +2 -0
- package/dist/server/standalone.js +67 -21
- package/package.json +1 -1
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import { AgentProvider, SpawnOptions, AgentProcess } from './types.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Parse `command -v claude` output to extract the executable path.
|
|
5
|
+
* Handles: direct paths, alias with/without quotes, bare command names.
|
|
6
|
+
* @internal Exported for testing only.
|
|
7
|
+
*/
|
|
8
|
+
declare function parseCommandVOutput(raw: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Validate a Claude CLI path by running `<path> --version`.
|
|
11
|
+
* Adds the binary's directory to PATH so shebang resolution works (nvm/fnm).
|
|
12
|
+
*/
|
|
13
|
+
declare function validateClaudePath(claudePath: string): {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
version?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Save a validated Claude path to cache for faster startup next time.
|
|
19
|
+
*/
|
|
20
|
+
declare function cacheClaudePath(claudePath: string, cacheDir?: string): void;
|
|
21
|
+
interface ResolveResult {
|
|
22
|
+
path: string;
|
|
23
|
+
version?: string;
|
|
24
|
+
source: "env" | "cache" | "static" | "shell" | "fallback";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve Claude CLI path. Tries: env override → cache → static paths → shell detection.
|
|
28
|
+
* All candidates are validated with `--version` before returning.
|
|
29
|
+
* Consumer apps should call this and handle the `fallback` source (= not found).
|
|
30
|
+
*/
|
|
31
|
+
declare function resolveClaudeCli(opts?: {
|
|
32
|
+
cacheDir?: string;
|
|
33
|
+
}): ResolveResult;
|
|
3
34
|
declare class ClaudeCodeProvider implements AgentProvider {
|
|
4
35
|
readonly name = "claude-code";
|
|
5
36
|
isAvailable(): Promise<boolean>;
|
|
6
37
|
spawn(options: SpawnOptions): AgentProcess;
|
|
7
38
|
}
|
|
8
39
|
|
|
9
|
-
export { ClaudeCodeProvider };
|
|
40
|
+
export { ClaudeCodeProvider, type ResolveResult, cacheClaudePath, parseCommandVOutput, resolveClaudeCli, validateClaudePath };
|
|
@@ -6,38 +6,80 @@ import { fileURLToPath } from "url";
|
|
|
6
6
|
import { writeHistoryJsonl, buildRecalledConversation } from "./cc-history-adapter.js";
|
|
7
7
|
import { logger } from "../../lib/logger.js";
|
|
8
8
|
const SHELL = process.env.SHELL || "/bin/zsh";
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
function parseCommandVOutput(raw) {
|
|
10
|
+
const trimmed = raw.trim();
|
|
11
|
+
if (!trimmed) return "claude";
|
|
12
|
+
const aliasMatch = trimmed.match(/=\s*['"]?([^'"]+?)['"]?\s*$/);
|
|
13
|
+
if (aliasMatch) return aliasMatch[1];
|
|
14
|
+
const pathMatch = trimmed.match(/^(\/\S+)/m);
|
|
15
|
+
if (pathMatch) return pathMatch[1];
|
|
16
|
+
return trimmed;
|
|
17
|
+
}
|
|
18
|
+
function validateClaudePath(claudePath) {
|
|
19
|
+
try {
|
|
20
|
+
const claudeDir = path.dirname(claudePath);
|
|
21
|
+
const env = { ...process.env, PATH: `${claudeDir}:${process.env.PATH ?? ""}` };
|
|
22
|
+
const out = execSync(`"${claudePath}" --version`, { encoding: "utf8", stdio: "pipe", timeout: 1e4, env }).trim();
|
|
23
|
+
return { ok: true, version: out.split("\n")[0].slice(0, 30) };
|
|
24
|
+
} catch {
|
|
25
|
+
return { ok: false };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function cacheClaudePath(claudePath, cacheDir) {
|
|
29
|
+
const dir = cacheDir ?? path.join(process.cwd(), ".sna");
|
|
30
|
+
try {
|
|
31
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
fs.writeFileSync(path.join(dir, "claude-path"), claudePath);
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function resolveClaudeCli(opts) {
|
|
37
|
+
const cacheDir = opts?.cacheDir;
|
|
38
|
+
if (process.env.SNA_CLAUDE_COMMAND) {
|
|
39
|
+
const v = validateClaudePath(process.env.SNA_CLAUDE_COMMAND);
|
|
40
|
+
return { path: process.env.SNA_CLAUDE_COMMAND, version: v.version, source: "env" };
|
|
41
|
+
}
|
|
42
|
+
const cacheFile = cacheDir ? path.join(cacheDir, "claude-path") : path.join(process.cwd(), ".sna/claude-path");
|
|
43
|
+
try {
|
|
44
|
+
const cached = fs.readFileSync(cacheFile, "utf8").trim();
|
|
45
|
+
if (cached) {
|
|
46
|
+
const v = validateClaudePath(cached);
|
|
47
|
+
if (v.ok) return { path: cached, version: v.version, source: "cache" };
|
|
20
48
|
}
|
|
49
|
+
} catch {
|
|
21
50
|
}
|
|
22
|
-
|
|
51
|
+
const staticPaths = [
|
|
23
52
|
"/opt/homebrew/bin/claude",
|
|
24
53
|
"/usr/local/bin/claude",
|
|
25
54
|
`${process.env.HOME}/.local/bin/claude`,
|
|
26
|
-
`${process.env.HOME}/.claude/bin/claude
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
`${process.env.HOME}/.claude/bin/claude`,
|
|
56
|
+
`${process.env.HOME}/.volta/bin/claude`
|
|
57
|
+
];
|
|
58
|
+
for (const p of staticPaths) {
|
|
59
|
+
const v = validateClaudePath(p);
|
|
60
|
+
if (v.ok) {
|
|
61
|
+
cacheClaudePath(p, cacheDir);
|
|
62
|
+
return { path: p, version: v.version, source: "static" };
|
|
32
63
|
}
|
|
33
64
|
}
|
|
34
65
|
try {
|
|
35
66
|
const raw = execSync(`${SHELL} -i -l -c "command -v claude" 2>/dev/null`, { encoding: "utf8", timeout: 5e3 }).trim();
|
|
36
|
-
const
|
|
37
|
-
|
|
67
|
+
const resolved = parseCommandVOutput(raw);
|
|
68
|
+
if (resolved && resolved !== "claude") {
|
|
69
|
+
const v = validateClaudePath(resolved);
|
|
70
|
+
if (v.ok) {
|
|
71
|
+
cacheClaudePath(resolved, cacheDir);
|
|
72
|
+
return { path: resolved, version: v.version, source: "shell" };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
38
75
|
} catch {
|
|
39
|
-
return "claude";
|
|
40
76
|
}
|
|
77
|
+
return { path: "claude", source: "fallback" };
|
|
78
|
+
}
|
|
79
|
+
function resolveClaudePath(cwd) {
|
|
80
|
+
const result = resolveClaudeCli({ cacheDir: path.join(cwd, ".sna") });
|
|
81
|
+
logger.log("agent", `claude path: ${result.source}=${result.path}${result.version ? ` (${result.version})` : ""}`);
|
|
82
|
+
return result.path;
|
|
41
83
|
}
|
|
42
84
|
const _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
43
85
|
constructor(proc, options) {
|
|
@@ -473,6 +515,10 @@ class ClaudeCodeProvider {
|
|
|
473
515
|
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
474
516
|
delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
|
|
475
517
|
delete cleanEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
|
518
|
+
const claudeDir = path.dirname(claudePath);
|
|
519
|
+
if (claudeDir && claudeDir !== ".") {
|
|
520
|
+
cleanEnv.PATH = `${claudeDir}:${cleanEnv.PATH ?? ""}`;
|
|
521
|
+
}
|
|
476
522
|
const proc = spawn(claudePath, [...claudePrefix, ...args], {
|
|
477
523
|
cwd: options.cwd,
|
|
478
524
|
env: cleanEnv,
|
|
@@ -483,5 +529,9 @@ class ClaudeCodeProvider {
|
|
|
483
529
|
}
|
|
484
530
|
}
|
|
485
531
|
export {
|
|
486
|
-
ClaudeCodeProvider
|
|
532
|
+
ClaudeCodeProvider,
|
|
533
|
+
cacheClaudePath,
|
|
534
|
+
parseCommandVOutput,
|
|
535
|
+
resolveClaudeCli,
|
|
536
|
+
validateClaudePath
|
|
487
537
|
};
|
package/dist/electron/index.cjs
CHANGED
|
@@ -30,7 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/electron/index.ts
|
|
31
31
|
var electron_exports = {};
|
|
32
32
|
__export(electron_exports, {
|
|
33
|
-
|
|
33
|
+
cacheClaudePath: () => cacheClaudePath,
|
|
34
|
+
parseCommandVOutput: () => parseCommandVOutput,
|
|
35
|
+
resolveClaudeCli: () => resolveClaudeCli,
|
|
36
|
+
startSnaServer: () => startSnaServer,
|
|
37
|
+
validateClaudePath: () => validateClaudePath
|
|
34
38
|
});
|
|
35
39
|
module.exports = __toCommonJS(electron_exports);
|
|
36
40
|
|
|
@@ -39,17 +43,490 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
39
43
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
40
44
|
|
|
41
45
|
// src/electron/index.ts
|
|
42
|
-
var
|
|
46
|
+
var import_child_process2 = require("child_process");
|
|
43
47
|
var import_url = require("url");
|
|
48
|
+
var import_fs3 = __toESM(require("fs"), 1);
|
|
49
|
+
|
|
50
|
+
// src/core/providers/claude-code.ts
|
|
51
|
+
var import_child_process = require("child_process");
|
|
52
|
+
var import_events = require("events");
|
|
53
|
+
var import_fs2 = __toESM(require("fs"), 1);
|
|
54
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
55
|
+
|
|
56
|
+
// src/core/providers/cc-history-adapter.ts
|
|
57
|
+
function buildRecalledConversation(history) {
|
|
58
|
+
const xml = history.map((msg) => `<${msg.role}>${msg.content}</${msg.role}>`).join("\n");
|
|
59
|
+
return JSON.stringify({
|
|
60
|
+
type: "assistant",
|
|
61
|
+
message: {
|
|
62
|
+
role: "assistant",
|
|
63
|
+
content: [{ type: "text", text: `<recalled-conversation>
|
|
64
|
+
${xml}
|
|
65
|
+
</recalled-conversation>` }]
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/lib/logger.ts
|
|
44
71
|
var import_fs = __toESM(require("fs"), 1);
|
|
45
72
|
var import_path = __toESM(require("path"), 1);
|
|
73
|
+
var LOG_PATH = import_path.default.join(process.cwd(), ".dev.log");
|
|
74
|
+
try {
|
|
75
|
+
import_fs.default.writeFileSync(LOG_PATH, "");
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
function ts() {
|
|
79
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
80
|
+
}
|
|
81
|
+
var tags = {
|
|
82
|
+
sna: " SNA ",
|
|
83
|
+
req: " REQ ",
|
|
84
|
+
agent: " AGT ",
|
|
85
|
+
stdin: " IN ",
|
|
86
|
+
stdout: " OUT ",
|
|
87
|
+
route: " API ",
|
|
88
|
+
ws: " WS ",
|
|
89
|
+
err: " ERR "
|
|
90
|
+
};
|
|
91
|
+
function appendFile(tag, args) {
|
|
92
|
+
const line = `${ts()} ${tag} ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
93
|
+
`;
|
|
94
|
+
import_fs.default.appendFile(LOG_PATH, line, () => {
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function log(tag, ...args) {
|
|
98
|
+
console.log(`${ts()} ${tags[tag] ?? tag}`, ...args);
|
|
99
|
+
appendFile(tags[tag] ?? tag, args);
|
|
100
|
+
}
|
|
101
|
+
function err(tag, ...args) {
|
|
102
|
+
console.error(`${ts()} ${tags[tag] ?? tag}`, ...args);
|
|
103
|
+
appendFile(tags[tag] ?? tag, args);
|
|
104
|
+
}
|
|
105
|
+
var logger = { log, err };
|
|
106
|
+
|
|
107
|
+
// src/core/providers/claude-code.ts
|
|
108
|
+
var SHELL = process.env.SHELL || "/bin/zsh";
|
|
109
|
+
function parseCommandVOutput(raw) {
|
|
110
|
+
const trimmed = raw.trim();
|
|
111
|
+
if (!trimmed) return "claude";
|
|
112
|
+
const aliasMatch = trimmed.match(/=\s*['"]?([^'"]+?)['"]?\s*$/);
|
|
113
|
+
if (aliasMatch) return aliasMatch[1];
|
|
114
|
+
const pathMatch = trimmed.match(/^(\/\S+)/m);
|
|
115
|
+
if (pathMatch) return pathMatch[1];
|
|
116
|
+
return trimmed;
|
|
117
|
+
}
|
|
118
|
+
function validateClaudePath(claudePath) {
|
|
119
|
+
try {
|
|
120
|
+
const claudeDir = import_path2.default.dirname(claudePath);
|
|
121
|
+
const env = { ...process.env, PATH: `${claudeDir}:${process.env.PATH ?? ""}` };
|
|
122
|
+
const out = (0, import_child_process.execSync)(`"${claudePath}" --version`, { encoding: "utf8", stdio: "pipe", timeout: 1e4, env }).trim();
|
|
123
|
+
return { ok: true, version: out.split("\n")[0].slice(0, 30) };
|
|
124
|
+
} catch {
|
|
125
|
+
return { ok: false };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function cacheClaudePath(claudePath, cacheDir) {
|
|
129
|
+
const dir = cacheDir ?? import_path2.default.join(process.cwd(), ".sna");
|
|
130
|
+
try {
|
|
131
|
+
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
132
|
+
import_fs2.default.writeFileSync(import_path2.default.join(dir, "claude-path"), claudePath);
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function resolveClaudeCli(opts) {
|
|
137
|
+
const cacheDir = opts?.cacheDir;
|
|
138
|
+
if (process.env.SNA_CLAUDE_COMMAND) {
|
|
139
|
+
const v = validateClaudePath(process.env.SNA_CLAUDE_COMMAND);
|
|
140
|
+
return { path: process.env.SNA_CLAUDE_COMMAND, version: v.version, source: "env" };
|
|
141
|
+
}
|
|
142
|
+
const cacheFile = cacheDir ? import_path2.default.join(cacheDir, "claude-path") : import_path2.default.join(process.cwd(), ".sna/claude-path");
|
|
143
|
+
try {
|
|
144
|
+
const cached = import_fs2.default.readFileSync(cacheFile, "utf8").trim();
|
|
145
|
+
if (cached) {
|
|
146
|
+
const v = validateClaudePath(cached);
|
|
147
|
+
if (v.ok) return { path: cached, version: v.version, source: "cache" };
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
const staticPaths = [
|
|
152
|
+
"/opt/homebrew/bin/claude",
|
|
153
|
+
"/usr/local/bin/claude",
|
|
154
|
+
`${process.env.HOME}/.local/bin/claude`,
|
|
155
|
+
`${process.env.HOME}/.claude/bin/claude`,
|
|
156
|
+
`${process.env.HOME}/.volta/bin/claude`
|
|
157
|
+
];
|
|
158
|
+
for (const p of staticPaths) {
|
|
159
|
+
const v = validateClaudePath(p);
|
|
160
|
+
if (v.ok) {
|
|
161
|
+
cacheClaudePath(p, cacheDir);
|
|
162
|
+
return { path: p, version: v.version, source: "static" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const raw = (0, import_child_process.execSync)(`${SHELL} -i -l -c "command -v claude" 2>/dev/null`, { encoding: "utf8", timeout: 5e3 }).trim();
|
|
167
|
+
const resolved = parseCommandVOutput(raw);
|
|
168
|
+
if (resolved && resolved !== "claude") {
|
|
169
|
+
const v = validateClaudePath(resolved);
|
|
170
|
+
if (v.ok) {
|
|
171
|
+
cacheClaudePath(resolved, cacheDir);
|
|
172
|
+
return { path: resolved, version: v.version, source: "shell" };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
return { path: "claude", source: "fallback" };
|
|
178
|
+
}
|
|
179
|
+
var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
180
|
+
constructor(proc, options) {
|
|
181
|
+
this.emitter = new import_events.EventEmitter();
|
|
182
|
+
this._alive = true;
|
|
183
|
+
this._sessionId = null;
|
|
184
|
+
this._initEmitted = false;
|
|
185
|
+
this.buffer = "";
|
|
186
|
+
/** True once we receive a real text_delta stream_event this turn */
|
|
187
|
+
this._receivedStreamEvents = false;
|
|
188
|
+
/** tool_use IDs already emitted via stream_event (to update instead of re-create in assistant block) */
|
|
189
|
+
this._streamedToolUseIds = /* @__PURE__ */ new Set();
|
|
190
|
+
/**
|
|
191
|
+
* FIFO event queue — ALL events (deltas, assistant, complete, etc.) go through
|
|
192
|
+
* this queue. A fixed-interval timer drains one item at a time, guaranteeing
|
|
193
|
+
* strict ordering: deltas → assistant → complete, never out of order.
|
|
194
|
+
*/
|
|
195
|
+
this.eventQueue = [];
|
|
196
|
+
this.drainTimer = null;
|
|
197
|
+
this.proc = proc;
|
|
198
|
+
proc.stdout.on("data", (chunk) => {
|
|
199
|
+
this.buffer += chunk.toString();
|
|
200
|
+
const lines = this.buffer.split("\n");
|
|
201
|
+
this.buffer = lines.pop() ?? "";
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
if (!line.trim()) continue;
|
|
204
|
+
logger.log("stdout", line);
|
|
205
|
+
try {
|
|
206
|
+
const msg = JSON.parse(line);
|
|
207
|
+
if (msg.session_id && !this._sessionId) {
|
|
208
|
+
this._sessionId = msg.session_id;
|
|
209
|
+
}
|
|
210
|
+
const event = this.normalizeEvent(msg);
|
|
211
|
+
if (event) this.enqueue(event);
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
proc.stderr.on("data", () => {
|
|
217
|
+
});
|
|
218
|
+
proc.on("exit", (code) => {
|
|
219
|
+
this._alive = false;
|
|
220
|
+
if (this.buffer.trim()) {
|
|
221
|
+
try {
|
|
222
|
+
const msg = JSON.parse(this.buffer);
|
|
223
|
+
const event = this.normalizeEvent(msg);
|
|
224
|
+
if (event) this.enqueue(event);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this.flushQueue();
|
|
229
|
+
this.emitter.emit("exit", code);
|
|
230
|
+
logger.log("agent", `process exited (code=${code})`);
|
|
231
|
+
});
|
|
232
|
+
proc.on("error", (err2) => {
|
|
233
|
+
this._alive = false;
|
|
234
|
+
this.emitter.emit("error", err2);
|
|
235
|
+
});
|
|
236
|
+
if (options.history?.length && !options._historyViaResume) {
|
|
237
|
+
const line = buildRecalledConversation(options.history);
|
|
238
|
+
this.proc.stdin.write(line + "\n");
|
|
239
|
+
}
|
|
240
|
+
if (options.prompt) {
|
|
241
|
+
this.send(options.prompt);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ~67 events/sec
|
|
245
|
+
/**
|
|
246
|
+
* Enqueue an event for ordered emission.
|
|
247
|
+
* Starts the drain timer if not already running.
|
|
248
|
+
*/
|
|
249
|
+
enqueue(event) {
|
|
250
|
+
this.eventQueue.push(event);
|
|
251
|
+
if (!this.drainTimer) {
|
|
252
|
+
this.drainTimer = setInterval(() => this.drainOne(), _ClaudeCodeProcess.DRAIN_INTERVAL_MS);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Emit one event from the front of the queue. Stop timer when empty. */
|
|
256
|
+
drainOne() {
|
|
257
|
+
const event = this.eventQueue.shift();
|
|
258
|
+
if (event) {
|
|
259
|
+
this.emitter.emit("event", event);
|
|
260
|
+
}
|
|
261
|
+
if (this.eventQueue.length === 0 && this.drainTimer) {
|
|
262
|
+
clearInterval(this.drainTimer);
|
|
263
|
+
this.drainTimer = null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Flush all remaining queued events immediately (used on process exit). */
|
|
267
|
+
flushQueue() {
|
|
268
|
+
if (this.drainTimer) {
|
|
269
|
+
clearInterval(this.drainTimer);
|
|
270
|
+
this.drainTimer = null;
|
|
271
|
+
}
|
|
272
|
+
while (this.eventQueue.length > 0) {
|
|
273
|
+
this.emitter.emit("event", this.eventQueue.shift());
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Split completed assistant text into delta chunks and enqueue them,
|
|
278
|
+
* followed by the final assistant event. All go through the FIFO queue
|
|
279
|
+
* so subsequent events (complete, etc.) are guaranteed to come after.
|
|
280
|
+
*/
|
|
281
|
+
enqueueTextAsDeltas(text) {
|
|
282
|
+
const CHUNK_SIZE = 4;
|
|
283
|
+
for (let i = 0; i < text.length; i += CHUNK_SIZE) {
|
|
284
|
+
this.enqueue({
|
|
285
|
+
type: "assistant_delta",
|
|
286
|
+
delta: text.slice(i, i + CHUNK_SIZE),
|
|
287
|
+
index: 0,
|
|
288
|
+
timestamp: Date.now()
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
this.enqueue({
|
|
292
|
+
type: "assistant",
|
|
293
|
+
message: text,
|
|
294
|
+
timestamp: Date.now()
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
get alive() {
|
|
298
|
+
return this._alive;
|
|
299
|
+
}
|
|
300
|
+
get pid() {
|
|
301
|
+
return this.proc.pid ?? null;
|
|
302
|
+
}
|
|
303
|
+
get sessionId() {
|
|
304
|
+
return this._sessionId;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Send a user message to the persistent Claude process via stdin.
|
|
308
|
+
* Accepts plain string or content block array (text + images).
|
|
309
|
+
*/
|
|
310
|
+
send(input) {
|
|
311
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
312
|
+
const content = typeof input === "string" ? input : input;
|
|
313
|
+
const msg = JSON.stringify({
|
|
314
|
+
type: "user",
|
|
315
|
+
message: { role: "user", content }
|
|
316
|
+
});
|
|
317
|
+
logger.log("stdin", msg.slice(0, 200));
|
|
318
|
+
this.proc.stdin.write(msg + "\n");
|
|
319
|
+
}
|
|
320
|
+
interrupt() {
|
|
321
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
322
|
+
const msg = JSON.stringify({
|
|
323
|
+
type: "control_request",
|
|
324
|
+
request: { subtype: "interrupt" }
|
|
325
|
+
});
|
|
326
|
+
this.proc.stdin.write(msg + "\n");
|
|
327
|
+
}
|
|
328
|
+
setModel(model) {
|
|
329
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
330
|
+
const msg = JSON.stringify({
|
|
331
|
+
type: "control_request",
|
|
332
|
+
request: { subtype: "set_model", model }
|
|
333
|
+
});
|
|
334
|
+
this.proc.stdin.write(msg + "\n");
|
|
335
|
+
}
|
|
336
|
+
setPermissionMode(mode) {
|
|
337
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
338
|
+
const msg = JSON.stringify({
|
|
339
|
+
type: "control_request",
|
|
340
|
+
request: { subtype: "set_permission_mode", permission_mode: mode }
|
|
341
|
+
});
|
|
342
|
+
this.proc.stdin.write(msg + "\n");
|
|
343
|
+
}
|
|
344
|
+
kill() {
|
|
345
|
+
if (this._alive) {
|
|
346
|
+
this._alive = false;
|
|
347
|
+
this.proc.kill("SIGTERM");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
on(event, handler) {
|
|
351
|
+
this.emitter.on(event, handler);
|
|
352
|
+
}
|
|
353
|
+
off(event, handler) {
|
|
354
|
+
this.emitter.off(event, handler);
|
|
355
|
+
}
|
|
356
|
+
normalizeEvent(msg) {
|
|
357
|
+
switch (msg.type) {
|
|
358
|
+
case "system": {
|
|
359
|
+
if (msg.subtype === "init") {
|
|
360
|
+
if (this._initEmitted) return null;
|
|
361
|
+
this._initEmitted = true;
|
|
362
|
+
return {
|
|
363
|
+
type: "init",
|
|
364
|
+
message: `Agent ready (${msg.model ?? "unknown"})`,
|
|
365
|
+
data: { sessionId: msg.session_id, model: msg.model },
|
|
366
|
+
timestamp: Date.now()
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
case "stream_event": {
|
|
372
|
+
const inner = msg.event;
|
|
373
|
+
if (!inner) return null;
|
|
374
|
+
if (inner.type === "content_block_start" && inner.content_block?.type === "tool_use") {
|
|
375
|
+
const block = inner.content_block;
|
|
376
|
+
this._receivedStreamEvents = true;
|
|
377
|
+
this._streamedToolUseIds.add(block.id);
|
|
378
|
+
return {
|
|
379
|
+
type: "tool_use",
|
|
380
|
+
message: block.name,
|
|
381
|
+
data: { toolName: block.name, id: block.id, input: null, streaming: true },
|
|
382
|
+
timestamp: Date.now()
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (inner.type === "content_block_delta") {
|
|
386
|
+
const delta = inner.delta;
|
|
387
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
388
|
+
this._receivedStreamEvents = true;
|
|
389
|
+
return {
|
|
390
|
+
type: "assistant_delta",
|
|
391
|
+
delta: delta.text,
|
|
392
|
+
index: inner.index ?? 0,
|
|
393
|
+
timestamp: Date.now()
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
397
|
+
return {
|
|
398
|
+
type: "thinking_delta",
|
|
399
|
+
message: delta.thinking,
|
|
400
|
+
timestamp: Date.now()
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
case "assistant": {
|
|
407
|
+
if (this._receivedStreamEvents && msg.message?.stop_reason === null) return null;
|
|
408
|
+
const content = msg.message?.content;
|
|
409
|
+
if (!Array.isArray(content)) return null;
|
|
410
|
+
const events = [];
|
|
411
|
+
const textBlocks = [];
|
|
412
|
+
for (const block of content) {
|
|
413
|
+
if (block.type === "thinking") {
|
|
414
|
+
events.push({
|
|
415
|
+
type: "thinking",
|
|
416
|
+
message: block.thinking ?? "",
|
|
417
|
+
timestamp: Date.now()
|
|
418
|
+
});
|
|
419
|
+
} else if (block.type === "tool_use") {
|
|
420
|
+
const alreadyStreamed = this._streamedToolUseIds.has(block.id);
|
|
421
|
+
if (alreadyStreamed) this._streamedToolUseIds.delete(block.id);
|
|
422
|
+
events.push({
|
|
423
|
+
type: "tool_use",
|
|
424
|
+
message: block.name,
|
|
425
|
+
data: { toolName: block.name, input: block.input, id: block.id, update: alreadyStreamed },
|
|
426
|
+
timestamp: Date.now()
|
|
427
|
+
});
|
|
428
|
+
} else if (block.type === "text") {
|
|
429
|
+
const text = (block.text ?? "").trim();
|
|
430
|
+
if (text) {
|
|
431
|
+
textBlocks.push(text);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (events.length > 0 || textBlocks.length > 0) {
|
|
436
|
+
for (const e of events) {
|
|
437
|
+
this.enqueue(e);
|
|
438
|
+
}
|
|
439
|
+
for (const text of textBlocks) {
|
|
440
|
+
this.enqueue({ type: "assistant", message: text, timestamp: Date.now() });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
case "user": {
|
|
446
|
+
const userContent = msg.message?.content;
|
|
447
|
+
if (!Array.isArray(userContent)) return null;
|
|
448
|
+
for (const block of userContent) {
|
|
449
|
+
if (block.type === "tool_result") {
|
|
450
|
+
return {
|
|
451
|
+
type: "tool_result",
|
|
452
|
+
message: typeof block.content === "string" ? block.content.slice(0, 300) : JSON.stringify(block.content).slice(0, 300),
|
|
453
|
+
data: { toolUseId: block.tool_use_id, isError: block.is_error },
|
|
454
|
+
timestamp: Date.now()
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
case "result": {
|
|
461
|
+
if (msg.subtype === "success") {
|
|
462
|
+
if (this._receivedStreamEvents && msg.result) {
|
|
463
|
+
this.enqueue({
|
|
464
|
+
type: "assistant",
|
|
465
|
+
message: msg.result,
|
|
466
|
+
timestamp: Date.now()
|
|
467
|
+
});
|
|
468
|
+
this._receivedStreamEvents = false;
|
|
469
|
+
this._streamedToolUseIds.clear();
|
|
470
|
+
}
|
|
471
|
+
const u = msg.usage ?? {};
|
|
472
|
+
const mu = msg.modelUsage ?? {};
|
|
473
|
+
const modelKey = Object.keys(mu)[0] ?? "";
|
|
474
|
+
const modelInfo = mu[modelKey] ?? {};
|
|
475
|
+
return {
|
|
476
|
+
type: "complete",
|
|
477
|
+
message: msg.result ?? "Done",
|
|
478
|
+
data: {
|
|
479
|
+
durationMs: msg.duration_ms,
|
|
480
|
+
costUsd: msg.total_cost_usd,
|
|
481
|
+
// Per-turn: actual context window usage this turn
|
|
482
|
+
inputTokens: u.input_tokens ?? 0,
|
|
483
|
+
outputTokens: u.output_tokens ?? 0,
|
|
484
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
485
|
+
cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
|
|
486
|
+
// Static model info
|
|
487
|
+
contextWindow: modelInfo.contextWindow ?? 0,
|
|
488
|
+
model: modelKey
|
|
489
|
+
},
|
|
490
|
+
timestamp: Date.now()
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if (msg.subtype === "error_during_execution" && msg.is_error === false) {
|
|
494
|
+
return {
|
|
495
|
+
type: "interrupted",
|
|
496
|
+
message: "Turn interrupted by user",
|
|
497
|
+
data: { durationMs: msg.duration_ms, costUsd: msg.total_cost_usd },
|
|
498
|
+
timestamp: Date.now()
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (msg.subtype?.startsWith("error") || msg.is_error) {
|
|
502
|
+
return {
|
|
503
|
+
type: "error",
|
|
504
|
+
message: msg.result ?? msg.error ?? "Unknown error",
|
|
505
|
+
timestamp: Date.now()
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
case "rate_limit_event":
|
|
511
|
+
return null;
|
|
512
|
+
default:
|
|
513
|
+
logger.log("agent", `unhandled event: ${msg.type}`, JSON.stringify(msg).substring(0, 200));
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
_ClaudeCodeProcess.DRAIN_INTERVAL_MS = 15;
|
|
519
|
+
var ClaudeCodeProcess = _ClaudeCodeProcess;
|
|
520
|
+
|
|
521
|
+
// src/electron/index.ts
|
|
522
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
46
523
|
function resolveStandaloneScript() {
|
|
47
524
|
const selfPath = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
48
|
-
let script =
|
|
525
|
+
let script = import_path3.default.resolve(import_path3.default.dirname(selfPath), "../server/standalone.js");
|
|
49
526
|
if (script.includes(".asar") && !script.includes(".asar.unpacked")) {
|
|
50
527
|
script = script.replace(/(\.asar)([/\\])/, ".asar.unpacked$2");
|
|
51
528
|
}
|
|
52
|
-
if (!
|
|
529
|
+
if (!import_fs3.default.existsSync(script)) {
|
|
53
530
|
throw new Error(
|
|
54
531
|
`SNA standalone script not found: ${script}
|
|
55
532
|
Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
@@ -60,14 +537,14 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
|
60
537
|
function buildNodePath() {
|
|
61
538
|
const resourcesPath = process.resourcesPath;
|
|
62
539
|
if (!resourcesPath) return void 0;
|
|
63
|
-
const unpacked =
|
|
64
|
-
if (!
|
|
540
|
+
const unpacked = import_path3.default.join(resourcesPath, "app.asar.unpacked", "node_modules");
|
|
541
|
+
if (!import_fs3.default.existsSync(unpacked)) return void 0;
|
|
65
542
|
const existing = process.env.NODE_PATH;
|
|
66
|
-
return existing ? `${unpacked}${
|
|
543
|
+
return existing ? `${unpacked}${import_path3.default.delimiter}${existing}` : unpacked;
|
|
67
544
|
}
|
|
68
545
|
async function startSnaServer(options) {
|
|
69
546
|
const port = options.port ?? 3099;
|
|
70
|
-
const cwd = options.cwd ??
|
|
547
|
+
const cwd = options.cwd ?? import_path3.default.dirname(options.dbPath);
|
|
71
548
|
const readyTimeout = options.readyTimeout ?? 15e3;
|
|
72
549
|
const { onLog } = options;
|
|
73
550
|
const standaloneScript = resolveStandaloneScript();
|
|
@@ -75,7 +552,7 @@ async function startSnaServer(options) {
|
|
|
75
552
|
let consumerModules;
|
|
76
553
|
try {
|
|
77
554
|
const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
|
|
78
|
-
consumerModules =
|
|
555
|
+
consumerModules = import_path3.default.resolve(bsPkg, "../..");
|
|
79
556
|
} catch {
|
|
80
557
|
}
|
|
81
558
|
const env = {
|
|
@@ -92,7 +569,7 @@ async function startSnaServer(options) {
|
|
|
92
569
|
// Consumer overrides last so they can always win
|
|
93
570
|
...options.env ?? {}
|
|
94
571
|
};
|
|
95
|
-
const proc = (0,
|
|
572
|
+
const proc = (0, import_child_process2.fork)(standaloneScript, [], {
|
|
96
573
|
cwd,
|
|
97
574
|
env,
|
|
98
575
|
stdio: "pipe"
|
|
@@ -132,10 +609,10 @@ async function startSnaServer(options) {
|
|
|
132
609
|
reject(new Error(`SNA server process exited (code=${code ?? "null"}) before becoming ready`));
|
|
133
610
|
}
|
|
134
611
|
});
|
|
135
|
-
proc.on("error", (
|
|
612
|
+
proc.on("error", (err2) => {
|
|
136
613
|
if (!isReady) {
|
|
137
614
|
clearTimeout(timer);
|
|
138
|
-
reject(
|
|
615
|
+
reject(err2);
|
|
139
616
|
}
|
|
140
617
|
});
|
|
141
618
|
});
|
|
@@ -149,5 +626,9 @@ async function startSnaServer(options) {
|
|
|
149
626
|
}
|
|
150
627
|
// Annotate the CommonJS export names for ESM import in node:
|
|
151
628
|
0 && (module.exports = {
|
|
152
|
-
|
|
629
|
+
cacheClaudePath,
|
|
630
|
+
parseCommandVOutput,
|
|
631
|
+
resolveClaudeCli,
|
|
632
|
+
startSnaServer,
|
|
633
|
+
validateClaudePath
|
|
153
634
|
});
|
package/dist/electron/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ChildProcess } from 'child_process';
|
|
2
|
+
export { ResolveResult, cacheClaudePath, parseCommandVOutput, resolveClaudeCli, validateClaudePath } from '../core/providers/claude-code.js';
|
|
3
|
+
import '../core/providers/types.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @sna-sdk/core/electron — Electron launcher API
|
package/dist/electron/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fork } from "child_process";
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
3
|
import fs from "fs";
|
|
4
|
+
import { resolveClaudeCli, validateClaudePath, cacheClaudePath, parseCommandVOutput } from "../core/providers/claude-code.js";
|
|
4
5
|
import path from "path";
|
|
5
6
|
function resolveStandaloneScript() {
|
|
6
7
|
const selfPath = fileURLToPath(import.meta.url);
|
|
@@ -107,5 +108,9 @@ async function startSnaServer(options) {
|
|
|
107
108
|
};
|
|
108
109
|
}
|
|
109
110
|
export {
|
|
110
|
-
|
|
111
|
+
cacheClaudePath,
|
|
112
|
+
parseCommandVOutput,
|
|
113
|
+
resolveClaudeCli,
|
|
114
|
+
startSnaServer,
|
|
115
|
+
validateClaudePath
|
|
111
116
|
};
|
package/dist/node/index.cjs
CHANGED
|
@@ -41,15 +41,415 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
|
41
41
|
// src/electron/index.ts
|
|
42
42
|
var import_child_process = require("child_process");
|
|
43
43
|
var import_url = require("url");
|
|
44
|
+
var import_fs2 = __toESM(require("fs"), 1);
|
|
45
|
+
|
|
46
|
+
// src/core/providers/claude-code.ts
|
|
47
|
+
var import_events = require("events");
|
|
48
|
+
|
|
49
|
+
// src/core/providers/cc-history-adapter.ts
|
|
50
|
+
function buildRecalledConversation(history) {
|
|
51
|
+
const xml = history.map((msg) => `<${msg.role}>${msg.content}</${msg.role}>`).join("\n");
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
type: "assistant",
|
|
54
|
+
message: {
|
|
55
|
+
role: "assistant",
|
|
56
|
+
content: [{ type: "text", text: `<recalled-conversation>
|
|
57
|
+
${xml}
|
|
58
|
+
</recalled-conversation>` }]
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/lib/logger.ts
|
|
44
64
|
var import_fs = __toESM(require("fs"), 1);
|
|
45
65
|
var import_path = __toESM(require("path"), 1);
|
|
66
|
+
var LOG_PATH = import_path.default.join(process.cwd(), ".dev.log");
|
|
67
|
+
try {
|
|
68
|
+
import_fs.default.writeFileSync(LOG_PATH, "");
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
function ts() {
|
|
72
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
73
|
+
}
|
|
74
|
+
var tags = {
|
|
75
|
+
sna: " SNA ",
|
|
76
|
+
req: " REQ ",
|
|
77
|
+
agent: " AGT ",
|
|
78
|
+
stdin: " IN ",
|
|
79
|
+
stdout: " OUT ",
|
|
80
|
+
route: " API ",
|
|
81
|
+
ws: " WS ",
|
|
82
|
+
err: " ERR "
|
|
83
|
+
};
|
|
84
|
+
function appendFile(tag, args) {
|
|
85
|
+
const line = `${ts()} ${tag} ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
86
|
+
`;
|
|
87
|
+
import_fs.default.appendFile(LOG_PATH, line, () => {
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function log(tag, ...args) {
|
|
91
|
+
console.log(`${ts()} ${tags[tag] ?? tag}`, ...args);
|
|
92
|
+
appendFile(tags[tag] ?? tag, args);
|
|
93
|
+
}
|
|
94
|
+
function err(tag, ...args) {
|
|
95
|
+
console.error(`${ts()} ${tags[tag] ?? tag}`, ...args);
|
|
96
|
+
appendFile(tags[tag] ?? tag, args);
|
|
97
|
+
}
|
|
98
|
+
var logger = { log, err };
|
|
99
|
+
|
|
100
|
+
// src/core/providers/claude-code.ts
|
|
101
|
+
var SHELL = process.env.SHELL || "/bin/zsh";
|
|
102
|
+
var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
103
|
+
constructor(proc, options) {
|
|
104
|
+
this.emitter = new import_events.EventEmitter();
|
|
105
|
+
this._alive = true;
|
|
106
|
+
this._sessionId = null;
|
|
107
|
+
this._initEmitted = false;
|
|
108
|
+
this.buffer = "";
|
|
109
|
+
/** True once we receive a real text_delta stream_event this turn */
|
|
110
|
+
this._receivedStreamEvents = false;
|
|
111
|
+
/** tool_use IDs already emitted via stream_event (to update instead of re-create in assistant block) */
|
|
112
|
+
this._streamedToolUseIds = /* @__PURE__ */ new Set();
|
|
113
|
+
/**
|
|
114
|
+
* FIFO event queue — ALL events (deltas, assistant, complete, etc.) go through
|
|
115
|
+
* this queue. A fixed-interval timer drains one item at a time, guaranteeing
|
|
116
|
+
* strict ordering: deltas → assistant → complete, never out of order.
|
|
117
|
+
*/
|
|
118
|
+
this.eventQueue = [];
|
|
119
|
+
this.drainTimer = null;
|
|
120
|
+
this.proc = proc;
|
|
121
|
+
proc.stdout.on("data", (chunk) => {
|
|
122
|
+
this.buffer += chunk.toString();
|
|
123
|
+
const lines = this.buffer.split("\n");
|
|
124
|
+
this.buffer = lines.pop() ?? "";
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
if (!line.trim()) continue;
|
|
127
|
+
logger.log("stdout", line);
|
|
128
|
+
try {
|
|
129
|
+
const msg = JSON.parse(line);
|
|
130
|
+
if (msg.session_id && !this._sessionId) {
|
|
131
|
+
this._sessionId = msg.session_id;
|
|
132
|
+
}
|
|
133
|
+
const event = this.normalizeEvent(msg);
|
|
134
|
+
if (event) this.enqueue(event);
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
proc.stderr.on("data", () => {
|
|
140
|
+
});
|
|
141
|
+
proc.on("exit", (code) => {
|
|
142
|
+
this._alive = false;
|
|
143
|
+
if (this.buffer.trim()) {
|
|
144
|
+
try {
|
|
145
|
+
const msg = JSON.parse(this.buffer);
|
|
146
|
+
const event = this.normalizeEvent(msg);
|
|
147
|
+
if (event) this.enqueue(event);
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this.flushQueue();
|
|
152
|
+
this.emitter.emit("exit", code);
|
|
153
|
+
logger.log("agent", `process exited (code=${code})`);
|
|
154
|
+
});
|
|
155
|
+
proc.on("error", (err2) => {
|
|
156
|
+
this._alive = false;
|
|
157
|
+
this.emitter.emit("error", err2);
|
|
158
|
+
});
|
|
159
|
+
if (options.history?.length && !options._historyViaResume) {
|
|
160
|
+
const line = buildRecalledConversation(options.history);
|
|
161
|
+
this.proc.stdin.write(line + "\n");
|
|
162
|
+
}
|
|
163
|
+
if (options.prompt) {
|
|
164
|
+
this.send(options.prompt);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ~67 events/sec
|
|
168
|
+
/**
|
|
169
|
+
* Enqueue an event for ordered emission.
|
|
170
|
+
* Starts the drain timer if not already running.
|
|
171
|
+
*/
|
|
172
|
+
enqueue(event) {
|
|
173
|
+
this.eventQueue.push(event);
|
|
174
|
+
if (!this.drainTimer) {
|
|
175
|
+
this.drainTimer = setInterval(() => this.drainOne(), _ClaudeCodeProcess.DRAIN_INTERVAL_MS);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Emit one event from the front of the queue. Stop timer when empty. */
|
|
179
|
+
drainOne() {
|
|
180
|
+
const event = this.eventQueue.shift();
|
|
181
|
+
if (event) {
|
|
182
|
+
this.emitter.emit("event", event);
|
|
183
|
+
}
|
|
184
|
+
if (this.eventQueue.length === 0 && this.drainTimer) {
|
|
185
|
+
clearInterval(this.drainTimer);
|
|
186
|
+
this.drainTimer = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Flush all remaining queued events immediately (used on process exit). */
|
|
190
|
+
flushQueue() {
|
|
191
|
+
if (this.drainTimer) {
|
|
192
|
+
clearInterval(this.drainTimer);
|
|
193
|
+
this.drainTimer = null;
|
|
194
|
+
}
|
|
195
|
+
while (this.eventQueue.length > 0) {
|
|
196
|
+
this.emitter.emit("event", this.eventQueue.shift());
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Split completed assistant text into delta chunks and enqueue them,
|
|
201
|
+
* followed by the final assistant event. All go through the FIFO queue
|
|
202
|
+
* so subsequent events (complete, etc.) are guaranteed to come after.
|
|
203
|
+
*/
|
|
204
|
+
enqueueTextAsDeltas(text) {
|
|
205
|
+
const CHUNK_SIZE = 4;
|
|
206
|
+
for (let i = 0; i < text.length; i += CHUNK_SIZE) {
|
|
207
|
+
this.enqueue({
|
|
208
|
+
type: "assistant_delta",
|
|
209
|
+
delta: text.slice(i, i + CHUNK_SIZE),
|
|
210
|
+
index: 0,
|
|
211
|
+
timestamp: Date.now()
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
this.enqueue({
|
|
215
|
+
type: "assistant",
|
|
216
|
+
message: text,
|
|
217
|
+
timestamp: Date.now()
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
get alive() {
|
|
221
|
+
return this._alive;
|
|
222
|
+
}
|
|
223
|
+
get pid() {
|
|
224
|
+
return this.proc.pid ?? null;
|
|
225
|
+
}
|
|
226
|
+
get sessionId() {
|
|
227
|
+
return this._sessionId;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Send a user message to the persistent Claude process via stdin.
|
|
231
|
+
* Accepts plain string or content block array (text + images).
|
|
232
|
+
*/
|
|
233
|
+
send(input) {
|
|
234
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
235
|
+
const content = typeof input === "string" ? input : input;
|
|
236
|
+
const msg = JSON.stringify({
|
|
237
|
+
type: "user",
|
|
238
|
+
message: { role: "user", content }
|
|
239
|
+
});
|
|
240
|
+
logger.log("stdin", msg.slice(0, 200));
|
|
241
|
+
this.proc.stdin.write(msg + "\n");
|
|
242
|
+
}
|
|
243
|
+
interrupt() {
|
|
244
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
245
|
+
const msg = JSON.stringify({
|
|
246
|
+
type: "control_request",
|
|
247
|
+
request: { subtype: "interrupt" }
|
|
248
|
+
});
|
|
249
|
+
this.proc.stdin.write(msg + "\n");
|
|
250
|
+
}
|
|
251
|
+
setModel(model) {
|
|
252
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
253
|
+
const msg = JSON.stringify({
|
|
254
|
+
type: "control_request",
|
|
255
|
+
request: { subtype: "set_model", model }
|
|
256
|
+
});
|
|
257
|
+
this.proc.stdin.write(msg + "\n");
|
|
258
|
+
}
|
|
259
|
+
setPermissionMode(mode) {
|
|
260
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
261
|
+
const msg = JSON.stringify({
|
|
262
|
+
type: "control_request",
|
|
263
|
+
request: { subtype: "set_permission_mode", permission_mode: mode }
|
|
264
|
+
});
|
|
265
|
+
this.proc.stdin.write(msg + "\n");
|
|
266
|
+
}
|
|
267
|
+
kill() {
|
|
268
|
+
if (this._alive) {
|
|
269
|
+
this._alive = false;
|
|
270
|
+
this.proc.kill("SIGTERM");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
on(event, handler) {
|
|
274
|
+
this.emitter.on(event, handler);
|
|
275
|
+
}
|
|
276
|
+
off(event, handler) {
|
|
277
|
+
this.emitter.off(event, handler);
|
|
278
|
+
}
|
|
279
|
+
normalizeEvent(msg) {
|
|
280
|
+
switch (msg.type) {
|
|
281
|
+
case "system": {
|
|
282
|
+
if (msg.subtype === "init") {
|
|
283
|
+
if (this._initEmitted) return null;
|
|
284
|
+
this._initEmitted = true;
|
|
285
|
+
return {
|
|
286
|
+
type: "init",
|
|
287
|
+
message: `Agent ready (${msg.model ?? "unknown"})`,
|
|
288
|
+
data: { sessionId: msg.session_id, model: msg.model },
|
|
289
|
+
timestamp: Date.now()
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
case "stream_event": {
|
|
295
|
+
const inner = msg.event;
|
|
296
|
+
if (!inner) return null;
|
|
297
|
+
if (inner.type === "content_block_start" && inner.content_block?.type === "tool_use") {
|
|
298
|
+
const block = inner.content_block;
|
|
299
|
+
this._receivedStreamEvents = true;
|
|
300
|
+
this._streamedToolUseIds.add(block.id);
|
|
301
|
+
return {
|
|
302
|
+
type: "tool_use",
|
|
303
|
+
message: block.name,
|
|
304
|
+
data: { toolName: block.name, id: block.id, input: null, streaming: true },
|
|
305
|
+
timestamp: Date.now()
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (inner.type === "content_block_delta") {
|
|
309
|
+
const delta = inner.delta;
|
|
310
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
311
|
+
this._receivedStreamEvents = true;
|
|
312
|
+
return {
|
|
313
|
+
type: "assistant_delta",
|
|
314
|
+
delta: delta.text,
|
|
315
|
+
index: inner.index ?? 0,
|
|
316
|
+
timestamp: Date.now()
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
320
|
+
return {
|
|
321
|
+
type: "thinking_delta",
|
|
322
|
+
message: delta.thinking,
|
|
323
|
+
timestamp: Date.now()
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
case "assistant": {
|
|
330
|
+
if (this._receivedStreamEvents && msg.message?.stop_reason === null) return null;
|
|
331
|
+
const content = msg.message?.content;
|
|
332
|
+
if (!Array.isArray(content)) return null;
|
|
333
|
+
const events = [];
|
|
334
|
+
const textBlocks = [];
|
|
335
|
+
for (const block of content) {
|
|
336
|
+
if (block.type === "thinking") {
|
|
337
|
+
events.push({
|
|
338
|
+
type: "thinking",
|
|
339
|
+
message: block.thinking ?? "",
|
|
340
|
+
timestamp: Date.now()
|
|
341
|
+
});
|
|
342
|
+
} else if (block.type === "tool_use") {
|
|
343
|
+
const alreadyStreamed = this._streamedToolUseIds.has(block.id);
|
|
344
|
+
if (alreadyStreamed) this._streamedToolUseIds.delete(block.id);
|
|
345
|
+
events.push({
|
|
346
|
+
type: "tool_use",
|
|
347
|
+
message: block.name,
|
|
348
|
+
data: { toolName: block.name, input: block.input, id: block.id, update: alreadyStreamed },
|
|
349
|
+
timestamp: Date.now()
|
|
350
|
+
});
|
|
351
|
+
} else if (block.type === "text") {
|
|
352
|
+
const text = (block.text ?? "").trim();
|
|
353
|
+
if (text) {
|
|
354
|
+
textBlocks.push(text);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (events.length > 0 || textBlocks.length > 0) {
|
|
359
|
+
for (const e of events) {
|
|
360
|
+
this.enqueue(e);
|
|
361
|
+
}
|
|
362
|
+
for (const text of textBlocks) {
|
|
363
|
+
this.enqueue({ type: "assistant", message: text, timestamp: Date.now() });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
case "user": {
|
|
369
|
+
const userContent = msg.message?.content;
|
|
370
|
+
if (!Array.isArray(userContent)) return null;
|
|
371
|
+
for (const block of userContent) {
|
|
372
|
+
if (block.type === "tool_result") {
|
|
373
|
+
return {
|
|
374
|
+
type: "tool_result",
|
|
375
|
+
message: typeof block.content === "string" ? block.content.slice(0, 300) : JSON.stringify(block.content).slice(0, 300),
|
|
376
|
+
data: { toolUseId: block.tool_use_id, isError: block.is_error },
|
|
377
|
+
timestamp: Date.now()
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
case "result": {
|
|
384
|
+
if (msg.subtype === "success") {
|
|
385
|
+
if (this._receivedStreamEvents && msg.result) {
|
|
386
|
+
this.enqueue({
|
|
387
|
+
type: "assistant",
|
|
388
|
+
message: msg.result,
|
|
389
|
+
timestamp: Date.now()
|
|
390
|
+
});
|
|
391
|
+
this._receivedStreamEvents = false;
|
|
392
|
+
this._streamedToolUseIds.clear();
|
|
393
|
+
}
|
|
394
|
+
const u = msg.usage ?? {};
|
|
395
|
+
const mu = msg.modelUsage ?? {};
|
|
396
|
+
const modelKey = Object.keys(mu)[0] ?? "";
|
|
397
|
+
const modelInfo = mu[modelKey] ?? {};
|
|
398
|
+
return {
|
|
399
|
+
type: "complete",
|
|
400
|
+
message: msg.result ?? "Done",
|
|
401
|
+
data: {
|
|
402
|
+
durationMs: msg.duration_ms,
|
|
403
|
+
costUsd: msg.total_cost_usd,
|
|
404
|
+
// Per-turn: actual context window usage this turn
|
|
405
|
+
inputTokens: u.input_tokens ?? 0,
|
|
406
|
+
outputTokens: u.output_tokens ?? 0,
|
|
407
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
408
|
+
cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
|
|
409
|
+
// Static model info
|
|
410
|
+
contextWindow: modelInfo.contextWindow ?? 0,
|
|
411
|
+
model: modelKey
|
|
412
|
+
},
|
|
413
|
+
timestamp: Date.now()
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (msg.subtype === "error_during_execution" && msg.is_error === false) {
|
|
417
|
+
return {
|
|
418
|
+
type: "interrupted",
|
|
419
|
+
message: "Turn interrupted by user",
|
|
420
|
+
data: { durationMs: msg.duration_ms, costUsd: msg.total_cost_usd },
|
|
421
|
+
timestamp: Date.now()
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (msg.subtype?.startsWith("error") || msg.is_error) {
|
|
425
|
+
return {
|
|
426
|
+
type: "error",
|
|
427
|
+
message: msg.result ?? msg.error ?? "Unknown error",
|
|
428
|
+
timestamp: Date.now()
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
case "rate_limit_event":
|
|
434
|
+
return null;
|
|
435
|
+
default:
|
|
436
|
+
logger.log("agent", `unhandled event: ${msg.type}`, JSON.stringify(msg).substring(0, 200));
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
_ClaudeCodeProcess.DRAIN_INTERVAL_MS = 15;
|
|
442
|
+
var ClaudeCodeProcess = _ClaudeCodeProcess;
|
|
443
|
+
|
|
444
|
+
// src/electron/index.ts
|
|
445
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
46
446
|
function resolveStandaloneScript() {
|
|
47
447
|
const selfPath = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
48
|
-
let script =
|
|
448
|
+
let script = import_path2.default.resolve(import_path2.default.dirname(selfPath), "../server/standalone.js");
|
|
49
449
|
if (script.includes(".asar") && !script.includes(".asar.unpacked")) {
|
|
50
450
|
script = script.replace(/(\.asar)([/\\])/, ".asar.unpacked$2");
|
|
51
451
|
}
|
|
52
|
-
if (!
|
|
452
|
+
if (!import_fs2.default.existsSync(script)) {
|
|
53
453
|
throw new Error(
|
|
54
454
|
`SNA standalone script not found: ${script}
|
|
55
455
|
Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
@@ -60,14 +460,14 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
|
60
460
|
function buildNodePath() {
|
|
61
461
|
const resourcesPath = process.resourcesPath;
|
|
62
462
|
if (!resourcesPath) return void 0;
|
|
63
|
-
const unpacked =
|
|
64
|
-
if (!
|
|
463
|
+
const unpacked = import_path2.default.join(resourcesPath, "app.asar.unpacked", "node_modules");
|
|
464
|
+
if (!import_fs2.default.existsSync(unpacked)) return void 0;
|
|
65
465
|
const existing = process.env.NODE_PATH;
|
|
66
|
-
return existing ? `${unpacked}${
|
|
466
|
+
return existing ? `${unpacked}${import_path2.default.delimiter}${existing}` : unpacked;
|
|
67
467
|
}
|
|
68
468
|
async function startSnaServer(options) {
|
|
69
469
|
const port = options.port ?? 3099;
|
|
70
|
-
const cwd = options.cwd ??
|
|
470
|
+
const cwd = options.cwd ?? import_path2.default.dirname(options.dbPath);
|
|
71
471
|
const readyTimeout = options.readyTimeout ?? 15e3;
|
|
72
472
|
const { onLog } = options;
|
|
73
473
|
const standaloneScript = resolveStandaloneScript();
|
|
@@ -75,7 +475,7 @@ async function startSnaServer(options) {
|
|
|
75
475
|
let consumerModules;
|
|
76
476
|
try {
|
|
77
477
|
const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
|
|
78
|
-
consumerModules =
|
|
478
|
+
consumerModules = import_path2.default.resolve(bsPkg, "../..");
|
|
79
479
|
} catch {
|
|
80
480
|
}
|
|
81
481
|
const env = {
|
|
@@ -132,10 +532,10 @@ async function startSnaServer(options) {
|
|
|
132
532
|
reject(new Error(`SNA server process exited (code=${code ?? "null"}) before becoming ready`));
|
|
133
533
|
}
|
|
134
534
|
});
|
|
135
|
-
proc.on("error", (
|
|
535
|
+
proc.on("error", (err2) => {
|
|
136
536
|
if (!isReady) {
|
|
137
537
|
clearTimeout(timer);
|
|
138
|
-
reject(
|
|
538
|
+
reject(err2);
|
|
139
539
|
}
|
|
140
540
|
});
|
|
141
541
|
});
|
package/dist/node/index.d.ts
CHANGED
|
@@ -397,38 +397,80 @@ var logger = { log, err };
|
|
|
397
397
|
|
|
398
398
|
// src/core/providers/claude-code.ts
|
|
399
399
|
var SHELL = process.env.SHELL || "/bin/zsh";
|
|
400
|
-
function
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
400
|
+
function parseCommandVOutput(raw) {
|
|
401
|
+
const trimmed = raw.trim();
|
|
402
|
+
if (!trimmed) return "claude";
|
|
403
|
+
const aliasMatch = trimmed.match(/=\s*['"]?([^'"]+?)['"]?\s*$/);
|
|
404
|
+
if (aliasMatch) return aliasMatch[1];
|
|
405
|
+
const pathMatch = trimmed.match(/^(\/\S+)/m);
|
|
406
|
+
if (pathMatch) return pathMatch[1];
|
|
407
|
+
return trimmed;
|
|
408
|
+
}
|
|
409
|
+
function validateClaudePath(claudePath) {
|
|
410
|
+
try {
|
|
411
|
+
const claudeDir = path4.dirname(claudePath);
|
|
412
|
+
const env = { ...process.env, PATH: `${claudeDir}:${process.env.PATH ?? ""}` };
|
|
413
|
+
const out = execSync(`"${claudePath}" --version`, { encoding: "utf8", stdio: "pipe", timeout: 1e4, env }).trim();
|
|
414
|
+
return { ok: true, version: out.split("\n")[0].slice(0, 30) };
|
|
415
|
+
} catch {
|
|
416
|
+
return { ok: false };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function cacheClaudePath(claudePath, cacheDir) {
|
|
420
|
+
const dir = cacheDir ?? path4.join(process.cwd(), ".sna");
|
|
421
|
+
try {
|
|
422
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
423
|
+
fs4.writeFileSync(path4.join(dir, "claude-path"), claudePath);
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function resolveClaudeCli(opts) {
|
|
428
|
+
const cacheDir = opts?.cacheDir;
|
|
429
|
+
if (process.env.SNA_CLAUDE_COMMAND) {
|
|
430
|
+
const v = validateClaudePath(process.env.SNA_CLAUDE_COMMAND);
|
|
431
|
+
return { path: process.env.SNA_CLAUDE_COMMAND, version: v.version, source: "env" };
|
|
432
|
+
}
|
|
433
|
+
const cacheFile = cacheDir ? path4.join(cacheDir, "claude-path") : path4.join(process.cwd(), ".sna/claude-path");
|
|
434
|
+
try {
|
|
435
|
+
const cached = fs4.readFileSync(cacheFile, "utf8").trim();
|
|
436
|
+
if (cached) {
|
|
437
|
+
const v = validateClaudePath(cached);
|
|
438
|
+
if (v.ok) return { path: cached, version: v.version, source: "cache" };
|
|
411
439
|
}
|
|
440
|
+
} catch {
|
|
412
441
|
}
|
|
413
|
-
|
|
442
|
+
const staticPaths = [
|
|
414
443
|
"/opt/homebrew/bin/claude",
|
|
415
444
|
"/usr/local/bin/claude",
|
|
416
445
|
`${process.env.HOME}/.local/bin/claude`,
|
|
417
|
-
`${process.env.HOME}/.claude/bin/claude
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
446
|
+
`${process.env.HOME}/.claude/bin/claude`,
|
|
447
|
+
`${process.env.HOME}/.volta/bin/claude`
|
|
448
|
+
];
|
|
449
|
+
for (const p of staticPaths) {
|
|
450
|
+
const v = validateClaudePath(p);
|
|
451
|
+
if (v.ok) {
|
|
452
|
+
cacheClaudePath(p, cacheDir);
|
|
453
|
+
return { path: p, version: v.version, source: "static" };
|
|
423
454
|
}
|
|
424
455
|
}
|
|
425
456
|
try {
|
|
426
457
|
const raw = execSync(`${SHELL} -i -l -c "command -v claude" 2>/dev/null`, { encoding: "utf8", timeout: 5e3 }).trim();
|
|
427
|
-
const
|
|
428
|
-
|
|
458
|
+
const resolved = parseCommandVOutput(raw);
|
|
459
|
+
if (resolved && resolved !== "claude") {
|
|
460
|
+
const v = validateClaudePath(resolved);
|
|
461
|
+
if (v.ok) {
|
|
462
|
+
cacheClaudePath(resolved, cacheDir);
|
|
463
|
+
return { path: resolved, version: v.version, source: "shell" };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
429
466
|
} catch {
|
|
430
|
-
return "claude";
|
|
431
467
|
}
|
|
468
|
+
return { path: "claude", source: "fallback" };
|
|
469
|
+
}
|
|
470
|
+
function resolveClaudePath(cwd) {
|
|
471
|
+
const result = resolveClaudeCli({ cacheDir: path4.join(cwd, ".sna") });
|
|
472
|
+
logger.log("agent", `claude path: ${result.source}=${result.path}${result.version ? ` (${result.version})` : ""}`);
|
|
473
|
+
return result.path;
|
|
432
474
|
}
|
|
433
475
|
var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
434
476
|
constructor(proc, options) {
|
|
@@ -864,6 +906,10 @@ var ClaudeCodeProvider = class {
|
|
|
864
906
|
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
865
907
|
delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
|
|
866
908
|
delete cleanEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
|
909
|
+
const claudeDir = path4.dirname(claudePath);
|
|
910
|
+
if (claudeDir && claudeDir !== ".") {
|
|
911
|
+
cleanEnv.PATH = `${claudeDir}:${cleanEnv.PATH ?? ""}`;
|
|
912
|
+
}
|
|
867
913
|
const proc = spawn2(claudePath, [...claudePrefix, ...args], {
|
|
868
914
|
cwd: options.cwd,
|
|
869
915
|
env: cleanEnv,
|