@sna-sdk/core 0.0.0

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 (45) hide show
  1. package/bin/sna.js +18 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +104 -0
  4. package/dist/core/providers/claude-code.d.ts +9 -0
  5. package/dist/core/providers/claude-code.js +257 -0
  6. package/dist/core/providers/codex.d.ts +18 -0
  7. package/dist/core/providers/codex.js +14 -0
  8. package/dist/core/providers/index.d.ts +14 -0
  9. package/dist/core/providers/index.js +22 -0
  10. package/dist/core/providers/types.d.ts +52 -0
  11. package/dist/core/providers/types.js +0 -0
  12. package/dist/db/schema.d.ts +13 -0
  13. package/dist/db/schema.js +41 -0
  14. package/dist/index.d.ts +15 -0
  15. package/dist/index.js +6 -0
  16. package/dist/lib/logger.d.ts +18 -0
  17. package/dist/lib/logger.js +50 -0
  18. package/dist/lib/sna-run.d.ts +25 -0
  19. package/dist/lib/sna-run.js +74 -0
  20. package/dist/scripts/emit.d.ts +2 -0
  21. package/dist/scripts/emit.js +48 -0
  22. package/dist/scripts/hook.d.ts +2 -0
  23. package/dist/scripts/hook.js +34 -0
  24. package/dist/scripts/init-db.d.ts +2 -0
  25. package/dist/scripts/init-db.js +3 -0
  26. package/dist/scripts/sna.d.ts +2 -0
  27. package/dist/scripts/sna.js +650 -0
  28. package/dist/scripts/workflow.d.ts +112 -0
  29. package/dist/scripts/workflow.js +622 -0
  30. package/dist/server/index.d.ts +30 -0
  31. package/dist/server/index.js +43 -0
  32. package/dist/server/routes/agent.d.ts +8 -0
  33. package/dist/server/routes/agent.js +148 -0
  34. package/dist/server/routes/emit.d.ts +11 -0
  35. package/dist/server/routes/emit.js +15 -0
  36. package/dist/server/routes/events.d.ts +12 -0
  37. package/dist/server/routes/events.js +54 -0
  38. package/dist/server/routes/run.d.ts +19 -0
  39. package/dist/server/routes/run.js +51 -0
  40. package/dist/server/session-manager.d.ts +64 -0
  41. package/dist/server/session-manager.js +101 -0
  42. package/dist/server/standalone.js +820 -0
  43. package/package.json +91 -0
  44. package/skills/sna-down/SKILL.md +23 -0
  45. package/skills/sna-up/SKILL.md +40 -0
@@ -0,0 +1,148 @@
1
+ import { Hono } from "hono";
2
+ import { streamSSE } from "hono/streaming";
3
+ import {
4
+ getProvider
5
+ } from "../../core/providers/index.js";
6
+ import { logger } from "../../lib/logger.js";
7
+ function getSessionId(c) {
8
+ return c.req.query("session") ?? "default";
9
+ }
10
+ function createAgentRoutes(sessionManager) {
11
+ const app = new Hono();
12
+ app.post("/sessions", async (c) => {
13
+ const body = await c.req.json().catch(() => ({}));
14
+ try {
15
+ const session = sessionManager.createSession({
16
+ label: body.label,
17
+ cwd: body.cwd
18
+ });
19
+ logger.log("route", `POST /sessions \u2192 created "${session.id}"`);
20
+ return c.json({ status: "created", sessionId: session.id, label: session.label });
21
+ } catch (e) {
22
+ logger.err("err", `POST /sessions \u2192 ${e.message}`);
23
+ return c.json({ status: "error", message: e.message }, 409);
24
+ }
25
+ });
26
+ app.get("/sessions", (c) => {
27
+ return c.json({ sessions: sessionManager.listSessions() });
28
+ });
29
+ app.delete("/sessions/:id", (c) => {
30
+ const id = c.req.param("id");
31
+ if (id === "default") {
32
+ return c.json({ status: "error", message: "Cannot remove default session" }, 400);
33
+ }
34
+ const removed = sessionManager.removeSession(id);
35
+ if (!removed) {
36
+ return c.json({ status: "error", message: "Session not found" }, 404);
37
+ }
38
+ logger.log("route", `DELETE /sessions/${id} \u2192 removed`);
39
+ return c.json({ status: "removed" });
40
+ });
41
+ app.post("/start", async (c) => {
42
+ const sessionId = getSessionId(c);
43
+ const body = await c.req.json().catch(() => ({}));
44
+ const session = sessionManager.getOrCreateSession(sessionId);
45
+ if (session.process?.alive && !body.force) {
46
+ logger.log("route", `POST /start?session=${sessionId} \u2192 already_running`);
47
+ return c.json({
48
+ status: "already_running",
49
+ provider: "claude-code",
50
+ sessionId: session.process.sessionId
51
+ });
52
+ }
53
+ if (session.process?.alive) {
54
+ session.process.kill();
55
+ }
56
+ session.eventBuffer.length = 0;
57
+ const provider = getProvider(body.provider ?? "claude-code");
58
+ try {
59
+ const proc = provider.spawn({
60
+ cwd: session.cwd,
61
+ prompt: body.prompt,
62
+ model: body.model ?? "claude-sonnet-4-6",
63
+ permissionMode: body.permissionMode ?? "acceptEdits"
64
+ });
65
+ sessionManager.setProcess(sessionId, proc);
66
+ logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
67
+ return c.json({
68
+ status: "started",
69
+ provider: provider.name,
70
+ sessionId: session.id
71
+ });
72
+ } catch (e) {
73
+ logger.err("err", `POST /start?session=${sessionId} failed: ${e.message}`);
74
+ return c.json({ status: "error", message: e.message }, 500);
75
+ }
76
+ });
77
+ app.post("/send", async (c) => {
78
+ const sessionId = getSessionId(c);
79
+ const session = sessionManager.getSession(sessionId);
80
+ if (!session?.process?.alive) {
81
+ logger.err("err", `POST /send?session=${sessionId} \u2192 no active session`);
82
+ return c.json(
83
+ { status: "error", message: `No active agent session "${sessionId}". Call POST /start first.` },
84
+ 400
85
+ );
86
+ }
87
+ const body = await c.req.json().catch(() => ({}));
88
+ if (!body.message) {
89
+ logger.err("err", `POST /send?session=${sessionId} \u2192 empty message`);
90
+ return c.json({ status: "error", message: "message is required" }, 400);
91
+ }
92
+ sessionManager.touch(sessionId);
93
+ logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
94
+ session.process.send(body.message);
95
+ return c.json({ status: "sent" });
96
+ });
97
+ app.get("/events", (c) => {
98
+ const sessionId = getSessionId(c);
99
+ const session = sessionManager.getOrCreateSession(sessionId);
100
+ const sinceParam = c.req.query("since");
101
+ let cursor = sinceParam ? parseInt(sinceParam, 10) : session.eventCounter;
102
+ return streamSSE(c, async (stream) => {
103
+ const POLL_MS = 300;
104
+ const KEEPALIVE_MS = 15e3;
105
+ let lastSend = Date.now();
106
+ while (true) {
107
+ if (cursor < session.eventCounter) {
108
+ const startIdx = Math.max(
109
+ 0,
110
+ session.eventBuffer.length - (session.eventCounter - cursor)
111
+ );
112
+ const newEvents = session.eventBuffer.slice(startIdx);
113
+ for (const event of newEvents) {
114
+ cursor++;
115
+ await stream.writeSSE({
116
+ id: String(cursor),
117
+ data: JSON.stringify(event)
118
+ });
119
+ lastSend = Date.now();
120
+ }
121
+ }
122
+ if (Date.now() - lastSend > KEEPALIVE_MS) {
123
+ await stream.writeSSE({ data: "" });
124
+ lastSend = Date.now();
125
+ }
126
+ await new Promise((r) => setTimeout(r, POLL_MS));
127
+ }
128
+ });
129
+ });
130
+ app.post("/kill", async (c) => {
131
+ const sessionId = getSessionId(c);
132
+ const killed = sessionManager.killSession(sessionId);
133
+ return c.json({ status: killed ? "killed" : "no_session" });
134
+ });
135
+ app.get("/status", (c) => {
136
+ const sessionId = getSessionId(c);
137
+ const session = sessionManager.getSession(sessionId);
138
+ return c.json({
139
+ alive: session?.process?.alive ?? false,
140
+ sessionId: session?.process?.sessionId ?? null,
141
+ eventCount: session?.eventCounter ?? 0
142
+ });
143
+ });
144
+ return app;
145
+ }
146
+ export {
147
+ createAgentRoutes
148
+ };
@@ -0,0 +1,11 @@
1
+ import * as hono_utils_http_status from 'hono/utils/http-status';
2
+ import * as hono from 'hono';
3
+ import { Context } from 'hono';
4
+
5
+ declare function emitRoute(c: Context): Promise<(Response & hono.TypedResponse<{
6
+ error: string;
7
+ }, 400, "json">) | (Response & hono.TypedResponse<{
8
+ id: number;
9
+ }, hono_utils_http_status.ContentfulStatusCode, "json">)>;
10
+
11
+ export { emitRoute };
@@ -0,0 +1,15 @@
1
+ import { getDb } from "../../db/schema.js";
2
+ async function emitRoute(c) {
3
+ const { skill, type, message, data } = await c.req.json();
4
+ if (!skill || !type || !message) {
5
+ return c.json({ error: "missing fields" }, 400);
6
+ }
7
+ const db = getDb();
8
+ const result = db.prepare(
9
+ `INSERT INTO skill_events (skill, type, message, data) VALUES (?, ?, ?, ?)`
10
+ ).run(skill, type, message, data ?? null);
11
+ return c.json({ id: result.lastInsertRowid });
12
+ }
13
+ export {
14
+ emitRoute
15
+ };
@@ -0,0 +1,12 @@
1
+ import { Context } from 'hono';
2
+
3
+ /**
4
+ * GET /events?since=<id>
5
+ *
6
+ * SSE stream of skill_events from SQLite.
7
+ * Polls every 500ms for new rows with id > lastSeen.
8
+ */
9
+
10
+ declare function eventsRoute(c: Context): Response;
11
+
12
+ export { eventsRoute };
@@ -0,0 +1,54 @@
1
+ import { streamSSE } from "hono/streaming";
2
+ import { getDb } from "../../db/schema.js";
3
+ const POLL_INTERVAL_MS = 500;
4
+ const KEEPALIVE_INTERVAL_MS = 15e3;
5
+ function eventsRoute(c) {
6
+ const sinceParam = c.req.query("since");
7
+ let lastId = sinceParam ? parseInt(sinceParam) : -1;
8
+ if (lastId === -1) {
9
+ const db = getDb();
10
+ const row = db.prepare("SELECT MAX(id) as maxId FROM skill_events").get();
11
+ lastId = row.maxId ?? 0;
12
+ }
13
+ return streamSSE(c, async (stream) => {
14
+ let closed = false;
15
+ stream.onAbort(() => {
16
+ closed = true;
17
+ });
18
+ const keepaliveTimer = setInterval(async () => {
19
+ if (closed) {
20
+ clearInterval(keepaliveTimer);
21
+ return;
22
+ }
23
+ try {
24
+ await stream.writeSSE({ data: "", event: "keepalive" });
25
+ } catch {
26
+ closed = true;
27
+ clearInterval(keepaliveTimer);
28
+ }
29
+ }, KEEPALIVE_INTERVAL_MS);
30
+ while (!closed) {
31
+ try {
32
+ const db = getDb();
33
+ const rows = db.prepare(`
34
+ SELECT id, skill, type, message, data, created_at
35
+ FROM skill_events
36
+ WHERE id > ?
37
+ ORDER BY id ASC
38
+ LIMIT 50
39
+ `).all(lastId);
40
+ for (const row of rows) {
41
+ if (closed) break;
42
+ await stream.writeSSE({ data: JSON.stringify(row) });
43
+ lastId = row.id;
44
+ }
45
+ } catch {
46
+ }
47
+ await stream.sleep(POLL_INTERVAL_MS);
48
+ }
49
+ clearInterval(keepaliveTimer);
50
+ });
51
+ }
52
+ export {
53
+ eventsRoute
54
+ };
@@ -0,0 +1,19 @@
1
+ import { Context } from 'hono';
2
+
3
+ /**
4
+ * GET /run?skill=<name>
5
+ *
6
+ * Spawn a registered command and stream stdout/stderr as SSE.
7
+ *
8
+ * @example
9
+ * import { createRunRoute } from "sna/server/routes/run";
10
+ *
11
+ * const runRoute = createRunRoute({
12
+ * status: [TSX, "src/scripts/sna.ts", "status"],
13
+ * collect: [TSX, "src/scripts/devlog.ts", "collect"],
14
+ * });
15
+ */
16
+
17
+ declare function createRunRoute(commands: Record<string, string[]>): (c: Context) => Response;
18
+
19
+ export { createRunRoute };
@@ -0,0 +1,51 @@
1
+ import { spawn } from "child_process";
2
+ import { streamSSE } from "hono/streaming";
3
+ const ROOT = process.cwd();
4
+ function createRunRoute(commands) {
5
+ return function runRoute(c) {
6
+ const skill = c.req.query("skill") ?? "";
7
+ const cmd = commands[skill];
8
+ if (!cmd) {
9
+ return c.text(`data: unknown skill: ${skill}
10
+
11
+ data: [done]
12
+
13
+ `, 200, {
14
+ "Content-Type": "text/event-stream"
15
+ });
16
+ }
17
+ return streamSSE(c, async (stream) => {
18
+ await stream.writeSSE({ data: `$ ${cmd.slice(1).join(" ")}` });
19
+ const child = spawn(cmd[0], cmd.slice(1), {
20
+ cwd: ROOT,
21
+ env: { ...process.env, FORCE_COLOR: "0" }
22
+ });
23
+ const write = (chunk) => {
24
+ for (const line of chunk.toString().split("\n")) {
25
+ if (line.trim()) stream.writeSSE({ data: line });
26
+ }
27
+ };
28
+ child.stdout.on("data", write);
29
+ child.stderr.on("data", (chunk) => {
30
+ for (const line of chunk.toString().split("\n")) {
31
+ if (line.trim() && !line.startsWith(">")) stream.writeSSE({ data: line });
32
+ }
33
+ });
34
+ await new Promise((resolve) => {
35
+ child.on("close", async (code) => {
36
+ await stream.writeSSE({ data: `[exit ${code ?? 0}]` });
37
+ await stream.writeSSE({ data: "[done]" });
38
+ resolve();
39
+ });
40
+ child.on("error", async (err) => {
41
+ await stream.writeSSE({ data: `Error: ${err.message}` });
42
+ await stream.writeSSE({ data: "[done]" });
43
+ resolve();
44
+ });
45
+ });
46
+ });
47
+ };
48
+ }
49
+ export {
50
+ createRunRoute
51
+ };
@@ -0,0 +1,64 @@
1
+ import { AgentProcess, AgentEvent } from '../core/providers/types.js';
2
+
3
+ /**
4
+ * SessionManager — manages multiple independent agent sessions.
5
+ *
6
+ * Each session owns its own AgentProcess, event buffer, and cursor.
7
+ * The default "default" session provides backward compatibility.
8
+ */
9
+
10
+ interface Session {
11
+ id: string;
12
+ process: AgentProcess | null;
13
+ eventBuffer: AgentEvent[];
14
+ eventCounter: number;
15
+ label: string;
16
+ cwd: string;
17
+ createdAt: number;
18
+ lastActivityAt: number;
19
+ }
20
+ interface SessionInfo {
21
+ id: string;
22
+ label: string;
23
+ alive: boolean;
24
+ cwd: string;
25
+ eventCount: number;
26
+ createdAt: number;
27
+ lastActivityAt: number;
28
+ }
29
+ interface SessionManagerOptions {
30
+ maxSessions?: number;
31
+ }
32
+ declare class SessionManager {
33
+ private sessions;
34
+ private maxSessions;
35
+ constructor(options?: SessionManagerOptions);
36
+ /** Create a new session. Throws if max sessions reached. */
37
+ createSession(opts?: {
38
+ id?: string;
39
+ label?: string;
40
+ cwd?: string;
41
+ }): Session;
42
+ /** Get a session by ID. */
43
+ getSession(id: string): Session | undefined;
44
+ /** Get or create a session (used for "default" backward compat). */
45
+ getOrCreateSession(id: string, opts?: {
46
+ label?: string;
47
+ cwd?: string;
48
+ }): Session;
49
+ /** Set the agent process for a session. Subscribes to events. */
50
+ setProcess(sessionId: string, proc: AgentProcess): void;
51
+ /** Kill the agent process in a session (session stays, can be restarted). */
52
+ killSession(id: string): boolean;
53
+ /** Remove a session entirely. Cannot remove "default". */
54
+ removeSession(id: string): boolean;
55
+ /** List all sessions as serializable info objects. */
56
+ listSessions(): SessionInfo[];
57
+ /** Touch a session's lastActivityAt timestamp. */
58
+ touch(id: string): void;
59
+ /** Kill all sessions. Used during shutdown. */
60
+ killAll(): void;
61
+ get size(): number;
62
+ }
63
+
64
+ export { type Session, type SessionInfo, SessionManager, type SessionManagerOptions };
@@ -0,0 +1,101 @@
1
+ const DEFAULT_MAX_SESSIONS = 5;
2
+ const MAX_EVENT_BUFFER = 500;
3
+ class SessionManager {
4
+ constructor(options = {}) {
5
+ this.sessions = /* @__PURE__ */ new Map();
6
+ this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
7
+ }
8
+ /** Create a new session. Throws if max sessions reached. */
9
+ createSession(opts = {}) {
10
+ const id = opts.id ?? crypto.randomUUID().slice(0, 8);
11
+ if (this.sessions.has(id)) {
12
+ return this.sessions.get(id);
13
+ }
14
+ if (this.sessions.size >= this.maxSessions) {
15
+ throw new Error(`Max sessions (${this.maxSessions}) reached`);
16
+ }
17
+ const session = {
18
+ id,
19
+ process: null,
20
+ eventBuffer: [],
21
+ eventCounter: 0,
22
+ label: opts.label ?? id,
23
+ cwd: opts.cwd ?? process.cwd(),
24
+ createdAt: Date.now(),
25
+ lastActivityAt: Date.now()
26
+ };
27
+ this.sessions.set(id, session);
28
+ return session;
29
+ }
30
+ /** Get a session by ID. */
31
+ getSession(id) {
32
+ return this.sessions.get(id);
33
+ }
34
+ /** Get or create a session (used for "default" backward compat). */
35
+ getOrCreateSession(id, opts) {
36
+ const existing = this.sessions.get(id);
37
+ if (existing) return existing;
38
+ return this.createSession({ id, ...opts });
39
+ }
40
+ /** Set the agent process for a session. Subscribes to events. */
41
+ setProcess(sessionId, proc) {
42
+ const session = this.sessions.get(sessionId);
43
+ if (!session) throw new Error(`Session "${sessionId}" not found`);
44
+ session.process = proc;
45
+ session.lastActivityAt = Date.now();
46
+ proc.on("event", (e) => {
47
+ session.eventBuffer.push(e);
48
+ session.eventCounter++;
49
+ if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
50
+ session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
51
+ }
52
+ });
53
+ }
54
+ /** Kill the agent process in a session (session stays, can be restarted). */
55
+ killSession(id) {
56
+ const session = this.sessions.get(id);
57
+ if (!session?.process?.alive) return false;
58
+ session.process.kill();
59
+ return true;
60
+ }
61
+ /** Remove a session entirely. Cannot remove "default". */
62
+ removeSession(id) {
63
+ if (id === "default") return false;
64
+ const session = this.sessions.get(id);
65
+ if (!session) return false;
66
+ if (session.process?.alive) session.process.kill();
67
+ this.sessions.delete(id);
68
+ return true;
69
+ }
70
+ /** List all sessions as serializable info objects. */
71
+ listSessions() {
72
+ return Array.from(this.sessions.values()).map((s) => ({
73
+ id: s.id,
74
+ label: s.label,
75
+ alive: s.process?.alive ?? false,
76
+ cwd: s.cwd,
77
+ eventCount: s.eventCounter,
78
+ createdAt: s.createdAt,
79
+ lastActivityAt: s.lastActivityAt
80
+ }));
81
+ }
82
+ /** Touch a session's lastActivityAt timestamp. */
83
+ touch(id) {
84
+ const session = this.sessions.get(id);
85
+ if (session) session.lastActivityAt = Date.now();
86
+ }
87
+ /** Kill all sessions. Used during shutdown. */
88
+ killAll() {
89
+ for (const session of this.sessions.values()) {
90
+ if (session.process?.alive) {
91
+ session.process.kill();
92
+ }
93
+ }
94
+ }
95
+ get size() {
96
+ return this.sessions.size;
97
+ }
98
+ }
99
+ export {
100
+ SessionManager
101
+ };