@sna-sdk/core 0.3.0 → 0.5.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 +6 -0
- package/dist/core/providers/cc-history-adapter.d.ts +37 -0
- package/dist/core/providers/cc-history-adapter.js +70 -0
- package/dist/core/providers/claude-code.js +50 -27
- package/dist/core/providers/types.d.ts +7 -1
- package/dist/db/schema.js +1 -1
- package/dist/scripts/sna.js +20 -2
- package/dist/scripts/tu-oneshot.d.ts +2 -0
- package/dist/scripts/tu-oneshot.js +66 -0
- package/dist/server/api-types.d.ts +13 -0
- package/dist/server/history-builder.d.ts +16 -0
- package/dist/server/history-builder.js +25 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -0
- package/dist/server/routes/agent.js +105 -17
- package/dist/server/session-manager.d.ts +23 -3
- package/dist/server/session-manager.js +67 -13
- package/dist/server/standalone.js +442 -87
- package/dist/server/ws.js +107 -5
- package/dist/testing/mock-api.js +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Server runtime for [Skills-Native Applications](https://github.com/neuradex/sna)
|
|
|
9
9
|
- **SQLite database** — schema and `getDb()` for `skill_events`, `chat_sessions`, `chat_messages`
|
|
10
10
|
- **Hono server factory** — `createSnaApp()` with events, emit, agent, chat, and run routes
|
|
11
11
|
- **WebSocket API** — `attachWebSocket()` wrapping all HTTP routes over a single WS connection
|
|
12
|
+
- **History management** — `agent.resume` auto-loads DB history, `agent.subscribe({ since: 0 })` unified history+realtime channel
|
|
12
13
|
- **One-shot execution** — `POST /agent/run-once` for single-request LLM calls
|
|
13
14
|
- **CLI** — `sna up/down/status`, `sna dispatch`, `sna gen client`, `sna tu` (mock API testing)
|
|
14
15
|
- **Agent providers** — Claude Code and Codex process management
|
|
@@ -86,6 +87,11 @@ const db = getDb(); // SQLite instance (data/sna.db)
|
|
|
86
87
|
| `@sna-sdk/core/lib/sna-run` | `snaRun()` helper for spawning Claude Code |
|
|
87
88
|
| `@sna-sdk/core/testing` | `startMockAnthropicServer()` for testing without real API calls |
|
|
88
89
|
|
|
90
|
+
**Environment Variables:**
|
|
91
|
+
- `SNA_DB_PATH` — Override SQLite database location (default: `process.cwd()/data/sna.db`)
|
|
92
|
+
- `SNA_CLAUDE_COMMAND` — Override claude binary path
|
|
93
|
+
- `SNA_PORT` — API server port (default: 3099)
|
|
94
|
+
|
|
89
95
|
## Documentation
|
|
90
96
|
|
|
91
97
|
- [Architecture](https://github.com/neuradex/sna/blob/main/docs/architecture.md)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { HistoryMessage } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* History injection for Claude Code via JSONL resume.
|
|
5
|
+
*
|
|
6
|
+
* Writes a JSONL session file and passes --resume <filepath> to CC.
|
|
7
|
+
* CC loads it as real multi-turn conversation history.
|
|
8
|
+
*
|
|
9
|
+
* Key discovery: --resume with a .jsonl file path bypasses CC's project
|
|
10
|
+
* directory lookup and calls loadMessagesFromJsonlPath directly.
|
|
11
|
+
* This is the only reliable way to inject synthetic history.
|
|
12
|
+
*
|
|
13
|
+
* Verified: real Claude Haiku correctly recalls injected context.
|
|
14
|
+
* Fallback: recalled-conversation XML if file write fails.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Write a JSONL session file for --resume <filepath>.
|
|
19
|
+
*
|
|
20
|
+
* Minimal format (verified working):
|
|
21
|
+
* {"parentUuid":null,"isSidechain":false,"type":"user","uuid":"...","timestamp":"...","cwd":"...","sessionId":"...","message":{"role":"user","content":"..."}}
|
|
22
|
+
* {"parentUuid":"<prev>","isSidechain":false,"type":"assistant","uuid":"...","timestamp":"...","cwd":"...","sessionId":"...","message":{"role":"assistant","content":[{"type":"text","text":"..."}]}}
|
|
23
|
+
*/
|
|
24
|
+
declare function writeHistoryJsonl(history: HistoryMessage[], opts: {
|
|
25
|
+
cwd: string;
|
|
26
|
+
}): {
|
|
27
|
+
filePath: string;
|
|
28
|
+
extraArgs: string[];
|
|
29
|
+
} | null;
|
|
30
|
+
/**
|
|
31
|
+
* Pack history into a single assistant stdin message.
|
|
32
|
+
* CC treats type:"assistant" as context injection (no API call triggered).
|
|
33
|
+
* Used when file write fails.
|
|
34
|
+
*/
|
|
35
|
+
declare function buildRecalledConversation(history: HistoryMessage[]): string;
|
|
36
|
+
|
|
37
|
+
export { buildRecalledConversation, writeHistoryJsonl };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
function writeHistoryJsonl(history, opts) {
|
|
4
|
+
for (let i = 1; i < history.length; i++) {
|
|
5
|
+
if (history[i].role === history[i - 1].role) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
`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.`
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const dir = path.join(opts.cwd, ".sna", "history");
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
const sessionId = crypto.randomUUID();
|
|
15
|
+
const filePath = path.join(dir, `${sessionId}.jsonl`);
|
|
16
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17
|
+
const lines = [];
|
|
18
|
+
let prevUuid = null;
|
|
19
|
+
for (const msg of history) {
|
|
20
|
+
const uuid = crypto.randomUUID();
|
|
21
|
+
if (msg.role === "user") {
|
|
22
|
+
lines.push(JSON.stringify({
|
|
23
|
+
parentUuid: prevUuid,
|
|
24
|
+
isSidechain: false,
|
|
25
|
+
type: "user",
|
|
26
|
+
uuid,
|
|
27
|
+
timestamp: now,
|
|
28
|
+
cwd: opts.cwd,
|
|
29
|
+
sessionId,
|
|
30
|
+
message: { role: "user", content: msg.content }
|
|
31
|
+
}));
|
|
32
|
+
} else {
|
|
33
|
+
lines.push(JSON.stringify({
|
|
34
|
+
parentUuid: prevUuid,
|
|
35
|
+
isSidechain: false,
|
|
36
|
+
type: "assistant",
|
|
37
|
+
uuid,
|
|
38
|
+
timestamp: now,
|
|
39
|
+
cwd: opts.cwd,
|
|
40
|
+
sessionId,
|
|
41
|
+
message: {
|
|
42
|
+
role: "assistant",
|
|
43
|
+
content: [{ type: "text", text: msg.content }]
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
prevUuid = uuid;
|
|
48
|
+
}
|
|
49
|
+
fs.writeFileSync(filePath, lines.join("\n") + "\n");
|
|
50
|
+
return { filePath, extraArgs: ["--resume", filePath] };
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function buildRecalledConversation(history) {
|
|
56
|
+
const xml = history.map((msg) => `<${msg.role}>${msg.content}</${msg.role}>`).join("\n");
|
|
57
|
+
return JSON.stringify({
|
|
58
|
+
type: "assistant",
|
|
59
|
+
message: {
|
|
60
|
+
role: "assistant",
|
|
61
|
+
content: [{ type: "text", text: `<recalled-conversation>
|
|
62
|
+
${xml}
|
|
63
|
+
</recalled-conversation>` }]
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
buildRecalledConversation,
|
|
69
|
+
writeHistoryJsonl
|
|
70
|
+
};
|
|
@@ -2,6 +2,7 @@ import { spawn, execSync } from "child_process";
|
|
|
2
2
|
import { EventEmitter } from "events";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import { writeHistoryJsonl, buildRecalledConversation } from "./cc-history-adapter.js";
|
|
5
6
|
import { logger } from "../../lib/logger.js";
|
|
6
7
|
const SHELL = process.env.SHELL || "/bin/zsh";
|
|
7
8
|
function resolveClaudePath(cwd) {
|
|
@@ -79,33 +80,44 @@ class ClaudeCodeProcess {
|
|
|
79
80
|
this._alive = false;
|
|
80
81
|
this.emitter.emit("error", err);
|
|
81
82
|
});
|
|
82
|
-
if (options.history?.length) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
for (const msg of options.history) {
|
|
87
|
-
if (msg.role === "user") {
|
|
88
|
-
const line = JSON.stringify({
|
|
89
|
-
type: "user",
|
|
90
|
-
message: { role: "user", content: msg.content }
|
|
91
|
-
});
|
|
92
|
-
this.proc.stdin.write(line + "\n");
|
|
93
|
-
} else if (msg.role === "assistant") {
|
|
94
|
-
const line = JSON.stringify({
|
|
95
|
-
type: "assistant",
|
|
96
|
-
message: {
|
|
97
|
-
role: "assistant",
|
|
98
|
-
content: [{ type: "text", text: msg.content }]
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
this.proc.stdin.write(line + "\n");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
83
|
+
if (options.history?.length && !options._historyViaResume) {
|
|
84
|
+
const line = buildRecalledConversation(options.history);
|
|
85
|
+
this.proc.stdin.write(line + "\n");
|
|
104
86
|
}
|
|
105
87
|
if (options.prompt) {
|
|
106
88
|
this.send(options.prompt);
|
|
107
89
|
}
|
|
108
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Split completed assistant text into chunks and emit assistant_delta events
|
|
93
|
+
* at a fixed rate (~270 chars/sec), followed by the final assistant event.
|
|
94
|
+
*
|
|
95
|
+
* CHUNK_SIZE chars every CHUNK_DELAY_MS → natural TPS feel regardless of length.
|
|
96
|
+
*/
|
|
97
|
+
emitTextAsDeltas(text) {
|
|
98
|
+
const CHUNK_SIZE = 4;
|
|
99
|
+
const CHUNK_DELAY_MS = 15;
|
|
100
|
+
let t = 0;
|
|
101
|
+
for (let i = 0; i < text.length; i += CHUNK_SIZE) {
|
|
102
|
+
const chunk = text.slice(i, i + CHUNK_SIZE);
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
this.emitter.emit("event", {
|
|
105
|
+
type: "assistant_delta",
|
|
106
|
+
delta: chunk,
|
|
107
|
+
index: 0,
|
|
108
|
+
timestamp: Date.now()
|
|
109
|
+
});
|
|
110
|
+
}, t);
|
|
111
|
+
t += CHUNK_DELAY_MS;
|
|
112
|
+
}
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
this.emitter.emit("event", {
|
|
115
|
+
type: "assistant",
|
|
116
|
+
message: text,
|
|
117
|
+
timestamp: Date.now()
|
|
118
|
+
});
|
|
119
|
+
}, t);
|
|
120
|
+
}
|
|
109
121
|
get alive() {
|
|
110
122
|
return this._alive;
|
|
111
123
|
}
|
|
@@ -181,6 +193,7 @@ class ClaudeCodeProcess {
|
|
|
181
193
|
const content = msg.message?.content;
|
|
182
194
|
if (!Array.isArray(content)) return null;
|
|
183
195
|
const events = [];
|
|
196
|
+
const textBlocks = [];
|
|
184
197
|
for (const block of content) {
|
|
185
198
|
if (block.type === "thinking") {
|
|
186
199
|
events.push({
|
|
@@ -198,15 +211,17 @@ class ClaudeCodeProcess {
|
|
|
198
211
|
} else if (block.type === "text") {
|
|
199
212
|
const text = (block.text ?? "").trim();
|
|
200
213
|
if (text) {
|
|
201
|
-
|
|
214
|
+
textBlocks.push(text);
|
|
202
215
|
}
|
|
203
216
|
}
|
|
204
217
|
}
|
|
205
|
-
if (events.length > 0) {
|
|
206
|
-
for (
|
|
207
|
-
this.emitter.emit("event",
|
|
218
|
+
if (events.length > 0 || textBlocks.length > 0) {
|
|
219
|
+
for (const e of events) {
|
|
220
|
+
this.emitter.emit("event", e);
|
|
221
|
+
}
|
|
222
|
+
for (const text of textBlocks) {
|
|
223
|
+
this.emitTextAsDeltas(text);
|
|
208
224
|
}
|
|
209
|
-
return events[0];
|
|
210
225
|
}
|
|
211
226
|
return null;
|
|
212
227
|
}
|
|
@@ -341,6 +356,14 @@ class ClaudeCodeProvider {
|
|
|
341
356
|
if (options.permissionMode) {
|
|
342
357
|
args.push("--permission-mode", options.permissionMode);
|
|
343
358
|
}
|
|
359
|
+
if (options.history?.length && options.prompt) {
|
|
360
|
+
const result = writeHistoryJsonl(options.history, { cwd: options.cwd });
|
|
361
|
+
if (result) {
|
|
362
|
+
args.push(...result.extraArgs);
|
|
363
|
+
options._historyViaResume = true;
|
|
364
|
+
logger.log("agent", `history via JSONL resume \u2192 ${result.filePath}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
344
367
|
if (extraArgsClean.length > 0) {
|
|
345
368
|
args.push(...extraArgsClean);
|
|
346
369
|
}
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
* Codex JSONL, etc.) into these common types.
|
|
6
6
|
*/
|
|
7
7
|
interface AgentEvent {
|
|
8
|
-
type: "init" | "thinking" | "text_delta" | "assistant" | "tool_use" | "tool_result" | "permission_needed" | "milestone" | "interrupted" | "error" | "complete";
|
|
8
|
+
type: "init" | "thinking" | "text_delta" | "assistant_delta" | "assistant" | "tool_use" | "tool_result" | "permission_needed" | "milestone" | "user_message" | "interrupted" | "error" | "complete";
|
|
9
9
|
message?: string;
|
|
10
10
|
data?: Record<string, unknown>;
|
|
11
|
+
/** Streaming text delta (for assistant_delta events only) */
|
|
12
|
+
delta?: string;
|
|
13
|
+
/** Content block index (for assistant_delta events only) */
|
|
14
|
+
index?: number;
|
|
11
15
|
timestamp: number;
|
|
12
16
|
}
|
|
13
17
|
/**
|
|
@@ -63,6 +67,8 @@ interface SpawnOptions {
|
|
|
63
67
|
* Must alternate user→assistant. Assistant content is auto-wrapped in array format.
|
|
64
68
|
*/
|
|
65
69
|
history?: HistoryMessage[];
|
|
70
|
+
/** @internal Set by provider when history was injected via JSONL resume. */
|
|
71
|
+
_historyViaResume?: boolean;
|
|
66
72
|
/**
|
|
67
73
|
* Additional CLI flags passed directly to the agent binary.
|
|
68
74
|
* e.g. ["--system-prompt", "You are...", "--append-system-prompt", "Also...", "--mcp-config", "path"]
|
package/dist/db/schema.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
const DB_PATH = path.join(process.cwd(), "data/sna.db");
|
|
4
|
+
const DB_PATH = process.env.SNA_DB_PATH ?? path.join(process.cwd(), "data/sna.db");
|
|
5
5
|
const NATIVE_DIR = path.join(process.cwd(), ".sna/native");
|
|
6
6
|
let _db = null;
|
|
7
7
|
function loadBetterSqlite3() {
|
package/dist/scripts/sna.js
CHANGED
|
@@ -212,6 +212,9 @@ function cmdTu(args2) {
|
|
|
212
212
|
case "claude":
|
|
213
213
|
cmdTuClaude(args2.slice(1));
|
|
214
214
|
break;
|
|
215
|
+
case "claude:oneshot":
|
|
216
|
+
cmdTuClaudeOneshot(args2.slice(1));
|
|
217
|
+
break;
|
|
215
218
|
default:
|
|
216
219
|
console.log(`
|
|
217
220
|
sna tu \u2014 Test utilities (mock Anthropic API)
|
|
@@ -221,7 +224,8 @@ function cmdTu(args2) {
|
|
|
221
224
|
sna tu api:down Stop mock API server
|
|
222
225
|
sna tu api:log Show mock API request/response log
|
|
223
226
|
sna tu api:log -f Follow log in real-time (tail -f)
|
|
224
|
-
sna tu claude ...
|
|
227
|
+
sna tu claude ... Run claude with mock API env vars (proxy)
|
|
228
|
+
sna tu claude:oneshot ... One-shot: mock server \u2192 claude \u2192 results \u2192 cleanup
|
|
225
229
|
|
|
226
230
|
Flow:
|
|
227
231
|
1. sna tu api:up \u2192 mock server on random port
|
|
@@ -338,6 +342,19 @@ function cmdTuClaude(args2) {
|
|
|
338
342
|
process.exit(e.status ?? 1);
|
|
339
343
|
}
|
|
340
344
|
}
|
|
345
|
+
function cmdTuClaudeOneshot(args2) {
|
|
346
|
+
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
|
347
|
+
const oneshotScript = fs.existsSync(path.join(scriptDir, "tu-oneshot.js")) ? path.join(scriptDir, "tu-oneshot.js") : path.join(scriptDir, "tu-oneshot.ts");
|
|
348
|
+
try {
|
|
349
|
+
execSync(`node --import tsx "${oneshotScript}" ${args2.map((a) => `"${a}"`).join(" ")}`, {
|
|
350
|
+
stdio: "inherit",
|
|
351
|
+
cwd: ROOT,
|
|
352
|
+
timeout: 9e4
|
|
353
|
+
});
|
|
354
|
+
} catch (e) {
|
|
355
|
+
process.exit(e.status ?? 1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
341
358
|
function readPidFile(filePath) {
|
|
342
359
|
try {
|
|
343
360
|
return parseInt(fs.readFileSync(filePath, "utf8").trim(), 10) || null;
|
|
@@ -784,7 +801,8 @@ Testing:
|
|
|
784
801
|
sna tu api:down Stop mock API server
|
|
785
802
|
sna tu api:log Show mock API request/response log
|
|
786
803
|
sna tu api:log -f Follow log in real-time
|
|
787
|
-
sna tu claude ...
|
|
804
|
+
sna tu claude ... Run claude with mock API (isolated env)
|
|
805
|
+
sna tu claude:oneshot ... One-shot: mock server \u2192 claude \u2192 structured results \u2192 cleanup
|
|
788
806
|
|
|
789
807
|
Set SNA_CLAUDE_COMMAND to override claude binary in SDK.
|
|
790
808
|
See: docs/testing.md
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { startMockAnthropicServer } from "../testing/mock-api.js";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
async function main() {
|
|
6
|
+
const ROOT = process.cwd();
|
|
7
|
+
const STATE_DIR = path.join(ROOT, ".sna");
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
let claudePath = "claude";
|
|
10
|
+
const cachedPath = path.join(STATE_DIR, "claude-path");
|
|
11
|
+
if (fs.existsSync(cachedPath)) {
|
|
12
|
+
claudePath = fs.readFileSync(cachedPath, "utf8").trim() || claudePath;
|
|
13
|
+
}
|
|
14
|
+
const mock = await startMockAnthropicServer();
|
|
15
|
+
const mockConfigDir = path.join(STATE_DIR, "mock-claude-oneshot");
|
|
16
|
+
fs.mkdirSync(mockConfigDir, { recursive: true });
|
|
17
|
+
const env = {
|
|
18
|
+
PATH: process.env.PATH ?? "",
|
|
19
|
+
HOME: process.env.HOME ?? "",
|
|
20
|
+
SHELL: process.env.SHELL ?? "/bin/zsh",
|
|
21
|
+
TERM: process.env.TERM ?? "xterm-256color",
|
|
22
|
+
LANG: process.env.LANG ?? "en_US.UTF-8",
|
|
23
|
+
ANTHROPIC_BASE_URL: `http://localhost:${mock.port}`,
|
|
24
|
+
ANTHROPIC_API_KEY: "sk-test-mock-oneshot",
|
|
25
|
+
CLAUDE_CONFIG_DIR: mockConfigDir
|
|
26
|
+
};
|
|
27
|
+
const stdoutPath = path.join(STATE_DIR, "mock-claude-stdout.log");
|
|
28
|
+
const stderrPath = path.join(STATE_DIR, "mock-claude-stderr.log");
|
|
29
|
+
const proc = spawn(claudePath, args, {
|
|
30
|
+
env,
|
|
31
|
+
cwd: ROOT,
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
33
|
+
});
|
|
34
|
+
let stdout = "";
|
|
35
|
+
let stderr = "";
|
|
36
|
+
proc.stdout.on("data", (d) => {
|
|
37
|
+
stdout += d.toString();
|
|
38
|
+
});
|
|
39
|
+
proc.stderr.on("data", (d) => {
|
|
40
|
+
stderr += d.toString();
|
|
41
|
+
});
|
|
42
|
+
proc.stdout.pipe(process.stdout);
|
|
43
|
+
proc.on("exit", (code) => {
|
|
44
|
+
fs.writeFileSync(stdoutPath, stdout);
|
|
45
|
+
fs.writeFileSync(stderrPath, stderr);
|
|
46
|
+
console.log(`
|
|
47
|
+
${"\u2500".repeat(60)}`);
|
|
48
|
+
console.log(`Mock API: ${mock.requests.length} request(s)`);
|
|
49
|
+
for (const req of mock.requests) {
|
|
50
|
+
console.log(` model=${req.model} stream=${req.stream} messages=${req.messages?.length}`);
|
|
51
|
+
}
|
|
52
|
+
console.log(`
|
|
53
|
+
Log files:`);
|
|
54
|
+
console.log(` stdout: ${stdoutPath}`);
|
|
55
|
+
console.log(` stderr: ${stderrPath}`);
|
|
56
|
+
console.log(` api log: ${path.join(STATE_DIR, "mock-api-last-request.json")}`);
|
|
57
|
+
console.log(` config: ${mockConfigDir}`);
|
|
58
|
+
console.log(` exit: ${code}`);
|
|
59
|
+
mock.close();
|
|
60
|
+
process.exit(code ?? 0);
|
|
61
|
+
});
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
proc.kill();
|
|
64
|
+
}, 6e4);
|
|
65
|
+
}
|
|
66
|
+
main();
|
|
@@ -25,6 +25,12 @@ interface ApiResponses {
|
|
|
25
25
|
"agent.send": {
|
|
26
26
|
status: "sent";
|
|
27
27
|
};
|
|
28
|
+
"agent.resume": {
|
|
29
|
+
status: "resumed";
|
|
30
|
+
provider: string;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
historyCount: number;
|
|
33
|
+
};
|
|
28
34
|
"agent.restart": {
|
|
29
35
|
status: "restarted";
|
|
30
36
|
provider: string;
|
|
@@ -46,9 +52,16 @@ interface ApiResponses {
|
|
|
46
52
|
};
|
|
47
53
|
"agent.status": {
|
|
48
54
|
alive: boolean;
|
|
55
|
+
agentStatus: "idle" | "busy" | "disconnected";
|
|
49
56
|
sessionId: string | null;
|
|
50
57
|
ccSessionId: string | null;
|
|
51
58
|
eventCount: number;
|
|
59
|
+
messageCount: number;
|
|
60
|
+
lastMessage: {
|
|
61
|
+
role: string;
|
|
62
|
+
content: string;
|
|
63
|
+
created_at: string;
|
|
64
|
+
} | null;
|
|
52
65
|
config: {
|
|
53
66
|
provider: string;
|
|
54
67
|
model: string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HistoryMessage } from '../core/providers/types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build HistoryMessage[] from chat_messages DB records.
|
|
5
|
+
*
|
|
6
|
+
* Filters to user/assistant roles, ensures alternation,
|
|
7
|
+
* and merges consecutive same-role messages.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load conversation history from DB for a session.
|
|
12
|
+
* Returns alternating user↔assistant messages ready for JSONL injection.
|
|
13
|
+
*/
|
|
14
|
+
declare function buildHistoryFromDb(sessionId: string): HistoryMessage[];
|
|
15
|
+
|
|
16
|
+
export { buildHistoryFromDb };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getDb } from "../db/schema.js";
|
|
2
|
+
function buildHistoryFromDb(sessionId) {
|
|
3
|
+
const db = getDb();
|
|
4
|
+
const rows = db.prepare(
|
|
5
|
+
`SELECT role, content FROM chat_messages
|
|
6
|
+
WHERE session_id = ? AND role IN ('user', 'assistant')
|
|
7
|
+
ORDER BY id ASC`
|
|
8
|
+
).all(sessionId);
|
|
9
|
+
if (rows.length === 0) return [];
|
|
10
|
+
const merged = [];
|
|
11
|
+
for (const row of rows) {
|
|
12
|
+
const role = row.role;
|
|
13
|
+
if (!row.content?.trim()) continue;
|
|
14
|
+
const last = merged[merged.length - 1];
|
|
15
|
+
if (last && last.role === role) {
|
|
16
|
+
last.content += "\n\n" + row.content;
|
|
17
|
+
} else {
|
|
18
|
+
merged.push({ role, content: row.content });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return merged;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
buildHistoryFromDb
|
|
25
|
+
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
import { SessionManager } from './session-manager.js';
|
|
4
|
-
export { Session, SessionConfigChangedEvent, SessionInfo, SessionLifecycleEvent, SessionLifecycleState, SessionManagerOptions, StartConfig } from './session-manager.js';
|
|
4
|
+
export { AgentStatus, Session, SessionConfigChangedEvent, SessionInfo, SessionLifecycleEvent, SessionLifecycleState, SessionManagerOptions, StartConfig } from './session-manager.js';
|
|
5
5
|
export { eventsRoute } from './routes/events.js';
|
|
6
6
|
export { createEmitRoute, emitRoute } from './routes/emit.js';
|
|
7
7
|
export { createRunRoute } from './routes/run.js';
|
|
8
8
|
export { createAgentRoutes } from './routes/agent.js';
|
|
9
9
|
export { createChatRoutes } from './routes/chat.js';
|
|
10
10
|
export { attachWebSocket } from './ws.js';
|
|
11
|
+
export { buildHistoryFromDb } from './history-builder.js';
|
|
11
12
|
import '../core/providers/types.js';
|
|
12
13
|
import 'hono/utils/http-status';
|
|
13
14
|
import 'ws';
|
package/dist/server/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { createAgentRoutes as createAgentRoutes2 } from "./routes/agent.js";
|
|
|
27
27
|
import { createChatRoutes as createChatRoutes2 } from "./routes/chat.js";
|
|
28
28
|
import { SessionManager as SessionManager2 } from "./session-manager.js";
|
|
29
29
|
import { attachWebSocket } from "./ws.js";
|
|
30
|
+
import { buildHistoryFromDb } from "./history-builder.js";
|
|
30
31
|
function snaPortRoute(c) {
|
|
31
32
|
const portFile = _path.join(process.cwd(), ".sna/sna-api.port");
|
|
32
33
|
try {
|
|
@@ -39,6 +40,7 @@ function snaPortRoute(c) {
|
|
|
39
40
|
export {
|
|
40
41
|
SessionManager2 as SessionManager,
|
|
41
42
|
attachWebSocket,
|
|
43
|
+
buildHistoryFromDb,
|
|
42
44
|
createAgentRoutes2 as createAgentRoutes,
|
|
43
45
|
createChatRoutes2 as createChatRoutes,
|
|
44
46
|
createEmitRoute2 as createEmitRoute,
|