@sna-sdk/core 0.0.10 → 0.1.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.
- package/README.md +26 -3
- package/dist/core/providers/claude-code.js +40 -2
- package/dist/core/providers/types.d.ts +5 -0
- package/dist/db/schema.js +5 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/lib/dispatch.d.ts +77 -0
- package/dist/lib/dispatch.js +159 -0
- package/dist/lib/parse-flags.d.ts +12 -0
- package/dist/lib/parse-flags.js +20 -0
- package/dist/scripts/emit.js +25 -47
- package/dist/scripts/gen-client.js +10 -8
- package/dist/scripts/hook.js +52 -25
- package/dist/scripts/sna.js +142 -16
- package/dist/scripts/workflow.js +22 -36
- package/dist/server/routes/agent.js +75 -6
- package/dist/server/routes/chat.js +56 -32
- package/dist/server/session-manager.d.ts +6 -1
- package/dist/server/session-manager.js +44 -2
- package/dist/server/standalone.js +244 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,10 @@ Server runtime for [Skills-Native Applications](https://github.com/neuradex/sna)
|
|
|
5
5
|
## What's included
|
|
6
6
|
|
|
7
7
|
- **Skill event pipeline** — emit, SSE streaming, and hook scripts
|
|
8
|
+
- **Dispatch** — unified event dispatcher with validation, session lifecycle, and cleanup (`sna dispatch` CLI + programmatic API)
|
|
8
9
|
- **SQLite database** — schema and `getDb()` for `skill_events`
|
|
9
10
|
- **Hono server factory** — `createSnaApp()` with events, emit, agent, and run routes
|
|
10
|
-
- **Lifecycle CLI** — `sna api:up`, `sna api:down`
|
|
11
|
+
- **Lifecycle CLI** — `sna api:up`, `sna api:down`, `sna dispatch`, `sna validate`
|
|
11
12
|
- **Agent providers** — Claude Code and Codex process management
|
|
12
13
|
|
|
13
14
|
## Install
|
|
@@ -18,7 +19,29 @@ npm install @sna-sdk/core
|
|
|
18
19
|
|
|
19
20
|
## Usage
|
|
20
21
|
|
|
21
|
-
###
|
|
22
|
+
### Dispatch skill events (recommended)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# CLI
|
|
26
|
+
ID=$(sna dispatch open --skill my-skill)
|
|
27
|
+
sna dispatch $ID start --message "Starting..."
|
|
28
|
+
sna dispatch $ID milestone --message "Step done"
|
|
29
|
+
sna dispatch $ID close --message "Done."
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// Programmatic
|
|
34
|
+
import { createDispatchHandle } from "@sna-sdk/core";
|
|
35
|
+
|
|
36
|
+
const h = createDispatchHandle({ skill: "my-skill" });
|
|
37
|
+
h.start("Starting...");
|
|
38
|
+
h.milestone("Step done");
|
|
39
|
+
await h.close();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Emit skill events (legacy, deprecated)
|
|
43
|
+
|
|
44
|
+
> Use `sna dispatch` instead. `emit.js` remains for backward compatibility.
|
|
22
45
|
|
|
23
46
|
```bash
|
|
24
47
|
node node_modules/@sna-sdk/core/dist/scripts/emit.js \
|
|
@@ -48,7 +71,7 @@ const db = getDb(); // SQLite instance (data/sna.db)
|
|
|
48
71
|
|
|
49
72
|
| Import path | Contents |
|
|
50
73
|
|-------------|----------|
|
|
51
|
-
| `@sna-sdk/core` | `DEFAULT_SNA_PORT`, `DEFAULT_SNA_URL`, types |
|
|
74
|
+
| `@sna-sdk/core` | `DEFAULT_SNA_PORT`, `DEFAULT_SNA_URL`, `dispatchOpen`, `dispatchSend`, `dispatchClose`, `createDispatchHandle`, `SEND_TYPES`, `loadSkillsManifest`, types |
|
|
52
75
|
| `@sna-sdk/core/server` | `createSnaApp()`, route handlers, `SessionManager` |
|
|
53
76
|
| `@sna-sdk/core/db/schema` | `getDb()`, `SkillEvent` type |
|
|
54
77
|
| `@sna-sdk/core/providers` | Agent provider factory, `ClaudeCodeProvider` |
|
|
@@ -228,12 +228,47 @@ class ClaudeCodeProvider {
|
|
|
228
228
|
}
|
|
229
229
|
spawn(options) {
|
|
230
230
|
const claudePath = resolveClaudePath(options.cwd);
|
|
231
|
+
const hookScript = path.join(options.cwd, "node_modules/@sna-sdk/core/dist/scripts/hook.js");
|
|
232
|
+
const sdkSettings = {};
|
|
233
|
+
if (options.permissionMode !== "bypassPermissions") {
|
|
234
|
+
sdkSettings.hooks = {
|
|
235
|
+
PreToolUse: [{
|
|
236
|
+
matcher: ".*",
|
|
237
|
+
hooks: [{ type: "command", command: `node "${hookScript}"` }]
|
|
238
|
+
}]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
let extraArgsClean = options.extraArgs ? [...options.extraArgs] : [];
|
|
242
|
+
const settingsIdx = extraArgsClean.indexOf("--settings");
|
|
243
|
+
if (settingsIdx !== -1 && settingsIdx + 1 < extraArgsClean.length) {
|
|
244
|
+
try {
|
|
245
|
+
const appSettings = JSON.parse(extraArgsClean[settingsIdx + 1]);
|
|
246
|
+
if (appSettings.hooks) {
|
|
247
|
+
for (const [event, hooks] of Object.entries(appSettings.hooks)) {
|
|
248
|
+
if (sdkSettings.hooks && sdkSettings.hooks[event]) {
|
|
249
|
+
sdkSettings.hooks[event] = [
|
|
250
|
+
...sdkSettings.hooks[event],
|
|
251
|
+
...hooks
|
|
252
|
+
];
|
|
253
|
+
} else {
|
|
254
|
+
sdkSettings.hooks[event] = hooks;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
delete appSettings.hooks;
|
|
258
|
+
}
|
|
259
|
+
Object.assign(sdkSettings, appSettings);
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
extraArgsClean.splice(settingsIdx, 2);
|
|
263
|
+
}
|
|
231
264
|
const args = [
|
|
232
265
|
"--output-format",
|
|
233
266
|
"stream-json",
|
|
234
267
|
"--input-format",
|
|
235
268
|
"stream-json",
|
|
236
|
-
"--verbose"
|
|
269
|
+
"--verbose",
|
|
270
|
+
"--settings",
|
|
271
|
+
JSON.stringify(sdkSettings)
|
|
237
272
|
];
|
|
238
273
|
if (options.model) {
|
|
239
274
|
args.push("--model", options.model);
|
|
@@ -241,6 +276,9 @@ class ClaudeCodeProvider {
|
|
|
241
276
|
if (options.permissionMode) {
|
|
242
277
|
args.push("--permission-mode", options.permissionMode);
|
|
243
278
|
}
|
|
279
|
+
if (extraArgsClean.length > 0) {
|
|
280
|
+
args.push(...extraArgsClean);
|
|
281
|
+
}
|
|
244
282
|
const cleanEnv = { ...process.env, ...options.env };
|
|
245
283
|
delete cleanEnv.CLAUDECODE;
|
|
246
284
|
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
@@ -250,7 +288,7 @@ class ClaudeCodeProvider {
|
|
|
250
288
|
env: cleanEnv,
|
|
251
289
|
stdio: ["pipe", "pipe", "pipe"]
|
|
252
290
|
});
|
|
253
|
-
logger.log("agent", `spawned claude-code (pid=${proc.pid})`);
|
|
291
|
+
logger.log("agent", `spawned claude-code (pid=${proc.pid}) \u2192 ${claudePath} ${args.join(" ")}`);
|
|
254
292
|
return new ClaudeCodeProcess(proc, options);
|
|
255
293
|
}
|
|
256
294
|
}
|
|
@@ -36,6 +36,11 @@ interface SpawnOptions {
|
|
|
36
36
|
model?: string;
|
|
37
37
|
permissionMode?: "default" | "acceptEdits" | "bypassPermissions" | "plan";
|
|
38
38
|
env?: Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* Additional CLI flags passed directly to the agent binary.
|
|
41
|
+
* e.g. ["--system-prompt", "You are...", "--append-system-prompt", "Also...", "--mcp-config", "path"]
|
|
42
|
+
*/
|
|
43
|
+
extraArgs?: string[];
|
|
39
44
|
}
|
|
40
45
|
/**
|
|
41
46
|
* Agent provider interface. Each backend (Claude Code, Codex, etc.)
|
package/dist/db/schema.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import path from "path";
|
|
3
|
-
const require2 = createRequire(path.join(process.cwd(), "node_modules", "_"));
|
|
4
|
-
const BetterSqlite3 = require2("better-sqlite3");
|
|
5
4
|
const DB_PATH = path.join(process.cwd(), "data/sna.db");
|
|
6
5
|
let _db = null;
|
|
7
6
|
function getDb() {
|
|
8
7
|
if (!_db) {
|
|
8
|
+
const req = createRequire(import.meta.url);
|
|
9
|
+
const BetterSqlite3 = req("better-sqlite3");
|
|
10
|
+
const dir = path.dirname(DB_PATH);
|
|
11
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
9
12
|
_db = new BetterSqlite3(DB_PATH);
|
|
10
13
|
_db.pragma("journal_mode = WAL");
|
|
11
14
|
initSchema(_db);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { ChatMessage, ChatSession, SkillEvent } from './db/schema.js';
|
|
2
2
|
export { AgentEvent, AgentProcess, AgentProvider, SpawnOptions } from './core/providers/types.js';
|
|
3
|
-
export { Session, SessionInfo, SessionManagerOptions } from './server/session-manager.js';
|
|
3
|
+
export { Session, SessionInfo, SessionManagerOptions, SessionState } from './server/session-manager.js';
|
|
4
|
+
export { DispatchCloseOptions, DispatchEventType, DispatchOpenOptions, DispatchOpenResult, DispatchSendOptions, createHandle as createDispatchHandle, close as dispatchClose, open as dispatchOpen, send as dispatchSend } from './lib/dispatch.js';
|
|
4
5
|
import 'better-sqlite3';
|
|
5
6
|
|
|
6
7
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const DEFAULT_SNA_PORT = 3099;
|
|
2
2
|
const DEFAULT_SNA_URL = `http://localhost:${DEFAULT_SNA_PORT}`;
|
|
3
|
+
import { open, send, close, createHandle } from "./lib/dispatch.js";
|
|
3
4
|
export {
|
|
4
5
|
DEFAULT_SNA_PORT,
|
|
5
|
-
DEFAULT_SNA_URL
|
|
6
|
+
DEFAULT_SNA_URL,
|
|
7
|
+
createHandle as createDispatchHandle,
|
|
8
|
+
close as dispatchClose,
|
|
9
|
+
open as dispatchOpen,
|
|
10
|
+
send as dispatchSend
|
|
6
11
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatch.ts — Unified event dispatcher for SNA.
|
|
3
|
+
*
|
|
4
|
+
* Single entry point for all skill lifecycle events.
|
|
5
|
+
* Used by both CLI (`sna dispatch`) and SDK (programmatic).
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle:
|
|
8
|
+
* dispatch.open({ skill }) → id (validate + create session, no event written)
|
|
9
|
+
* dispatch.send(id, { type, message }) (write event to DB)
|
|
10
|
+
* dispatch.close(id) (complete + kill session)
|
|
11
|
+
* dispatch.close(id, { error }) (error + kill session)
|
|
12
|
+
*
|
|
13
|
+
* Responsibilities:
|
|
14
|
+
* - Validate skill name against .sna/skills.json (fallback: SKILL.md existence)
|
|
15
|
+
* - Write events to SQLite (skill_events table)
|
|
16
|
+
* - On close: notify SNA API server to kill background session
|
|
17
|
+
*/
|
|
18
|
+
interface DispatchOpenOptions {
|
|
19
|
+
skill: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
}
|
|
23
|
+
interface DispatchOpenResult {
|
|
24
|
+
id: string;
|
|
25
|
+
skill: string;
|
|
26
|
+
sessionId: string | null;
|
|
27
|
+
}
|
|
28
|
+
type DispatchEventType = "called" | "start" | "progress" | "milestone" | "permission_needed";
|
|
29
|
+
interface DispatchSendOptions {
|
|
30
|
+
type: DispatchEventType;
|
|
31
|
+
message: string;
|
|
32
|
+
data?: string;
|
|
33
|
+
}
|
|
34
|
+
interface DispatchCloseOptions {
|
|
35
|
+
error?: string;
|
|
36
|
+
message?: string;
|
|
37
|
+
}
|
|
38
|
+
interface DispatchSession {
|
|
39
|
+
id: string;
|
|
40
|
+
skill: string;
|
|
41
|
+
sessionId: string | null;
|
|
42
|
+
cwd: string;
|
|
43
|
+
closed: boolean;
|
|
44
|
+
}
|
|
45
|
+
declare const SEND_TYPES: readonly string[];
|
|
46
|
+
declare function loadSkillsManifest(cwd: string): Record<string, unknown> | null;
|
|
47
|
+
/**
|
|
48
|
+
* Open a dispatch session. Validates skill name, creates session.
|
|
49
|
+
* Does NOT write any event — caller decides what to send first.
|
|
50
|
+
*/
|
|
51
|
+
declare function open(opts: DispatchOpenOptions): DispatchOpenResult;
|
|
52
|
+
/**
|
|
53
|
+
* Send an event within an open dispatch session.
|
|
54
|
+
*/
|
|
55
|
+
declare function send(id: string, opts: DispatchSendOptions): void;
|
|
56
|
+
/**
|
|
57
|
+
* Close a dispatch session. Emits terminal events and triggers cleanup.
|
|
58
|
+
*/
|
|
59
|
+
declare function close(id: string, opts?: DispatchCloseOptions): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Get an active dispatch session (for internal inspection).
|
|
62
|
+
*/
|
|
63
|
+
declare function getSession(id: string): DispatchSession | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Convenience: create a dispatch handle with chainable methods.
|
|
66
|
+
*/
|
|
67
|
+
declare function createHandle(opts: DispatchOpenOptions): {
|
|
68
|
+
id: string;
|
|
69
|
+
skill: string;
|
|
70
|
+
called: (message: string) => void;
|
|
71
|
+
start: (message: string) => void;
|
|
72
|
+
progress: (message: string) => void;
|
|
73
|
+
milestone: (message: string) => void;
|
|
74
|
+
close: (closeOpts?: DispatchCloseOptions) => Promise<void>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export { type DispatchCloseOptions, type DispatchEventType, type DispatchOpenOptions, type DispatchOpenResult, type DispatchSendOptions, SEND_TYPES, close, createHandle, getSession, loadSkillsManifest, open, send };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { getDb } from "../db/schema.js";
|
|
5
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
6
|
+
const SEND_TYPES = [
|
|
7
|
+
"called",
|
|
8
|
+
"start",
|
|
9
|
+
"progress",
|
|
10
|
+
"milestone",
|
|
11
|
+
"permission_needed"
|
|
12
|
+
];
|
|
13
|
+
function loadSkillsManifest(cwd) {
|
|
14
|
+
const manifestPath = path.join(cwd, ".sna/skills.json");
|
|
15
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function skillMdExists(cwd, skill) {
|
|
23
|
+
return fs.existsSync(path.join(cwd, ".claude/skills", skill, "SKILL.md"));
|
|
24
|
+
}
|
|
25
|
+
function generateId() {
|
|
26
|
+
return crypto.randomBytes(4).toString("hex");
|
|
27
|
+
}
|
|
28
|
+
function writeEvent(sessionId, skill, type, message, data) {
|
|
29
|
+
const db = getDb();
|
|
30
|
+
db.prepare(`
|
|
31
|
+
INSERT INTO skill_events (session_id, skill, type, message, data)
|
|
32
|
+
VALUES (?, ?, ?, ?, ?)
|
|
33
|
+
`).run(sessionId, skill, type, message, data ?? null);
|
|
34
|
+
}
|
|
35
|
+
async function notifySessionClose(cwd, sessionId) {
|
|
36
|
+
if (!sessionId) return;
|
|
37
|
+
try {
|
|
38
|
+
const port = fs.readFileSync(path.join(cwd, ".sna/sna-api.port"), "utf8").trim();
|
|
39
|
+
if (!port) return;
|
|
40
|
+
await fetch(`http://localhost:${port}/agent/kill?session=${encodeURIComponent(sessionId)}`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
signal: AbortSignal.timeout(3e3)
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const PREFIX = {
|
|
48
|
+
called: "\u2192",
|
|
49
|
+
start: "\u25B6",
|
|
50
|
+
progress: "\xB7",
|
|
51
|
+
milestone: "\u25C6",
|
|
52
|
+
permission_needed: "\u26A0",
|
|
53
|
+
complete: "\u2713",
|
|
54
|
+
error: "\u2717",
|
|
55
|
+
success: "\u2713",
|
|
56
|
+
failed: "\u2717"
|
|
57
|
+
};
|
|
58
|
+
function log(skill, type, message) {
|
|
59
|
+
const p = PREFIX[type] ?? "\xB7";
|
|
60
|
+
console.log(`${p} [${skill}] ${message}`);
|
|
61
|
+
}
|
|
62
|
+
function open(opts) {
|
|
63
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
64
|
+
const manifest = loadSkillsManifest(cwd);
|
|
65
|
+
if (manifest) {
|
|
66
|
+
if (!(opts.skill in manifest)) {
|
|
67
|
+
if (skillMdExists(cwd, opts.skill)) {
|
|
68
|
+
console.warn(
|
|
69
|
+
`\u26A0 Skill "${opts.skill}" has SKILL.md but is not in .sna/skills.json \u2014 run 'sna gen client'`
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
const available = Object.keys(manifest).join(", ");
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Unknown skill: "${opts.skill}". Available: ${available}.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
if (!skillMdExists(cwd, opts.skill)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Unknown skill: "${opts.skill}". No .sna/skills.json and no SKILL.md found.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const id = generateId();
|
|
86
|
+
const sessionId = opts.sessionId ?? process.env.SNA_SESSION_ID ?? null;
|
|
87
|
+
const session = {
|
|
88
|
+
id,
|
|
89
|
+
skill: opts.skill,
|
|
90
|
+
sessionId,
|
|
91
|
+
cwd,
|
|
92
|
+
closed: false
|
|
93
|
+
};
|
|
94
|
+
activeSessions.set(id, session);
|
|
95
|
+
return { id, skill: opts.skill, sessionId };
|
|
96
|
+
}
|
|
97
|
+
function send(id, opts) {
|
|
98
|
+
const session = activeSessions.get(id);
|
|
99
|
+
if (!session) {
|
|
100
|
+
throw new Error(`Dispatch session "${id}" not found. Call dispatch.open() first.`);
|
|
101
|
+
}
|
|
102
|
+
if (session.closed) {
|
|
103
|
+
throw new Error(`Dispatch session "${id}" is already closed.`);
|
|
104
|
+
}
|
|
105
|
+
if (!SEND_TYPES.includes(opts.type)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Invalid event type: "${opts.type}". Must be one of: ${SEND_TYPES.join(", ")}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
writeEvent(session.sessionId, session.skill, opts.type, opts.message, opts.data);
|
|
111
|
+
log(session.skill, opts.type, opts.message);
|
|
112
|
+
}
|
|
113
|
+
async function close(id, opts) {
|
|
114
|
+
const session = activeSessions.get(id);
|
|
115
|
+
if (!session) {
|
|
116
|
+
throw new Error(`Dispatch session "${id}" not found.`);
|
|
117
|
+
}
|
|
118
|
+
if (session.closed) {
|
|
119
|
+
throw new Error(`Dispatch session "${id}" is already closed.`);
|
|
120
|
+
}
|
|
121
|
+
session.closed = true;
|
|
122
|
+
if (opts?.error) {
|
|
123
|
+
const message = opts.error;
|
|
124
|
+
writeEvent(session.sessionId, session.skill, "error", message);
|
|
125
|
+
writeEvent(session.sessionId, session.skill, "failed", message);
|
|
126
|
+
log(session.skill, "error", message);
|
|
127
|
+
} else {
|
|
128
|
+
const message = opts?.message ?? "Done";
|
|
129
|
+
writeEvent(session.sessionId, session.skill, "complete", message);
|
|
130
|
+
writeEvent(session.sessionId, session.skill, "success", message);
|
|
131
|
+
log(session.skill, "complete", message);
|
|
132
|
+
}
|
|
133
|
+
await notifySessionClose(session.cwd, session.sessionId);
|
|
134
|
+
activeSessions.delete(id);
|
|
135
|
+
}
|
|
136
|
+
function getSession(id) {
|
|
137
|
+
return activeSessions.get(id);
|
|
138
|
+
}
|
|
139
|
+
function createHandle(opts) {
|
|
140
|
+
const result = open(opts);
|
|
141
|
+
return {
|
|
142
|
+
id: result.id,
|
|
143
|
+
skill: result.skill,
|
|
144
|
+
called: (message) => send(result.id, { type: "called", message }),
|
|
145
|
+
start: (message) => send(result.id, { type: "start", message }),
|
|
146
|
+
progress: (message) => send(result.id, { type: "progress", message }),
|
|
147
|
+
milestone: (message) => send(result.id, { type: "milestone", message }),
|
|
148
|
+
close: (closeOpts) => close(result.id, closeOpts)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
SEND_TYPES,
|
|
153
|
+
close,
|
|
154
|
+
createHandle,
|
|
155
|
+
getSession,
|
|
156
|
+
loadSkillsManifest,
|
|
157
|
+
open,
|
|
158
|
+
send
|
|
159
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parse-flags.ts — Shared CLI flag parser for SNA scripts.
|
|
3
|
+
*
|
|
4
|
+
* Parses --key value pairs from argv-style arrays.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Parse --key value pairs from an argument array.
|
|
8
|
+
* Handles --flag (without value) by setting it to "true".
|
|
9
|
+
*/
|
|
10
|
+
declare function parseFlags(args: string[]): Record<string, string>;
|
|
11
|
+
|
|
12
|
+
export { parseFlags };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function parseFlags(args) {
|
|
2
|
+
const result = {};
|
|
3
|
+
for (let i = 0; i < args.length; i++) {
|
|
4
|
+
const arg = args[i];
|
|
5
|
+
if (arg?.startsWith("--")) {
|
|
6
|
+
const key = arg.slice(2);
|
|
7
|
+
const next = args[i + 1];
|
|
8
|
+
if (next && !next.startsWith("--")) {
|
|
9
|
+
result[key] = next;
|
|
10
|
+
i++;
|
|
11
|
+
} else {
|
|
12
|
+
result[key] = "true";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
parseFlags
|
|
20
|
+
};
|
package/dist/scripts/emit.js
CHANGED
|
@@ -1,51 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const result = {};
|
|
4
|
-
for (let i = 0; i < args2.length; i += 2) {
|
|
5
|
-
const key = args2[i]?.replace(/^--/, "");
|
|
6
|
-
if (key) result[key] = args2[i + 1] ?? "";
|
|
7
|
-
}
|
|
8
|
-
return result;
|
|
9
|
-
}
|
|
1
|
+
import { open, send, close, SEND_TYPES } from "../lib/dispatch.js";
|
|
2
|
+
import { parseFlags } from "../lib/parse-flags.js";
|
|
10
3
|
const [, , ...args] = process.argv;
|
|
11
|
-
const flags =
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
"success",
|
|
15
|
-
"failed",
|
|
16
|
-
"permission_needed",
|
|
17
|
-
"start",
|
|
18
|
-
"progress",
|
|
19
|
-
"milestone",
|
|
20
|
-
"complete",
|
|
21
|
-
"error"
|
|
22
|
-
];
|
|
4
|
+
const flags = parseFlags(args);
|
|
5
|
+
const CLOSE_SUCCESS_TYPES = ["complete", "success"];
|
|
6
|
+
const CLOSE_ERROR_TYPES = ["error", "failed"];
|
|
23
7
|
if (!flags.skill || !flags.type || !flags.message) {
|
|
24
|
-
console.error("
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
if (!VALID_TYPES.includes(flags.type)) {
|
|
28
|
-
console.error(`Invalid type: ${flags.type}. Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
8
|
+
console.error("DEPRECATED: Use 'sna dispatch' instead.");
|
|
9
|
+
console.error("Usage: node emit.js --skill <name> --type <type> --message <text>");
|
|
29
10
|
process.exit(1);
|
|
30
11
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
};
|
|
50
|
-
const p = prefix[flags.type] ?? "\xB7";
|
|
51
|
-
console.log(`${p} [${flags.skill}] ${flags.message}`);
|
|
12
|
+
(async () => {
|
|
13
|
+
try {
|
|
14
|
+
const d = open({ skill: flags.skill });
|
|
15
|
+
if (SEND_TYPES.includes(flags.type)) {
|
|
16
|
+
send(d.id, { type: flags.type, message: flags.message, data: flags.data });
|
|
17
|
+
} else if (CLOSE_SUCCESS_TYPES.includes(flags.type)) {
|
|
18
|
+
await close(d.id, { message: flags.message });
|
|
19
|
+
} else if (CLOSE_ERROR_TYPES.includes(flags.type)) {
|
|
20
|
+
await close(d.id, { error: flags.message });
|
|
21
|
+
} else {
|
|
22
|
+
console.error(`Unknown type: ${flags.type}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error(`\u2717 ${err.message}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { scanSkills } from "../lib/skill-parser.js";
|
|
4
|
+
import { parseFlags } from "../lib/parse-flags.js";
|
|
4
5
|
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
6
|
function tsType(argDef) {
|
|
14
7
|
switch (argDef.type) {
|
|
15
8
|
case "number":
|
|
@@ -106,7 +99,16 @@ const code = generateClient(schemas);
|
|
|
106
99
|
const outDir = path.dirname(outPath);
|
|
107
100
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
108
101
|
fs.writeFileSync(outPath, code);
|
|
102
|
+
const snaDir = path.join(ROOT, ".sna");
|
|
103
|
+
if (!fs.existsSync(snaDir)) fs.mkdirSync(snaDir, { recursive: true });
|
|
104
|
+
const skillsManifest = {};
|
|
105
|
+
for (const s of schemas) {
|
|
106
|
+
skillsManifest[s.name] = { description: s.description, args: s.args };
|
|
107
|
+
}
|
|
108
|
+
const manifestPath = path.join(snaDir, "skills.json");
|
|
109
|
+
fs.writeFileSync(manifestPath, JSON.stringify(skillsManifest, null, 2) + "\n");
|
|
109
110
|
console.log(`\u2713 Generated ${outPath}`);
|
|
111
|
+
console.log(`\u2713 Generated ${manifestPath}`);
|
|
110
112
|
console.log(` ${schemas.length} skills:`);
|
|
111
113
|
for (const s of schemas) {
|
|
112
114
|
const argCount = Object.keys(s.args).length;
|
package/dist/scripts/hook.js
CHANGED
|
@@ -1,34 +1,61 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
2
3
|
const chunks = [];
|
|
3
4
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
4
|
-
process.stdin.on("end", () => {
|
|
5
|
+
process.stdin.on("end", async () => {
|
|
5
6
|
try {
|
|
6
7
|
const raw = Buffer.concat(chunks).toString().trim();
|
|
7
|
-
if (!raw)
|
|
8
|
+
if (!raw) {
|
|
9
|
+
allow();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
const input = JSON.parse(raw);
|
|
9
13
|
const toolName = input.tool_name ?? "unknown";
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
14
|
+
const safeTools = ["Read", "Glob", "Grep", "Agent", "TodoRead", "TodoWrite"];
|
|
15
|
+
if (safeTools.includes(toolName)) {
|
|
16
|
+
allow();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const portFile = path.join(process.cwd(), ".sna/sna-api.port");
|
|
20
|
+
let port;
|
|
21
|
+
try {
|
|
22
|
+
port = fs.readFileSync(portFile, "utf8").trim();
|
|
23
|
+
} catch {
|
|
24
|
+
allow();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const sessionId = process.env.SNA_SESSION_ID ?? "default";
|
|
28
|
+
const apiUrl = `http://localhost:${port}`;
|
|
29
|
+
const res = await fetch(`${apiUrl}/agent/permission-request?session=${encodeURIComponent(sessionId)}`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
tool_name: input.tool_name,
|
|
34
|
+
tool_input: input.tool_input
|
|
35
|
+
}),
|
|
36
|
+
signal: AbortSignal.timeout(3e5)
|
|
37
|
+
// 5 min timeout
|
|
38
|
+
});
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
if (data.approved) {
|
|
41
|
+
allow();
|
|
42
|
+
} else {
|
|
43
|
+
deny("User denied this tool execution");
|
|
44
|
+
}
|
|
31
45
|
} catch {
|
|
46
|
+
allow();
|
|
32
47
|
}
|
|
33
|
-
process.exit(0);
|
|
34
48
|
});
|
|
49
|
+
function allow() {
|
|
50
|
+
console.log(JSON.stringify({
|
|
51
|
+
hookSpecificOutput: {
|
|
52
|
+
hookEventName: "PreToolUse",
|
|
53
|
+
permissionDecision: "allow"
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
function deny(reason) {
|
|
59
|
+
process.stderr.write(reason);
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|