@sna-sdk/core 0.0.5 → 0.0.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.
@@ -174,21 +174,23 @@ class ClaudeCodeProcess {
174
174
  }
175
175
  case "result": {
176
176
  if (msg.subtype === "success") {
177
+ const u = msg.usage ?? {};
177
178
  const mu = msg.modelUsage ?? {};
178
179
  const modelKey = Object.keys(mu)[0] ?? "";
179
- const u = mu[modelKey] ?? {};
180
+ const modelInfo = mu[modelKey] ?? {};
180
181
  return {
181
182
  type: "complete",
182
183
  message: msg.result ?? "Done",
183
184
  data: {
184
185
  durationMs: msg.duration_ms,
185
186
  costUsd: msg.total_cost_usd,
186
- inputTokens: u.inputTokens ?? 0,
187
- outputTokens: u.outputTokens ?? 0,
188
- cacheReadTokens: u.cacheReadInputTokens ?? 0,
189
- cacheWriteTokens: u.cacheCreationInputTokens ?? 0,
190
- contextWindow: u.contextWindow ?? 0,
191
- maxOutputTokens: u.maxOutputTokens ?? 0,
187
+ // Per-turn: actual context window usage this turn
188
+ inputTokens: u.input_tokens ?? 0,
189
+ outputTokens: u.output_tokens ?? 0,
190
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
191
+ cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
192
+ // Static model info
193
+ contextWindow: modelInfo.contextWindow ?? 0,
192
194
  model: modelKey
193
195
  },
194
196
  timestamp: Date.now()
@@ -1,8 +1,24 @@
1
1
  import Database from 'better-sqlite3';
2
2
 
3
3
  declare function getDb(): Database.Database;
4
+ interface ChatSession {
5
+ id: string;
6
+ label: string;
7
+ type: "main" | "background";
8
+ created_at: string;
9
+ }
10
+ interface ChatMessage {
11
+ id: number;
12
+ session_id: string;
13
+ role: string;
14
+ content: string;
15
+ skill_name: string | null;
16
+ meta: string | null;
17
+ created_at: string;
18
+ }
4
19
  interface SkillEvent {
5
20
  id: number;
21
+ session_id: string | null;
6
22
  skill: string;
7
23
  type: "invoked" | "called" | "success" | "failed" | "permission_needed" | "start" | "progress" | "milestone" | "complete" | "error";
8
24
  message: string;
@@ -10,4 +26,4 @@ interface SkillEvent {
10
26
  created_at: string;
11
27
  }
12
28
 
13
- export { type SkillEvent, getDb };
29
+ export { type ChatMessage, type ChatSession, type SkillEvent, getDb };
package/dist/db/schema.js CHANGED
@@ -23,8 +23,31 @@ function migrateSkillEvents(db) {
23
23
  function initSchema(db) {
24
24
  migrateSkillEvents(db);
25
25
  db.exec(`
26
+ CREATE TABLE IF NOT EXISTS chat_sessions (
27
+ id TEXT PRIMARY KEY,
28
+ label TEXT NOT NULL DEFAULT '',
29
+ type TEXT NOT NULL DEFAULT 'main',
30
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
31
+ );
32
+
33
+ -- Ensure default session always exists
34
+ INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES ('default', 'Chat', 'main');
35
+
36
+ CREATE TABLE IF NOT EXISTS chat_messages (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ session_id TEXT NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
39
+ role TEXT NOT NULL,
40
+ content TEXT NOT NULL DEFAULT '',
41
+ skill_name TEXT,
42
+ meta TEXT,
43
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
44
+ );
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id);
47
+
26
48
  CREATE TABLE IF NOT EXISTS skill_events (
27
49
  id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ session_id TEXT REFERENCES chat_sessions(id) ON DELETE SET NULL,
28
51
  skill TEXT NOT NULL,
29
52
  type TEXT NOT NULL,
30
53
  message TEXT NOT NULL,
@@ -34,6 +57,7 @@ function initSchema(db) {
34
57
 
35
58
  CREATE INDEX IF NOT EXISTS idx_skill_events_skill ON skill_events(skill);
36
59
  CREATE INDEX IF NOT EXISTS idx_skill_events_created ON skill_events(created_at);
60
+ CREATE INDEX IF NOT EXISTS idx_skill_events_session ON skill_events(session_id);
37
61
  `);
38
62
  }
39
63
  export {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { SkillEvent } from './db/schema.js';
1
+ export { ChatMessage, ChatSession, SkillEvent } from './db/schema.js';
2
2
  export { AgentEvent, AgentProcess, AgentProvider, SpawnOptions } from './core/providers/types.js';
3
3
  export { Session, SessionInfo, SessionManagerOptions } from './server/session-manager.js';
4
4
  import 'better-sqlite3';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * skill-parser.ts — Parse SKILL.md frontmatter for sna schema definitions.
3
+ *
4
+ * Reads .claude/skills/<name>/SKILL.md files and extracts the `sna` field
5
+ * from YAML frontmatter.
6
+ */
7
+ interface SkillArgDef {
8
+ type: "string" | "number" | "boolean" | "string[]" | "number[]";
9
+ required?: boolean;
10
+ description?: string;
11
+ }
12
+ interface SkillSchema {
13
+ name: string;
14
+ camelName: string;
15
+ description: string;
16
+ args: Record<string, SkillArgDef>;
17
+ }
18
+ /** Parse a single SKILL.md file and extract sna schema. */
19
+ declare function parseSkillFile(filePath: string): SkillSchema | null;
20
+ /** Scan a directory for SKILL.md files and parse all sna schemas. */
21
+ declare function scanSkills(skillsDir: string): SkillSchema[];
22
+
23
+ export { type SkillArgDef, type SkillSchema, parseSkillFile, scanSkills };
@@ -0,0 +1,54 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import yaml from "js-yaml";
4
+ function toCamelCase(kebab) {
5
+ return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6
+ }
7
+ function parseFrontmatter(content) {
8
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
9
+ if (!match) return null;
10
+ try {
11
+ return yaml.load(match[1]);
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ function parseSkillFile(filePath) {
17
+ const content = fs.readFileSync(filePath, "utf-8");
18
+ const fm = parseFrontmatter(content);
19
+ if (!fm) return null;
20
+ const name = path.basename(path.dirname(filePath));
21
+ const description = fm.description ?? "";
22
+ const sna = fm.sna;
23
+ const rawArgs = sna?.args ?? {};
24
+ const args = {};
25
+ for (const [key, def] of Object.entries(rawArgs)) {
26
+ args[key] = {
27
+ type: def.type ?? "string",
28
+ required: def.required === true,
29
+ description: def.description ?? void 0
30
+ };
31
+ }
32
+ return {
33
+ name,
34
+ camelName: toCamelCase(name),
35
+ description,
36
+ args
37
+ };
38
+ }
39
+ function scanSkills(skillsDir) {
40
+ if (!fs.existsSync(skillsDir)) return [];
41
+ const schemas = [];
42
+ const entries = fs.readdirSync(skillsDir);
43
+ for (const entry of entries) {
44
+ const skillMd = path.join(skillsDir, entry, "SKILL.md");
45
+ if (!fs.existsSync(skillMd)) continue;
46
+ const schema = parseSkillFile(skillMd);
47
+ if (schema) schemas.push(schema);
48
+ }
49
+ return schemas;
50
+ }
51
+ export {
52
+ parseSkillFile,
53
+ scanSkills
54
+ };
@@ -21,18 +21,21 @@ const VALID_TYPES = [
21
21
  "error"
22
22
  ];
23
23
  if (!flags.skill || !flags.type || !flags.message) {
24
- console.error("Usage: tsx node_modules/sna/src/scripts/emit.ts --skill <name> --type <type> --message <text> [--data <json>]");
24
+ console.error("Usage: tsx node_modules/@sna-sdk/core/src/scripts/emit.ts --skill <name> --type <type> --message <text> [--data <json>]");
25
25
  process.exit(1);
26
26
  }
27
27
  if (!VALID_TYPES.includes(flags.type)) {
28
28
  console.error(`Invalid type: ${flags.type}. Must be one of: ${VALID_TYPES.join(", ")}`);
29
29
  process.exit(1);
30
30
  }
31
- const db = getDb();
32
- db.prepare(`
33
- INSERT INTO skill_events (skill, type, message, data)
34
- VALUES (?, ?, ?, ?)
35
- `).run(flags.skill, flags.type, flags.message, flags.data ?? null);
31
+ const sessionId = process.env.SNA_SESSION_ID;
32
+ if (sessionId) {
33
+ const db = getDb();
34
+ db.prepare(`
35
+ INSERT INTO skill_events (session_id, skill, type, message, data)
36
+ VALUES (?, ?, ?, ?, ?)
37
+ `).run(sessionId, flags.skill, flags.type, flags.message, flags.data ?? null);
38
+ }
36
39
  const prefix = {
37
40
  called: "\u2192",
38
41
  success: "\u2713",
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,114 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { scanSkills } from "../lib/skill-parser.js";
4
+ const ROOT = process.cwd();
5
+ function parseFlags(args) {
6
+ const flags2 = {};
7
+ for (let i = 0; i < args.length; i += 2) {
8
+ const key = args[i]?.replace(/^--/, "");
9
+ if (key) flags2[key] = args[i + 1] ?? "";
10
+ }
11
+ return flags2;
12
+ }
13
+ function tsType(argDef) {
14
+ switch (argDef.type) {
15
+ case "number":
16
+ return "number";
17
+ case "boolean":
18
+ return "boolean";
19
+ case "string[]":
20
+ return "string[]";
21
+ case "number[]":
22
+ return "number[]";
23
+ default:
24
+ return "string";
25
+ }
26
+ }
27
+ function generateClient(schemas2) {
28
+ const lines = [];
29
+ lines.push(`/**`);
30
+ lines.push(` * SNA Client \u2014 Auto-generated from SKILL.md frontmatter.`);
31
+ lines.push(` * DO NOT EDIT. Re-generate with: sna gen client`);
32
+ lines.push(` */`);
33
+ lines.push(``);
34
+ lines.push(`import type { SkillResult } from "@sna-sdk/react/hooks";`);
35
+ lines.push(``);
36
+ for (const s of schemas2) {
37
+ const argEntries = Object.entries(s.args);
38
+ if (argEntries.length > 0) {
39
+ lines.push(`export interface ${capitalize(s.camelName)}Args {`);
40
+ for (const [key, def] of argEntries) {
41
+ if (def.description) lines.push(` /** ${def.description} */`);
42
+ lines.push(` ${key}${def.required ? "" : "?"}: ${tsType(def)};`);
43
+ }
44
+ lines.push(`}`);
45
+ lines.push(``);
46
+ }
47
+ }
48
+ lines.push(`export interface SnaSkills {`);
49
+ for (const s of schemas2) {
50
+ const argEntries = Object.entries(s.args);
51
+ const argsType = argEntries.length > 0 ? `${capitalize(s.camelName)}Args` : "void";
52
+ lines.push(` /** ${s.description} */`);
53
+ lines.push(` ${s.camelName}: (${argsType === "void" ? "" : `args: ${argsType}`}) => Promise<SkillResult>;`);
54
+ }
55
+ lines.push(`}`);
56
+ lines.push(``);
57
+ lines.push(`export const skillDefinitions = {`);
58
+ for (const s of schemas2) {
59
+ const argKeys = Object.keys(s.args);
60
+ lines.push(` ${s.camelName}: { name: "${s.name}", argKeys: [${argKeys.map((k) => `"${k}"`).join(", ")}] },`);
61
+ }
62
+ lines.push(`} as const;`);
63
+ lines.push(``);
64
+ lines.push(`/**`);
65
+ lines.push(` * Create a typed SNA client.`);
66
+ lines.push(` *`);
67
+ lines.push(` * @example`);
68
+ lines.push(` * const { skills } = useSnaClient();`);
69
+ lines.push(` * await skills.formFill({ sessionId: 123 });`);
70
+ lines.push(` */`);
71
+ lines.push(`export function bindSkills(`);
72
+ lines.push(` runner: (command: string) => Promise<SkillResult>,`);
73
+ lines.push(`): SnaSkills {`);
74
+ lines.push(` const skills = {} as SnaSkills;`);
75
+ lines.push(` for (const [method, def] of Object.entries(skillDefinitions)) {`);
76
+ lines.push(` (skills as any)[method] = (args?: Record<string, unknown>) => {`);
77
+ lines.push(` const parts = [def.name];`);
78
+ lines.push(` if (args) {`);
79
+ lines.push(` for (const key of def.argKeys) {`);
80
+ lines.push(` if (args[key] !== undefined) {`);
81
+ lines.push(` const val = args[key];`);
82
+ lines.push(` parts.push(Array.isArray(val) ? val.join(" ") : String(val));`);
83
+ lines.push(` }`);
84
+ lines.push(` }`);
85
+ lines.push(` }`);
86
+ lines.push(` return runner(parts.join(" "));`);
87
+ lines.push(` };`);
88
+ lines.push(` }`);
89
+ lines.push(` return skills;`);
90
+ lines.push(`}`);
91
+ return lines.join("\n") + "\n";
92
+ }
93
+ function capitalize(s) {
94
+ return s.charAt(0).toUpperCase() + s.slice(1);
95
+ }
96
+ const [, , ...rawArgs] = process.argv;
97
+ const flags = parseFlags(rawArgs);
98
+ const skillsDir = flags["skills-dir"] ?? path.join(ROOT, ".claude/skills");
99
+ const outPath = flags.out ?? path.join(ROOT, "src/sna-client.ts");
100
+ const schemas = scanSkills(skillsDir);
101
+ if (schemas.length === 0) {
102
+ console.log("No skills with sna.args found in", skillsDir);
103
+ process.exit(0);
104
+ }
105
+ const code = generateClient(schemas);
106
+ const outDir = path.dirname(outPath);
107
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
108
+ fs.writeFileSync(outPath, code);
109
+ console.log(`\u2713 Generated ${outPath}`);
110
+ console.log(` ${schemas.length} skills:`);
111
+ for (const s of schemas) {
112
+ const argCount = Object.keys(s.args).length;
113
+ console.log(` ${s.camelName}(${argCount > 0 ? `{${Object.keys(s.args).join(", ")}}` : ""}) \u2192 ${s.name}`);
114
+ }
@@ -1,5 +1,6 @@
1
1
  import { execSync, spawn } from "child_process";
2
2
  import fs from "fs";
3
+ import net from "net";
3
4
  import path from "path";
4
5
  import { cmdNew, cmdWorkflow, cmdCancel, cmdTasks } from "./workflow.js";
5
6
  const ROOT = process.cwd();
@@ -13,7 +14,7 @@ const SNA_API_LOG_FILE = path.join(STATE_DIR, "sna-api.log");
13
14
  const PORT = process.env.PORT ?? "3000";
14
15
  const DB_PATH = path.join(ROOT, "data/app.db");
15
16
  const CLAUDE_PATH_FILE = path.join(STATE_DIR, "claude-path");
16
- const SNA_CORE_DIR = path.join(ROOT, "node_modules/sna");
17
+ const SNA_CORE_DIR = path.join(ROOT, "node_modules/@sna-sdk/core");
17
18
  function ensureStateDir() {
18
19
  if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
19
20
  }
@@ -56,10 +57,10 @@ function clearSnaApiState() {
56
57
  }
57
58
  }
58
59
  function findFreePort() {
59
- const net = require("net");
60
60
  const srv = net.createServer();
61
61
  srv.listen(0);
62
- const port = String(srv.address().port);
62
+ const addr = srv.address();
63
+ const port = String(addr.port);
63
64
  srv.close();
64
65
  return port;
65
66
  }
@@ -373,7 +374,7 @@ function cmdInit(force2 = false) {
373
374
  if (!fs.existsSync(claudeDir)) {
374
375
  fs.mkdirSync(claudeDir, { recursive: true });
375
376
  }
376
- const hookCommand = `node "$CLAUDE_PROJECT_DIR"/node_modules/sna/dist/scripts/hook.js`;
377
+ const hookCommand = `node "$CLAUDE_PROJECT_DIR"/node_modules/@sna-sdk/core/dist/scripts/hook.js`;
377
378
  const permissionHook = {
378
379
  matcher: ".*",
379
380
  hooks: [{ type: "command", async: true, command: hookCommand }]
@@ -398,7 +399,7 @@ function cmdInit(force2 = false) {
398
399
  } else {
399
400
  step(".claude/settings.json \u2014 hook already set, skipped");
400
401
  }
401
- const claudeMdTemplate = path.join(ROOT, "node_modules/sna/CLAUDE.md.template");
402
+ const claudeMdTemplate = path.join(ROOT, "node_modules/@sna-sdk/core/CLAUDE.md.template");
402
403
  const claudeMdDest = path.join(claudeDir, "CLAUDE.md");
403
404
  if (fs.existsSync(claudeMdTemplate)) {
404
405
  if (force2 || !fs.existsSync(claudeMdDest)) {
@@ -408,7 +409,7 @@ function cmdInit(force2 = false) {
408
409
  step(".claude/CLAUDE.md \u2014 already exists, skipped");
409
410
  }
410
411
  }
411
- const snaCoreSkillsDir = path.join(ROOT, "node_modules/sna/skills");
412
+ const snaCoreSkillsDir = path.join(ROOT, "node_modules/@sna-sdk/core/skills");
412
413
  const destSkillsDir = path.join(claudeDir, "skills");
413
414
  if (fs.existsSync(snaCoreSkillsDir)) {
414
415
  const skillNames = fs.readdirSync(snaCoreSkillsDir);
@@ -644,6 +645,15 @@ Run "sna help submit" for data submission patterns.`);
644
645
  console.log();
645
646
  cmdUp();
646
647
  break;
648
+ case "gen":
649
+ if (args[0] === "client") {
650
+ const { execSync: exec } = require("child_process");
651
+ const genScript = path.join(__dirname, "gen-client.js");
652
+ exec(`node "${genScript}" ${args.slice(1).join(" ")}`, { stdio: "inherit", cwd: ROOT });
653
+ } else {
654
+ console.log("Usage: sna gen client [--out <path>] [--skills-dir <path>]");
655
+ }
656
+ break;
647
657
  default:
648
658
  printHelp();
649
659
  }
@@ -6,6 +6,7 @@ export { eventsRoute } from './routes/events.js';
6
6
  export { emitRoute } from './routes/emit.js';
7
7
  export { createRunRoute } from './routes/run.js';
8
8
  export { createAgentRoutes } from './routes/agent.js';
9
+ export { createChatRoutes } from './routes/chat.js';
9
10
  import '../core/providers/types.js';
10
11
  import 'hono/utils/http-status';
11
12
 
@@ -5,6 +5,7 @@ import { eventsRoute } from "./routes/events.js";
5
5
  import { emitRoute } from "./routes/emit.js";
6
6
  import { createRunRoute } from "./routes/run.js";
7
7
  import { createAgentRoutes } from "./routes/agent.js";
8
+ import { createChatRoutes } from "./routes/chat.js";
8
9
  import { SessionManager } from "./session-manager.js";
9
10
  function createSnaApp(options = {}) {
10
11
  const sessionManager = options.sessionManager ?? new SessionManager();
@@ -13,6 +14,7 @@ function createSnaApp(options = {}) {
13
14
  app.get("/events", eventsRoute);
14
15
  app.post("/emit", emitRoute);
15
16
  app.route("/agent", createAgentRoutes(sessionManager));
17
+ app.route("/chat", createChatRoutes());
16
18
  if (options.runCommands) {
17
19
  app.get("/run", createRunRoute(options.runCommands));
18
20
  }
@@ -22,6 +24,7 @@ import { eventsRoute as eventsRoute2 } from "./routes/events.js";
22
24
  import { emitRoute as emitRoute2 } from "./routes/emit.js";
23
25
  import { createRunRoute as createRunRoute2 } from "./routes/run.js";
24
26
  import { createAgentRoutes as createAgentRoutes2 } from "./routes/agent.js";
27
+ import { createChatRoutes as createChatRoutes2 } from "./routes/chat.js";
25
28
  import { SessionManager as SessionManager2 } from "./session-manager.js";
26
29
  function snaPortRoute(c) {
27
30
  const portFile = _path.join(process.cwd(), ".sna/sna-api.port");
@@ -35,6 +38,7 @@ function snaPortRoute(c) {
35
38
  export {
36
39
  SessionManager2 as SessionManager,
37
40
  createAgentRoutes2 as createAgentRoutes,
41
+ createChatRoutes2 as createChatRoutes,
38
42
  createRunRoute2 as createRunRoute,
39
43
  createSnaApp,
40
44
  emitRoute2 as emitRoute,
@@ -4,6 +4,7 @@ import {
4
4
  getProvider
5
5
  } from "../../core/providers/index.js";
6
6
  import { logger } from "../../lib/logger.js";
7
+ import { getDb } from "../../db/schema.js";
7
8
  function getSessionId(c) {
8
9
  return c.req.query("session") ?? "default";
9
10
  }
@@ -55,12 +56,23 @@ function createAgentRoutes(sessionManager) {
55
56
  }
56
57
  session.eventBuffer.length = 0;
57
58
  const provider = getProvider(body.provider ?? "claude-code");
59
+ const skillMatch = body.prompt?.match(/^Execute the skill:\s*(\S+)/);
60
+ if (skillMatch) {
61
+ try {
62
+ const db = getDb();
63
+ db.prepare(
64
+ `INSERT INTO skill_events (session_id, skill, type, message) VALUES (?, ?, 'invoked', ?)`
65
+ ).run(sessionId, skillMatch[1], `Skill ${skillMatch[1]} invoked`);
66
+ } catch {
67
+ }
68
+ }
58
69
  try {
59
70
  const proc = provider.spawn({
60
71
  cwd: session.cwd,
61
72
  prompt: body.prompt,
62
73
  model: body.model ?? "claude-sonnet-4-6",
63
- permissionMode: body.permissionMode ?? "acceptEdits"
74
+ permissionMode: body.permissionMode ?? "acceptEdits",
75
+ env: { SNA_SESSION_ID: sessionId }
64
76
  });
65
77
  sessionManager.setProcess(sessionId, proc);
66
78
  logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
@@ -0,0 +1,6 @@
1
+ import * as hono_types from 'hono/types';
2
+ import { Hono } from 'hono';
3
+
4
+ declare function createChatRoutes(): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
5
+
6
+ export { createChatRoutes };
@@ -0,0 +1,67 @@
1
+ import { Hono } from "hono";
2
+ import { getDb } from "../../db/schema.js";
3
+ function createChatRoutes() {
4
+ const app = new Hono();
5
+ app.get("/sessions", (c) => {
6
+ const db = getDb();
7
+ const sessions = db.prepare(
8
+ `SELECT id, label, type, created_at FROM chat_sessions ORDER BY created_at DESC`
9
+ ).all();
10
+ return c.json({ sessions });
11
+ });
12
+ app.post("/sessions", async (c) => {
13
+ const body = await c.req.json().catch(() => ({}));
14
+ const id = body.id ?? crypto.randomUUID().slice(0, 8);
15
+ const db = getDb();
16
+ db.prepare(
17
+ `INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, ?)`
18
+ ).run(id, body.label ?? id, body.type ?? "background");
19
+ return c.json({ status: "created", id });
20
+ });
21
+ app.delete("/sessions/:id", (c) => {
22
+ const id = c.req.param("id");
23
+ if (id === "default") {
24
+ return c.json({ status: "error", message: "Cannot delete default session" }, 400);
25
+ }
26
+ const db = getDb();
27
+ db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
28
+ return c.json({ status: "deleted" });
29
+ });
30
+ app.get("/sessions/:id/messages", (c) => {
31
+ const id = c.req.param("id");
32
+ const sinceParam = c.req.query("since");
33
+ const db = getDb();
34
+ const query = sinceParam ? db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? AND id > ? ORDER BY id ASC`) : db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? ORDER BY id ASC`);
35
+ const messages = sinceParam ? query.all(id, parseInt(sinceParam, 10)) : query.all(id);
36
+ return c.json({ messages });
37
+ });
38
+ app.post("/sessions/:id/messages", async (c) => {
39
+ const sessionId = c.req.param("id");
40
+ const body = await c.req.json().catch(() => ({}));
41
+ if (!body.role) {
42
+ return c.json({ status: "error", message: "role is required" }, 400);
43
+ }
44
+ const db = getDb();
45
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, sessionId);
46
+ const result = db.prepare(
47
+ `INSERT INTO chat_messages (session_id, role, content, skill_name, meta) VALUES (?, ?, ?, ?, ?)`
48
+ ).run(
49
+ sessionId,
50
+ body.role,
51
+ body.content ?? "",
52
+ body.skill_name ?? null,
53
+ body.meta ? JSON.stringify(body.meta) : null
54
+ );
55
+ return c.json({ status: "created", id: result.lastInsertRowid });
56
+ });
57
+ app.delete("/sessions/:id/messages", (c) => {
58
+ const id = c.req.param("id");
59
+ const db = getDb();
60
+ db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
61
+ return c.json({ status: "cleared" });
62
+ });
63
+ return app;
64
+ }
65
+ export {
66
+ createChatRoutes
67
+ };
@@ -5,7 +5,7 @@ const KEEPALIVE_INTERVAL_MS = 15e3;
5
5
  function eventsRoute(c) {
6
6
  const sinceParam = c.req.query("since");
7
7
  let lastId = sinceParam ? parseInt(sinceParam) : -1;
8
- if (lastId === -1) {
8
+ if (lastId <= 0) {
9
9
  const db = getDb();
10
10
  const row = db.prepare("SELECT MAX(id) as maxId FROM skill_events").get();
11
11
  lastId = row.maxId ?? 0;
@@ -1,11 +1,11 @@
1
1
  // src/server/standalone.ts
2
2
  import { serve } from "@hono/node-server";
3
- import { Hono as Hono3 } from "hono";
3
+ import { Hono as Hono4 } from "hono";
4
4
  import { cors } from "hono/cors";
5
5
  import chalk2 from "chalk";
6
6
 
7
7
  // src/server/index.ts
8
- import { Hono as Hono2 } from "hono";
8
+ import { Hono as Hono3 } from "hono";
9
9
 
10
10
  // src/server/routes/events.ts
11
11
  import { streamSSE } from "hono/streaming";
@@ -36,8 +36,31 @@ function migrateSkillEvents(db) {
36
36
  function initSchema(db) {
37
37
  migrateSkillEvents(db);
38
38
  db.exec(`
39
+ CREATE TABLE IF NOT EXISTS chat_sessions (
40
+ id TEXT PRIMARY KEY,
41
+ label TEXT NOT NULL DEFAULT '',
42
+ type TEXT NOT NULL DEFAULT 'main',
43
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
44
+ );
45
+
46
+ -- Ensure default session always exists
47
+ INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES ('default', 'Chat', 'main');
48
+
49
+ CREATE TABLE IF NOT EXISTS chat_messages (
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ session_id TEXT NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
52
+ role TEXT NOT NULL,
53
+ content TEXT NOT NULL DEFAULT '',
54
+ skill_name TEXT,
55
+ meta TEXT,
56
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id);
60
+
39
61
  CREATE TABLE IF NOT EXISTS skill_events (
40
62
  id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ session_id TEXT REFERENCES chat_sessions(id) ON DELETE SET NULL,
41
64
  skill TEXT NOT NULL,
42
65
  type TEXT NOT NULL,
43
66
  message TEXT NOT NULL,
@@ -47,6 +70,7 @@ function initSchema(db) {
47
70
 
48
71
  CREATE INDEX IF NOT EXISTS idx_skill_events_skill ON skill_events(skill);
49
72
  CREATE INDEX IF NOT EXISTS idx_skill_events_created ON skill_events(created_at);
73
+ CREATE INDEX IF NOT EXISTS idx_skill_events_session ON skill_events(session_id);
50
74
  `);
51
75
  }
52
76
 
@@ -56,7 +80,7 @@ var KEEPALIVE_INTERVAL_MS = 15e3;
56
80
  function eventsRoute(c) {
57
81
  const sinceParam = c.req.query("since");
58
82
  let lastId = sinceParam ? parseInt(sinceParam) : -1;
59
- if (lastId === -1) {
83
+ if (lastId <= 0) {
60
84
  const db = getDb();
61
85
  const row = db.prepare("SELECT MAX(id) as maxId FROM skill_events").get();
62
86
  lastId = row.maxId ?? 0;
@@ -395,21 +419,23 @@ var ClaudeCodeProcess = class {
395
419
  }
396
420
  case "result": {
397
421
  if (msg.subtype === "success") {
422
+ const u = msg.usage ?? {};
398
423
  const mu = msg.modelUsage ?? {};
399
424
  const modelKey = Object.keys(mu)[0] ?? "";
400
- const u = mu[modelKey] ?? {};
425
+ const modelInfo = mu[modelKey] ?? {};
401
426
  return {
402
427
  type: "complete",
403
428
  message: msg.result ?? "Done",
404
429
  data: {
405
430
  durationMs: msg.duration_ms,
406
431
  costUsd: msg.total_cost_usd,
407
- inputTokens: u.inputTokens ?? 0,
408
- outputTokens: u.outputTokens ?? 0,
409
- cacheReadTokens: u.cacheReadInputTokens ?? 0,
410
- cacheWriteTokens: u.cacheCreationInputTokens ?? 0,
411
- contextWindow: u.contextWindow ?? 0,
412
- maxOutputTokens: u.maxOutputTokens ?? 0,
432
+ // Per-turn: actual context window usage this turn
433
+ inputTokens: u.input_tokens ?? 0,
434
+ outputTokens: u.output_tokens ?? 0,
435
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
436
+ cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
437
+ // Static model info
438
+ contextWindow: modelInfo.contextWindow ?? 0,
413
439
  model: modelKey
414
440
  },
415
441
  timestamp: Date.now()
@@ -550,12 +576,23 @@ function createAgentRoutes(sessionManager2) {
550
576
  }
551
577
  session.eventBuffer.length = 0;
552
578
  const provider2 = getProvider(body.provider ?? "claude-code");
579
+ const skillMatch = body.prompt?.match(/^Execute the skill:\s*(\S+)/);
580
+ if (skillMatch) {
581
+ try {
582
+ const db = getDb();
583
+ db.prepare(
584
+ `INSERT INTO skill_events (session_id, skill, type, message) VALUES (?, ?, 'invoked', ?)`
585
+ ).run(sessionId, skillMatch[1], `Skill ${skillMatch[1]} invoked`);
586
+ } catch {
587
+ }
588
+ }
553
589
  try {
554
590
  const proc = provider2.spawn({
555
591
  cwd: session.cwd,
556
592
  prompt: body.prompt,
557
593
  model: body.model ?? "claude-sonnet-4-6",
558
- permissionMode: body.permissionMode ?? "acceptEdits"
594
+ permissionMode: body.permissionMode ?? "acceptEdits",
595
+ env: { SNA_SESSION_ID: sessionId }
559
596
  });
560
597
  sessionManager2.setProcess(sessionId, proc);
561
598
  logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
@@ -639,6 +676,71 @@ function createAgentRoutes(sessionManager2) {
639
676
  return app;
640
677
  }
641
678
 
679
+ // src/server/routes/chat.ts
680
+ import { Hono as Hono2 } from "hono";
681
+ function createChatRoutes() {
682
+ const app = new Hono2();
683
+ app.get("/sessions", (c) => {
684
+ const db = getDb();
685
+ const sessions = db.prepare(
686
+ `SELECT id, label, type, created_at FROM chat_sessions ORDER BY created_at DESC`
687
+ ).all();
688
+ return c.json({ sessions });
689
+ });
690
+ app.post("/sessions", async (c) => {
691
+ const body = await c.req.json().catch(() => ({}));
692
+ const id = body.id ?? crypto.randomUUID().slice(0, 8);
693
+ const db = getDb();
694
+ db.prepare(
695
+ `INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, ?)`
696
+ ).run(id, body.label ?? id, body.type ?? "background");
697
+ return c.json({ status: "created", id });
698
+ });
699
+ app.delete("/sessions/:id", (c) => {
700
+ const id = c.req.param("id");
701
+ if (id === "default") {
702
+ return c.json({ status: "error", message: "Cannot delete default session" }, 400);
703
+ }
704
+ const db = getDb();
705
+ db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
706
+ return c.json({ status: "deleted" });
707
+ });
708
+ app.get("/sessions/:id/messages", (c) => {
709
+ const id = c.req.param("id");
710
+ const sinceParam = c.req.query("since");
711
+ const db = getDb();
712
+ const query = sinceParam ? db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? AND id > ? ORDER BY id ASC`) : db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? ORDER BY id ASC`);
713
+ const messages = sinceParam ? query.all(id, parseInt(sinceParam, 10)) : query.all(id);
714
+ return c.json({ messages });
715
+ });
716
+ app.post("/sessions/:id/messages", async (c) => {
717
+ const sessionId = c.req.param("id");
718
+ const body = await c.req.json().catch(() => ({}));
719
+ if (!body.role) {
720
+ return c.json({ status: "error", message: "role is required" }, 400);
721
+ }
722
+ const db = getDb();
723
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, sessionId);
724
+ const result = db.prepare(
725
+ `INSERT INTO chat_messages (session_id, role, content, skill_name, meta) VALUES (?, ?, ?, ?, ?)`
726
+ ).run(
727
+ sessionId,
728
+ body.role,
729
+ body.content ?? "",
730
+ body.skill_name ?? null,
731
+ body.meta ? JSON.stringify(body.meta) : null
732
+ );
733
+ return c.json({ status: "created", id: result.lastInsertRowid });
734
+ });
735
+ app.delete("/sessions/:id/messages", (c) => {
736
+ const id = c.req.param("id");
737
+ const db = getDb();
738
+ db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
739
+ return c.json({ status: "cleared" });
740
+ });
741
+ return app;
742
+ }
743
+
642
744
  // src/server/session-manager.ts
643
745
  var DEFAULT_MAX_SESSIONS = 5;
644
746
  var MAX_EVENT_BUFFER = 500;
@@ -742,11 +844,12 @@ var SessionManager = class {
742
844
  // src/server/index.ts
743
845
  function createSnaApp(options = {}) {
744
846
  const sessionManager2 = options.sessionManager ?? new SessionManager();
745
- const app = new Hono2();
847
+ const app = new Hono3();
746
848
  app.get("/health", (c) => c.json({ ok: true, name: "sna", version: "1" }));
747
849
  app.get("/events", eventsRoute);
748
850
  app.post("/emit", emitRoute);
749
851
  app.route("/agent", createAgentRoutes(sessionManager2));
852
+ app.route("/chat", createChatRoutes());
750
853
  if (options.runCommands) {
751
854
  app.get("/run", createRunRoute(options.runCommands));
752
855
  }
@@ -758,7 +861,7 @@ var port = parseInt(process.env.SNA_PORT ?? "3099", 10);
758
861
  var permissionMode = process.env.SNA_PERMISSION_MODE ?? "acceptEdits";
759
862
  var defaultModel = process.env.SNA_MODEL ?? "claude-sonnet-4-6";
760
863
  var maxSessions = parseInt(process.env.SNA_MAX_SESSIONS ?? "5", 10);
761
- var root = new Hono3();
864
+ var root = new Hono4();
762
865
  root.use("*", cors({ origin: "*", allowMethods: ["GET", "POST", "DELETE", "OPTIONS"] }));
763
866
  var methodColor = {
764
867
  GET: chalk2.green,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sna-sdk/core",
3
- "version": "0.0.5",
3
+ "version": "0.0.10",
4
4
  "description": "Skills-Native Application runtime — server, providers, session management, database, and CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,11 @@
44
44
  "types": "./dist/server/routes/agent.d.ts",
45
45
  "default": "./dist/server/routes/agent.js"
46
46
  },
47
+ "./server/routes/chat": {
48
+ "source": "./src/server/routes/chat.ts",
49
+ "types": "./dist/server/routes/chat.d.ts",
50
+ "default": "./dist/server/routes/chat.js"
51
+ },
47
52
  "./db/schema": {
48
53
  "source": "./src/db/schema.ts",
49
54
  "types": "./dist/db/schema.d.ts",
@@ -60,12 +65,6 @@
60
65
  "default": "./dist/lib/sna-run.js"
61
66
  }
62
67
  },
63
- "scripts": {
64
- "build": "tsup",
65
- "dev": "tsx --watch src/server/standalone.ts || true",
66
- "test": "node --import tsx --test test/**/*.test.ts",
67
- "prepublishOnly": "pnpm build"
68
- },
69
68
  "engines": {
70
69
  "node": ">=18.0.0"
71
70
  },
@@ -96,5 +95,10 @@
96
95
  "tsup": "^8.0.0",
97
96
  "tsx": "^4.0.0",
98
97
  "typescript": "^5.0.0"
98
+ },
99
+ "scripts": {
100
+ "build": "tsup",
101
+ "dev": "tsx --watch src/server/standalone.ts || true",
102
+ "test": "node --import tsx --test test/**/*.test.ts"
99
103
  }
100
- }
104
+ }