@sna-sdk/core 0.9.10 → 0.9.12
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/db/schema.js +5 -3
- package/dist/electron/index.cjs +2377 -26
- package/dist/electron/index.d.ts +31 -1
- package/dist/electron/index.js +80 -0
- package/dist/node/index.cjs +338 -18
- package/dist/node/index.d.ts +2 -0
- package/dist/server/standalone.js +5 -3
- package/package.json +1 -1
package/dist/electron/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ChildProcess } from 'child_process';
|
|
2
|
+
import http from 'http';
|
|
2
3
|
export { ResolveResult, cacheClaudePath, parseCommandVOutput, resolveClaudeCli, validateClaudePath } from '../core/providers/claude-code.js';
|
|
4
|
+
import { SessionManager } from '../server/session-manager.js';
|
|
3
5
|
import '../core/providers/types.js';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -102,5 +104,33 @@ interface SnaServerHandle {
|
|
|
102
104
|
* Throws if the server fails to start within `options.readyTimeout`.
|
|
103
105
|
*/
|
|
104
106
|
declare function startSnaServer(options: SnaServerOptions): Promise<SnaServerHandle>;
|
|
107
|
+
interface InProcessSnaServerHandle {
|
|
108
|
+
/** No child process — the server runs in the calling process. */
|
|
109
|
+
process: null;
|
|
110
|
+
/** The port the server is listening on. */
|
|
111
|
+
port: number;
|
|
112
|
+
/** The session manager instance. */
|
|
113
|
+
sessionManager: SessionManager;
|
|
114
|
+
/** The underlying HTTP server. */
|
|
115
|
+
httpServer: http.Server;
|
|
116
|
+
/** Graceful shutdown: kill all sessions and close the HTTP server. */
|
|
117
|
+
stop(): Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Launch the SNA API server **in-process** (no fork).
|
|
121
|
+
*
|
|
122
|
+
* Designed for Electron main processes where fork() causes problems:
|
|
123
|
+
* - asar module resolution failures
|
|
124
|
+
* - PATH / env propagation issues
|
|
125
|
+
* - orphaned child processes on crash
|
|
126
|
+
*
|
|
127
|
+
* The server runs on the same Node.js event loop as the caller. Use `stop()`
|
|
128
|
+
* to tear down cleanly (e.g., in Electron's `before-quit` handler).
|
|
129
|
+
*
|
|
130
|
+
* Unlike fork mode, this does **not** spawn a default agent — the consumer
|
|
131
|
+
* controls session/agent lifecycle via the returned `sessionManager` or via
|
|
132
|
+
* the WebSocket / HTTP API.
|
|
133
|
+
*/
|
|
134
|
+
declare function startSnaServerInProcess(options: SnaServerOptions): Promise<InProcessSnaServerHandle>;
|
|
105
135
|
|
|
106
|
-
export { type SnaServerHandle, type SnaServerOptions, startSnaServer };
|
|
136
|
+
export { type InProcessSnaServerHandle, type SnaServerHandle, type SnaServerOptions, startSnaServer, startSnaServerInProcess };
|
package/dist/electron/index.js
CHANGED
|
@@ -3,6 +3,14 @@ import { fileURLToPath } from "url";
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { resolveClaudeCli, validateClaudePath, cacheClaudePath, parseCommandVOutput } from "../core/providers/claude-code.js";
|
|
5
5
|
import path from "path";
|
|
6
|
+
import { Hono } from "hono";
|
|
7
|
+
import { cors } from "hono/cors";
|
|
8
|
+
import { serve } from "@hono/node-server";
|
|
9
|
+
import { createSnaApp } from "../server/index.js";
|
|
10
|
+
import { SessionManager } from "../server/session-manager.js";
|
|
11
|
+
import { attachWebSocket } from "../server/ws.js";
|
|
12
|
+
import { setConfig, getConfig } from "../config.js";
|
|
13
|
+
import { getDb } from "../db/schema.js";
|
|
6
14
|
function resolveStandaloneScript() {
|
|
7
15
|
const selfPath = fileURLToPath(import.meta.url);
|
|
8
16
|
let script = path.resolve(path.dirname(selfPath), "../server/standalone.js");
|
|
@@ -107,10 +115,82 @@ async function startSnaServer(options) {
|
|
|
107
115
|
}
|
|
108
116
|
};
|
|
109
117
|
}
|
|
118
|
+
async function startSnaServerInProcess(options) {
|
|
119
|
+
const port = options.port ?? 3099;
|
|
120
|
+
const cwd = options.cwd ?? path.dirname(options.dbPath);
|
|
121
|
+
setConfig({
|
|
122
|
+
port,
|
|
123
|
+
dbPath: options.dbPath,
|
|
124
|
+
...options.maxSessions != null ? { maxSessions: options.maxSessions } : {},
|
|
125
|
+
...options.permissionMode ? { defaultPermissionMode: options.permissionMode } : {},
|
|
126
|
+
...options.model ? { model: options.model } : {},
|
|
127
|
+
...options.permissionTimeoutMs != null ? { permissionTimeoutMs: options.permissionTimeoutMs } : {}
|
|
128
|
+
});
|
|
129
|
+
process.env.SNA_PORT = String(port);
|
|
130
|
+
process.env.SNA_DB_PATH = options.dbPath;
|
|
131
|
+
if (options.maxSessions != null) process.env.SNA_MAX_SESSIONS = String(options.maxSessions);
|
|
132
|
+
if (options.permissionMode) process.env.SNA_PERMISSION_MODE = options.permissionMode;
|
|
133
|
+
if (options.model) process.env.SNA_MODEL = options.model;
|
|
134
|
+
if (options.permissionTimeoutMs != null) process.env.SNA_PERMISSION_TIMEOUT_MS = String(options.permissionTimeoutMs);
|
|
135
|
+
if (options.nativeBinding) process.env.SNA_SQLITE_NATIVE_BINDING = options.nativeBinding;
|
|
136
|
+
if (!process.env.SNA_MODULES_PATH) {
|
|
137
|
+
try {
|
|
138
|
+
const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
|
|
139
|
+
process.env.SNA_MODULES_PATH = path.resolve(bsPkg, "../..");
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const originalCwd = process.cwd();
|
|
144
|
+
try {
|
|
145
|
+
process.chdir(cwd);
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
getDb();
|
|
150
|
+
} catch (err) {
|
|
151
|
+
process.chdir(originalCwd);
|
|
152
|
+
throw new Error(`SNA in-process: database init failed: ${err.message}`);
|
|
153
|
+
}
|
|
154
|
+
const config = getConfig();
|
|
155
|
+
const root = new Hono();
|
|
156
|
+
root.use("*", cors({ origin: "*", allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"] }));
|
|
157
|
+
root.onError((err, c) => {
|
|
158
|
+
const pathname = new URL(c.req.url).pathname;
|
|
159
|
+
if (options.onLog) options.onLog(`ERR ${c.req.method} ${pathname} \u2192 ${err.message}`);
|
|
160
|
+
return c.json({ status: "error", message: err.message, stack: err.stack }, 500);
|
|
161
|
+
});
|
|
162
|
+
root.use("*", async (c, next) => {
|
|
163
|
+
const m = c.req.method;
|
|
164
|
+
const pathname = new URL(c.req.url).pathname;
|
|
165
|
+
if (options.onLog) options.onLog(`${m.padEnd(6)} ${pathname}`);
|
|
166
|
+
await next();
|
|
167
|
+
});
|
|
168
|
+
const sessionManager = new SessionManager({ maxSessions: config.maxSessions });
|
|
169
|
+
root.route("/", createSnaApp({ sessionManager }));
|
|
170
|
+
const httpServer = serve({ fetch: root.fetch, port }, () => {
|
|
171
|
+
if (options.onLog) options.onLog(`API server ready \u2192 http://localhost:${port}`);
|
|
172
|
+
if (options.onLog) options.onLog(`WebSocket endpoint \u2192 ws://localhost:${port}/ws`);
|
|
173
|
+
});
|
|
174
|
+
attachWebSocket(httpServer, sessionManager);
|
|
175
|
+
return {
|
|
176
|
+
process: null,
|
|
177
|
+
port,
|
|
178
|
+
sessionManager,
|
|
179
|
+
httpServer,
|
|
180
|
+
async stop() {
|
|
181
|
+
sessionManager.killAll();
|
|
182
|
+
await new Promise((resolve) => {
|
|
183
|
+
httpServer.close(() => resolve());
|
|
184
|
+
setTimeout(() => resolve(), 3e3).unref();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
110
189
|
export {
|
|
111
190
|
cacheClaudePath,
|
|
112
191
|
parseCommandVOutput,
|
|
113
192
|
resolveClaudeCli,
|
|
114
193
|
startSnaServer,
|
|
194
|
+
startSnaServerInProcess,
|
|
115
195
|
validateClaudePath
|
|
116
196
|
};
|
package/dist/node/index.cjs
CHANGED
|
@@ -39,14 +39,72 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
39
39
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
40
40
|
|
|
41
41
|
// src/electron/index.ts
|
|
42
|
-
var
|
|
43
|
-
var
|
|
44
|
-
var
|
|
42
|
+
var import_child_process2 = require("child_process");
|
|
43
|
+
var import_url2 = require("url");
|
|
44
|
+
var import_fs4 = __toESM(require("fs"), 1);
|
|
45
45
|
|
|
46
46
|
// src/core/providers/claude-code.ts
|
|
47
|
+
var import_child_process = require("child_process");
|
|
47
48
|
var import_events = require("events");
|
|
49
|
+
var import_fs3 = __toESM(require("fs"), 1);
|
|
50
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
51
|
+
var import_url = require("url");
|
|
48
52
|
|
|
49
53
|
// src/core/providers/cc-history-adapter.ts
|
|
54
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
55
|
+
var import_path = __toESM(require("path"), 1);
|
|
56
|
+
function writeHistoryJsonl(history, opts) {
|
|
57
|
+
for (let i = 1; i < history.length; i++) {
|
|
58
|
+
if (history[i].role === history[i - 1].role) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`History validation failed: consecutive ${history[i].role} at index ${i - 1} and ${i}. Messages must alternate user\u2194assistant. Merge tool results into text before injecting.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const dir = import_path.default.join(opts.cwd, ".sna", "history");
|
|
66
|
+
import_fs.default.mkdirSync(dir, { recursive: true });
|
|
67
|
+
const sessionId = crypto.randomUUID();
|
|
68
|
+
const filePath = import_path.default.join(dir, `${sessionId}.jsonl`);
|
|
69
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
70
|
+
const lines = [];
|
|
71
|
+
let prevUuid = null;
|
|
72
|
+
for (const msg of history) {
|
|
73
|
+
const uuid = crypto.randomUUID();
|
|
74
|
+
if (msg.role === "user") {
|
|
75
|
+
lines.push(JSON.stringify({
|
|
76
|
+
parentUuid: prevUuid,
|
|
77
|
+
isSidechain: false,
|
|
78
|
+
type: "user",
|
|
79
|
+
uuid,
|
|
80
|
+
timestamp: now,
|
|
81
|
+
cwd: opts.cwd,
|
|
82
|
+
sessionId,
|
|
83
|
+
message: { role: "user", content: msg.content }
|
|
84
|
+
}));
|
|
85
|
+
} else {
|
|
86
|
+
lines.push(JSON.stringify({
|
|
87
|
+
parentUuid: prevUuid,
|
|
88
|
+
isSidechain: false,
|
|
89
|
+
type: "assistant",
|
|
90
|
+
uuid,
|
|
91
|
+
timestamp: now,
|
|
92
|
+
cwd: opts.cwd,
|
|
93
|
+
sessionId,
|
|
94
|
+
message: {
|
|
95
|
+
role: "assistant",
|
|
96
|
+
content: [{ type: "text", text: msg.content }]
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
prevUuid = uuid;
|
|
101
|
+
}
|
|
102
|
+
import_fs.default.writeFileSync(filePath, lines.join("\n") + "\n");
|
|
103
|
+
return { filePath, extraArgs: ["--resume", filePath] };
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
50
108
|
function buildRecalledConversation(history) {
|
|
51
109
|
const xml = history.map((msg) => `<${msg.role}>${msg.content}</${msg.role}>`).join("\n");
|
|
52
110
|
return JSON.stringify({
|
|
@@ -61,11 +119,11 @@ ${xml}
|
|
|
61
119
|
}
|
|
62
120
|
|
|
63
121
|
// src/lib/logger.ts
|
|
64
|
-
var
|
|
65
|
-
var
|
|
66
|
-
var LOG_PATH =
|
|
122
|
+
var import_fs2 = __toESM(require("fs"), 1);
|
|
123
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
124
|
+
var LOG_PATH = import_path2.default.join(process.cwd(), ".dev.log");
|
|
67
125
|
try {
|
|
68
|
-
|
|
126
|
+
import_fs2.default.writeFileSync(LOG_PATH, "");
|
|
69
127
|
} catch {
|
|
70
128
|
}
|
|
71
129
|
function ts() {
|
|
@@ -84,7 +142,7 @@ var tags = {
|
|
|
84
142
|
function appendFile(tag, args) {
|
|
85
143
|
const line = `${ts()} ${tag} ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
86
144
|
`;
|
|
87
|
-
|
|
145
|
+
import_fs2.default.appendFile(LOG_PATH, line, () => {
|
|
88
146
|
});
|
|
89
147
|
}
|
|
90
148
|
function log(tag, ...args) {
|
|
@@ -99,6 +157,81 @@ var logger = { log, err };
|
|
|
99
157
|
|
|
100
158
|
// src/core/providers/claude-code.ts
|
|
101
159
|
var SHELL = process.env.SHELL || "/bin/zsh";
|
|
160
|
+
function parseCommandVOutput(raw) {
|
|
161
|
+
const trimmed = raw.trim();
|
|
162
|
+
if (!trimmed) return "claude";
|
|
163
|
+
const aliasMatch = trimmed.match(/=\s*['"]?([^'"]+?)['"]?\s*$/);
|
|
164
|
+
if (aliasMatch) return aliasMatch[1];
|
|
165
|
+
const pathMatch = trimmed.match(/^(\/\S+)/m);
|
|
166
|
+
if (pathMatch) return pathMatch[1];
|
|
167
|
+
return trimmed;
|
|
168
|
+
}
|
|
169
|
+
function validateClaudePath(claudePath) {
|
|
170
|
+
try {
|
|
171
|
+
const claudeDir = import_path3.default.dirname(claudePath);
|
|
172
|
+
const env = { ...process.env, PATH: `${claudeDir}:${process.env.PATH ?? ""}` };
|
|
173
|
+
const out = (0, import_child_process.execSync)(`"${claudePath}" --version`, { encoding: "utf8", stdio: "pipe", timeout: 1e4, env }).trim();
|
|
174
|
+
return { ok: true, version: out.split("\n")[0].slice(0, 30) };
|
|
175
|
+
} catch {
|
|
176
|
+
return { ok: false };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function cacheClaudePath(claudePath, cacheDir) {
|
|
180
|
+
const dir = cacheDir ?? import_path3.default.join(process.cwd(), ".sna");
|
|
181
|
+
try {
|
|
182
|
+
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
183
|
+
import_fs3.default.writeFileSync(import_path3.default.join(dir, "claude-path"), claudePath);
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function resolveClaudeCli(opts) {
|
|
188
|
+
const cacheDir = opts?.cacheDir;
|
|
189
|
+
if (process.env.SNA_CLAUDE_COMMAND) {
|
|
190
|
+
const v = validateClaudePath(process.env.SNA_CLAUDE_COMMAND);
|
|
191
|
+
return { path: process.env.SNA_CLAUDE_COMMAND, version: v.version, source: "env" };
|
|
192
|
+
}
|
|
193
|
+
const cacheFile = cacheDir ? import_path3.default.join(cacheDir, "claude-path") : import_path3.default.join(process.cwd(), ".sna/claude-path");
|
|
194
|
+
try {
|
|
195
|
+
const cached = import_fs3.default.readFileSync(cacheFile, "utf8").trim();
|
|
196
|
+
if (cached) {
|
|
197
|
+
const v = validateClaudePath(cached);
|
|
198
|
+
if (v.ok) return { path: cached, version: v.version, source: "cache" };
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
const staticPaths = [
|
|
203
|
+
"/opt/homebrew/bin/claude",
|
|
204
|
+
"/usr/local/bin/claude",
|
|
205
|
+
`${process.env.HOME}/.local/bin/claude`,
|
|
206
|
+
`${process.env.HOME}/.claude/bin/claude`,
|
|
207
|
+
`${process.env.HOME}/.volta/bin/claude`
|
|
208
|
+
];
|
|
209
|
+
for (const p of staticPaths) {
|
|
210
|
+
const v = validateClaudePath(p);
|
|
211
|
+
if (v.ok) {
|
|
212
|
+
cacheClaudePath(p, cacheDir);
|
|
213
|
+
return { path: p, version: v.version, source: "static" };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const raw = (0, import_child_process.execSync)(`${SHELL} -i -l -c "command -v claude" 2>/dev/null`, { encoding: "utf8", timeout: 5e3 }).trim();
|
|
218
|
+
const resolved = parseCommandVOutput(raw);
|
|
219
|
+
if (resolved && resolved !== "claude") {
|
|
220
|
+
const v = validateClaudePath(resolved);
|
|
221
|
+
if (v.ok) {
|
|
222
|
+
cacheClaudePath(resolved, cacheDir);
|
|
223
|
+
return { path: resolved, version: v.version, source: "shell" };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
return { path: "claude", source: "fallback" };
|
|
229
|
+
}
|
|
230
|
+
function resolveClaudePath(cwd) {
|
|
231
|
+
const result = resolveClaudeCli({ cacheDir: import_path3.default.join(cwd, ".sna") });
|
|
232
|
+
logger.log("agent", `claude path: ${result.source}=${result.path}${result.version ? ` (${result.version})` : ""}`);
|
|
233
|
+
return result.path;
|
|
234
|
+
}
|
|
102
235
|
var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
103
236
|
constructor(proc, options) {
|
|
104
237
|
this.emitter = new import_events.EventEmitter();
|
|
@@ -440,16 +573,203 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
440
573
|
};
|
|
441
574
|
_ClaudeCodeProcess.DRAIN_INTERVAL_MS = 15;
|
|
442
575
|
var ClaudeCodeProcess = _ClaudeCodeProcess;
|
|
576
|
+
var ClaudeCodeProvider = class {
|
|
577
|
+
constructor() {
|
|
578
|
+
this.name = "claude-code";
|
|
579
|
+
}
|
|
580
|
+
async isAvailable() {
|
|
581
|
+
try {
|
|
582
|
+
const p = resolveClaudePath(process.cwd());
|
|
583
|
+
(0, import_child_process.execSync)(`test -x "${p}"`, { stdio: "pipe" });
|
|
584
|
+
return true;
|
|
585
|
+
} catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
spawn(options) {
|
|
590
|
+
const claudeCommand = resolveClaudePath(options.cwd);
|
|
591
|
+
const claudeParts = claudeCommand.split(/\s+/);
|
|
592
|
+
const claudePath = claudeParts[0];
|
|
593
|
+
const claudePrefix = claudeParts.slice(1);
|
|
594
|
+
let pkgRoot = import_path3.default.dirname((0, import_url.fileURLToPath)(importMetaUrl));
|
|
595
|
+
while (!import_fs3.default.existsSync(import_path3.default.join(pkgRoot, "package.json"))) {
|
|
596
|
+
const parent = import_path3.default.dirname(pkgRoot);
|
|
597
|
+
if (parent === pkgRoot) break;
|
|
598
|
+
pkgRoot = parent;
|
|
599
|
+
}
|
|
600
|
+
const hookScript = import_path3.default.join(pkgRoot, "dist", "scripts", "hook.js");
|
|
601
|
+
const sessionId = options.env?.SNA_SESSION_ID ?? "default";
|
|
602
|
+
const sdkSettings = {};
|
|
603
|
+
if (options.permissionMode !== "bypassPermissions") {
|
|
604
|
+
sdkSettings.hooks = {
|
|
605
|
+
PreToolUse: [{
|
|
606
|
+
matcher: ".*",
|
|
607
|
+
hooks: [{ type: "command", command: `node "${hookScript}" --session=${sessionId}` }]
|
|
608
|
+
}]
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
let extraArgsClean = options.extraArgs ? [...options.extraArgs] : [];
|
|
612
|
+
const settingsIdx = extraArgsClean.indexOf("--settings");
|
|
613
|
+
if (settingsIdx !== -1 && settingsIdx + 1 < extraArgsClean.length) {
|
|
614
|
+
try {
|
|
615
|
+
const appSettings = JSON.parse(extraArgsClean[settingsIdx + 1]);
|
|
616
|
+
if (appSettings.hooks) {
|
|
617
|
+
for (const [event, hooks] of Object.entries(appSettings.hooks)) {
|
|
618
|
+
if (sdkSettings.hooks && sdkSettings.hooks[event]) {
|
|
619
|
+
sdkSettings.hooks[event] = [
|
|
620
|
+
...sdkSettings.hooks[event],
|
|
621
|
+
...hooks
|
|
622
|
+
];
|
|
623
|
+
} else {
|
|
624
|
+
sdkSettings.hooks[event] = hooks;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
delete appSettings.hooks;
|
|
628
|
+
}
|
|
629
|
+
Object.assign(sdkSettings, appSettings);
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
extraArgsClean.splice(settingsIdx, 2);
|
|
633
|
+
}
|
|
634
|
+
const args = [
|
|
635
|
+
"--output-format",
|
|
636
|
+
"stream-json",
|
|
637
|
+
"--input-format",
|
|
638
|
+
"stream-json",
|
|
639
|
+
"--verbose",
|
|
640
|
+
"--include-partial-messages",
|
|
641
|
+
"--settings",
|
|
642
|
+
JSON.stringify(sdkSettings)
|
|
643
|
+
];
|
|
644
|
+
if (options.model) {
|
|
645
|
+
args.push("--model", options.model);
|
|
646
|
+
}
|
|
647
|
+
if (options.permissionMode) {
|
|
648
|
+
args.push("--permission-mode", options.permissionMode);
|
|
649
|
+
}
|
|
650
|
+
if (options.history?.length && options.prompt) {
|
|
651
|
+
const result = writeHistoryJsonl(options.history, { cwd: options.cwd });
|
|
652
|
+
if (result) {
|
|
653
|
+
args.push(...result.extraArgs);
|
|
654
|
+
options._historyViaResume = true;
|
|
655
|
+
logger.log("agent", `history via JSONL resume \u2192 ${result.filePath}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (extraArgsClean.length > 0) {
|
|
659
|
+
args.push(...extraArgsClean);
|
|
660
|
+
}
|
|
661
|
+
const cleanEnv = { ...process.env, ...options.env };
|
|
662
|
+
if (options.configDir) {
|
|
663
|
+
cleanEnv.CLAUDE_CONFIG_DIR = options.configDir;
|
|
664
|
+
}
|
|
665
|
+
delete cleanEnv.CLAUDECODE;
|
|
666
|
+
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
667
|
+
delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
|
|
668
|
+
delete cleanEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
|
669
|
+
const claudeDir = import_path3.default.dirname(claudePath);
|
|
670
|
+
if (claudeDir && claudeDir !== ".") {
|
|
671
|
+
cleanEnv.PATH = `${claudeDir}:${cleanEnv.PATH ?? ""}`;
|
|
672
|
+
}
|
|
673
|
+
const proc = (0, import_child_process.spawn)(claudePath, [...claudePrefix, ...args], {
|
|
674
|
+
cwd: options.cwd,
|
|
675
|
+
env: cleanEnv,
|
|
676
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
677
|
+
});
|
|
678
|
+
logger.log("agent", `spawned claude-code (pid=${proc.pid}) \u2192 ${claudeCommand} ${args.join(" ")}`);
|
|
679
|
+
return new ClaudeCodeProcess(proc, options);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// src/electron/index.ts
|
|
684
|
+
var import_path6 = __toESM(require("path"), 1);
|
|
685
|
+
var import_hono4 = require("hono");
|
|
686
|
+
var import_cors = require("hono/cors");
|
|
687
|
+
var import_node_server = require("@hono/node-server");
|
|
688
|
+
|
|
689
|
+
// src/server/index.ts
|
|
690
|
+
var import_hono3 = require("hono");
|
|
691
|
+
|
|
692
|
+
// src/server/routes/events.ts
|
|
693
|
+
var import_streaming = require("hono/streaming");
|
|
694
|
+
|
|
695
|
+
// src/db/schema.ts
|
|
696
|
+
var import_node_module = require("module");
|
|
697
|
+
var import_path4 = __toESM(require("path"), 1);
|
|
698
|
+
var NATIVE_DIR = import_path4.default.join(process.cwd(), ".sna/native");
|
|
699
|
+
|
|
700
|
+
// src/config.ts
|
|
701
|
+
var defaults = {
|
|
702
|
+
port: 3099,
|
|
703
|
+
model: "claude-sonnet-4-6",
|
|
704
|
+
defaultProvider: "claude-code",
|
|
705
|
+
defaultPermissionMode: "default",
|
|
706
|
+
maxSessions: 5,
|
|
707
|
+
maxEventBuffer: 500,
|
|
708
|
+
permissionTimeoutMs: 0,
|
|
709
|
+
// app controls — no SDK-side timeout
|
|
710
|
+
runOnceTimeoutMs: 12e4,
|
|
711
|
+
pollIntervalMs: 500,
|
|
712
|
+
keepaliveIntervalMs: 15e3,
|
|
713
|
+
skillPollMs: 2e3,
|
|
714
|
+
dbPath: "data/sna.db"
|
|
715
|
+
};
|
|
716
|
+
function fromEnv() {
|
|
717
|
+
const env = {};
|
|
718
|
+
if (process.env.SNA_PORT) env.port = parseInt(process.env.SNA_PORT, 10);
|
|
719
|
+
if (process.env.SNA_MODEL) env.model = process.env.SNA_MODEL;
|
|
720
|
+
if (process.env.SNA_PERMISSION_MODE) env.defaultPermissionMode = process.env.SNA_PERMISSION_MODE;
|
|
721
|
+
if (process.env.SNA_MAX_SESSIONS) env.maxSessions = parseInt(process.env.SNA_MAX_SESSIONS, 10);
|
|
722
|
+
if (process.env.SNA_DB_PATH) env.dbPath = process.env.SNA_DB_PATH;
|
|
723
|
+
if (process.env.SNA_PERMISSION_TIMEOUT_MS) env.permissionTimeoutMs = parseInt(process.env.SNA_PERMISSION_TIMEOUT_MS, 10);
|
|
724
|
+
return env;
|
|
725
|
+
}
|
|
726
|
+
var current = { ...defaults, ...fromEnv() };
|
|
727
|
+
|
|
728
|
+
// src/server/routes/run.ts
|
|
729
|
+
var import_streaming2 = require("hono/streaming");
|
|
730
|
+
var ROOT = process.cwd();
|
|
731
|
+
|
|
732
|
+
// src/server/routes/agent.ts
|
|
733
|
+
var import_hono = require("hono");
|
|
734
|
+
var import_streaming3 = require("hono/streaming");
|
|
735
|
+
|
|
736
|
+
// src/core/providers/codex.ts
|
|
737
|
+
var CodexProvider = class {
|
|
738
|
+
constructor() {
|
|
739
|
+
this.name = "codex";
|
|
740
|
+
}
|
|
741
|
+
async isAvailable() {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
spawn(_options) {
|
|
745
|
+
throw new Error("Codex provider not yet implemented");
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// src/core/providers/index.ts
|
|
750
|
+
var providers = {
|
|
751
|
+
"claude-code": new ClaudeCodeProvider(),
|
|
752
|
+
"codex": new CodexProvider()
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/server/image-store.ts
|
|
756
|
+
var import_path5 = __toESM(require("path"), 1);
|
|
757
|
+
var IMAGE_DIR = import_path5.default.join(process.cwd(), "data/images");
|
|
758
|
+
|
|
759
|
+
// src/server/routes/chat.ts
|
|
760
|
+
var import_hono2 = require("hono");
|
|
761
|
+
|
|
762
|
+
// src/server/ws.ts
|
|
763
|
+
var import_ws = require("ws");
|
|
443
764
|
|
|
444
765
|
// src/electron/index.ts
|
|
445
|
-
var import_path2 = __toESM(require("path"), 1);
|
|
446
766
|
function resolveStandaloneScript() {
|
|
447
|
-
const selfPath = (0,
|
|
448
|
-
let script =
|
|
767
|
+
const selfPath = (0, import_url2.fileURLToPath)(importMetaUrl);
|
|
768
|
+
let script = import_path6.default.resolve(import_path6.default.dirname(selfPath), "../server/standalone.js");
|
|
449
769
|
if (script.includes(".asar") && !script.includes(".asar.unpacked")) {
|
|
450
770
|
script = script.replace(/(\.asar)([/\\])/, ".asar.unpacked$2");
|
|
451
771
|
}
|
|
452
|
-
if (!
|
|
772
|
+
if (!import_fs4.default.existsSync(script)) {
|
|
453
773
|
throw new Error(
|
|
454
774
|
`SNA standalone script not found: ${script}
|
|
455
775
|
Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
@@ -460,14 +780,14 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
|
460
780
|
function buildNodePath() {
|
|
461
781
|
const resourcesPath = process.resourcesPath;
|
|
462
782
|
if (!resourcesPath) return void 0;
|
|
463
|
-
const unpacked =
|
|
464
|
-
if (!
|
|
783
|
+
const unpacked = import_path6.default.join(resourcesPath, "app.asar.unpacked", "node_modules");
|
|
784
|
+
if (!import_fs4.default.existsSync(unpacked)) return void 0;
|
|
465
785
|
const existing = process.env.NODE_PATH;
|
|
466
|
-
return existing ? `${unpacked}${
|
|
786
|
+
return existing ? `${unpacked}${import_path6.default.delimiter}${existing}` : unpacked;
|
|
467
787
|
}
|
|
468
788
|
async function startSnaServer(options) {
|
|
469
789
|
const port = options.port ?? 3099;
|
|
470
|
-
const cwd = options.cwd ??
|
|
790
|
+
const cwd = options.cwd ?? import_path6.default.dirname(options.dbPath);
|
|
471
791
|
const readyTimeout = options.readyTimeout ?? 15e3;
|
|
472
792
|
const { onLog } = options;
|
|
473
793
|
const standaloneScript = resolveStandaloneScript();
|
|
@@ -475,7 +795,7 @@ async function startSnaServer(options) {
|
|
|
475
795
|
let consumerModules;
|
|
476
796
|
try {
|
|
477
797
|
const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
|
|
478
|
-
consumerModules =
|
|
798
|
+
consumerModules = import_path6.default.resolve(bsPkg, "../..");
|
|
479
799
|
} catch {
|
|
480
800
|
}
|
|
481
801
|
const env = {
|
|
@@ -492,7 +812,7 @@ async function startSnaServer(options) {
|
|
|
492
812
|
// Consumer overrides last so they can always win
|
|
493
813
|
...options.env ?? {}
|
|
494
814
|
};
|
|
495
|
-
const proc = (0,
|
|
815
|
+
const proc = (0, import_child_process2.fork)(standaloneScript, [], {
|
|
496
816
|
cwd,
|
|
497
817
|
env,
|
|
498
818
|
stdio: "pipe"
|
package/dist/node/index.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ import { streamSSE } from "hono/streaming";
|
|
|
13
13
|
import { createRequire } from "module";
|
|
14
14
|
import fs from "fs";
|
|
15
15
|
import path from "path";
|
|
16
|
-
|
|
16
|
+
function getDbPath() {
|
|
17
|
+
return process.env.SNA_DB_PATH ?? path.join(process.cwd(), "data/sna.db");
|
|
18
|
+
}
|
|
17
19
|
var NATIVE_DIR = path.join(process.cwd(), ".sna/native");
|
|
18
20
|
var _db = null;
|
|
19
21
|
function loadBetterSqlite3() {
|
|
@@ -36,10 +38,10 @@ function loadBetterSqlite3() {
|
|
|
36
38
|
function getDb() {
|
|
37
39
|
if (!_db) {
|
|
38
40
|
const BetterSqlite3 = loadBetterSqlite3();
|
|
39
|
-
const dir = path.dirname(
|
|
41
|
+
const dir = path.dirname(getDbPath());
|
|
40
42
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
41
43
|
const nativeBinding = process.env.SNA_SQLITE_NATIVE_BINDING || void 0;
|
|
42
|
-
_db = nativeBinding ? new BetterSqlite3(
|
|
44
|
+
_db = nativeBinding ? new BetterSqlite3(getDbPath(), { nativeBinding }) : new BetterSqlite3(getDbPath());
|
|
43
45
|
_db.pragma("journal_mode = WAL");
|
|
44
46
|
initSchema(_db);
|
|
45
47
|
}
|