@sna-sdk/core 0.9.9 → 0.9.11

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.
@@ -1,4 +1,8 @@
1
1
  import { ChildProcess } from 'child_process';
2
+ import http from 'http';
3
+ export { ResolveResult, cacheClaudePath, parseCommandVOutput, resolveClaudeCli, validateClaudePath } from '../core/providers/claude-code.js';
4
+ import { SessionManager } from '../server/session-manager.js';
5
+ import '../core/providers/types.js';
2
6
 
3
7
  /**
4
8
  * @sna-sdk/core/electron — Electron launcher API
@@ -100,5 +104,33 @@ interface SnaServerHandle {
100
104
  * Throws if the server fails to start within `options.readyTimeout`.
101
105
  */
102
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>;
103
135
 
104
- export { type SnaServerHandle, type SnaServerOptions, startSnaServer };
136
+ export { type InProcessSnaServerHandle, type SnaServerHandle, type SnaServerOptions, startSnaServer, startSnaServerInProcess };
@@ -1,7 +1,16 @@
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";
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";
5
14
  function resolveStandaloneScript() {
6
15
  const selfPath = fileURLToPath(import.meta.url);
7
16
  let script = path.resolve(path.dirname(selfPath), "../server/standalone.js");
@@ -106,6 +115,82 @@ async function startSnaServer(options) {
106
115
  }
107
116
  };
108
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
+ }
109
189
  export {
110
- startSnaServer
190
+ cacheClaudePath,
191
+ parseCommandVOutput,
192
+ resolveClaudeCli,
193
+ startSnaServer,
194
+ startSnaServerInProcess,
195
+ validateClaudePath
111
196
  };