@xiaolei.shawn/mcp-server 0.2.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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +63 -0
- package/dist/dashboard.d.ts +4 -0
- package/dist/dashboard.js +401 -0
- package/dist/event-envelope.d.ts +33 -0
- package/dist/event-envelope.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +147 -0
- package/dist/store.d.ts +70 -0
- package/dist/store.js +254 -0
- package/dist/tools.d.ts +661 -0
- package/dist/tools.js +783 -0
- package/package.json +59 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const EVENT_SCHEMA_VERSION = 1;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { startDashboardServer } from "./dashboard.js";
|
|
8
|
+
import { exportSessionJson, listSessionFiles } from "./store.js";
|
|
9
|
+
import { handleGatewayAct, handleGatewayBeginRun, handleGatewayEndRun, handleRecordActivity, handleRecordAssumption, handleRecordDecision, handleRecordIntent, handleRecordSessionEnd, handleRecordSessionStart, handleRecordVerification, toolSchemas, } from "./tools.js";
|
|
10
|
+
function registerTools(server) {
|
|
11
|
+
server.registerTool("gateway_begin_run", {
|
|
12
|
+
description: "Gateway start: starts/reuses session and can create initial intent.",
|
|
13
|
+
inputSchema: toolSchemas.gateway_begin_run.inputSchema,
|
|
14
|
+
}, handleGatewayBeginRun);
|
|
15
|
+
server.registerTool("gateway_act", {
|
|
16
|
+
description: "Gateway action router: maps operation to semantic recorder tools/events with validation.",
|
|
17
|
+
inputSchema: toolSchemas.gateway_act.inputSchema,
|
|
18
|
+
}, handleGatewayAct);
|
|
19
|
+
server.registerTool("gateway_end_run", {
|
|
20
|
+
description: "Gateway end: closes active session with outcome/summary.",
|
|
21
|
+
inputSchema: toolSchemas.gateway_end_run.inputSchema,
|
|
22
|
+
}, handleGatewayEndRun);
|
|
23
|
+
server.registerTool("record_session_start", {
|
|
24
|
+
description: "Start a session and persist a canonical session_start event.",
|
|
25
|
+
inputSchema: toolSchemas.record_session_start.inputSchema,
|
|
26
|
+
}, handleRecordSessionStart);
|
|
27
|
+
server.registerTool("record_intent", {
|
|
28
|
+
description: "Record intent for the active session and return intent_id.",
|
|
29
|
+
inputSchema: toolSchemas.record_intent.inputSchema,
|
|
30
|
+
}, handleRecordIntent);
|
|
31
|
+
server.registerTool("record_activity", {
|
|
32
|
+
description: "Record activity events (file_op or tool_call) for the active session.",
|
|
33
|
+
inputSchema: toolSchemas.record_activity.inputSchema,
|
|
34
|
+
}, handleRecordActivity);
|
|
35
|
+
server.registerTool("record_decision", {
|
|
36
|
+
description: "Record a decision event for the active session.",
|
|
37
|
+
inputSchema: toolSchemas.record_decision.inputSchema,
|
|
38
|
+
}, handleRecordDecision);
|
|
39
|
+
server.registerTool("record_assumption", {
|
|
40
|
+
description: "Record an assumption event for the active session.",
|
|
41
|
+
inputSchema: toolSchemas.record_assumption.inputSchema,
|
|
42
|
+
}, handleRecordAssumption);
|
|
43
|
+
server.registerTool("record_verification", {
|
|
44
|
+
description: "Record verification outcomes (test/lint/typecheck/manual).",
|
|
45
|
+
inputSchema: toolSchemas.record_verification.inputSchema,
|
|
46
|
+
}, handleRecordVerification);
|
|
47
|
+
server.registerTool("record_session_end", {
|
|
48
|
+
description: "End active session and persist a canonical session_end event.",
|
|
49
|
+
inputSchema: toolSchemas.record_session_end.inputSchema,
|
|
50
|
+
}, handleRecordSessionEnd);
|
|
51
|
+
}
|
|
52
|
+
function openBrowser(url) {
|
|
53
|
+
const platform = process.platform;
|
|
54
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
55
|
+
const args = platform === "darwin"
|
|
56
|
+
? [url]
|
|
57
|
+
: platform === "win32"
|
|
58
|
+
? ["/c", "start", "", url]
|
|
59
|
+
: [url];
|
|
60
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
61
|
+
child.unref();
|
|
62
|
+
}
|
|
63
|
+
async function runMcpServer() {
|
|
64
|
+
const server = new McpServer({
|
|
65
|
+
name: "al-mcp-server",
|
|
66
|
+
version: "0.2.0",
|
|
67
|
+
});
|
|
68
|
+
registerTools(server);
|
|
69
|
+
startDashboardServer();
|
|
70
|
+
const transport = new StdioServerTransport();
|
|
71
|
+
await server.connect(transport);
|
|
72
|
+
process.stderr.write("AL MCP server connected (stdio)\n");
|
|
73
|
+
}
|
|
74
|
+
function runStart(openFlag) {
|
|
75
|
+
const dashboard = startDashboardServer();
|
|
76
|
+
if (!dashboard) {
|
|
77
|
+
process.stderr.write("Dashboard is disabled via env.\n");
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const url = `http://${dashboard.host}:${dashboard.port}`;
|
|
82
|
+
if (openFlag) {
|
|
83
|
+
try {
|
|
84
|
+
openBrowser(url);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
process.stderr.write(`Failed to open browser: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
process.stderr.write(`AgentLens local gateway is running at ${url}\n` +
|
|
91
|
+
"Use MCP mode in your agent config with: `agentlens mcp`\n");
|
|
92
|
+
}
|
|
93
|
+
function parseFlag(flag) {
|
|
94
|
+
const index = process.argv.indexOf(flag);
|
|
95
|
+
if (index === -1)
|
|
96
|
+
return undefined;
|
|
97
|
+
return process.argv[index + 1];
|
|
98
|
+
}
|
|
99
|
+
function runExport() {
|
|
100
|
+
const explicitSession = parseFlag("--session");
|
|
101
|
+
const out = parseFlag("--out");
|
|
102
|
+
const latest = process.argv.includes("--latest");
|
|
103
|
+
const sessions = listSessionFiles();
|
|
104
|
+
if (sessions.length === 0) {
|
|
105
|
+
throw new Error("No sessions found.");
|
|
106
|
+
}
|
|
107
|
+
const target = explicitSession
|
|
108
|
+
? sessions.find((item) => item.session_id === explicitSession)
|
|
109
|
+
: latest || !explicitSession
|
|
110
|
+
? sessions[0]
|
|
111
|
+
: undefined;
|
|
112
|
+
if (!target) {
|
|
113
|
+
throw new Error(`Session not found: ${explicitSession}`);
|
|
114
|
+
}
|
|
115
|
+
const exported = exportSessionJson(target.session_id);
|
|
116
|
+
if (out) {
|
|
117
|
+
const path = resolve(out);
|
|
118
|
+
writeFileSync(path, exported, "utf-8");
|
|
119
|
+
process.stdout.write(`Exported ${target.session_id} -> ${path}\n`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
process.stdout.write(exported + "\n");
|
|
123
|
+
}
|
|
124
|
+
async function main() {
|
|
125
|
+
const command = process.argv[2] ?? "mcp";
|
|
126
|
+
if (command === "start") {
|
|
127
|
+
runStart(process.argv.includes("--open"));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (command === "export") {
|
|
131
|
+
runExport();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (command === "mcp") {
|
|
135
|
+
await runMcpServer();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
process.stderr.write("Usage:\n" +
|
|
139
|
+
" agentlens start [--open]\n" +
|
|
140
|
+
" agentlens mcp\n" +
|
|
141
|
+
" agentlens export [--latest|--session <id>] [--out <path>]\n");
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
144
|
+
main().catch((error) => {
|
|
145
|
+
process.stderr.write(`Fatal: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
});
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type ActorType, type CanonicalEvent, type SessionLogFile } from "./event-envelope.js";
|
|
2
|
+
export interface SessionState {
|
|
3
|
+
session_id: string;
|
|
4
|
+
goal: string;
|
|
5
|
+
user_prompt?: string;
|
|
6
|
+
repo?: string;
|
|
7
|
+
branch?: string;
|
|
8
|
+
started_at: string;
|
|
9
|
+
ended_at?: string;
|
|
10
|
+
next_seq: number;
|
|
11
|
+
active_intent_id?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SessionFileInfo {
|
|
14
|
+
session_id: string;
|
|
15
|
+
path: string;
|
|
16
|
+
size_bytes: number;
|
|
17
|
+
updated_at: string;
|
|
18
|
+
}
|
|
19
|
+
export interface NormalizedSessionSnapshot {
|
|
20
|
+
session_id: string;
|
|
21
|
+
goal: string;
|
|
22
|
+
started_at: string;
|
|
23
|
+
ended_at?: string;
|
|
24
|
+
outcome: "completed" | "partial" | "failed" | "aborted" | "unknown";
|
|
25
|
+
event_count: number;
|
|
26
|
+
intent_count: number;
|
|
27
|
+
verification: {
|
|
28
|
+
pass: number;
|
|
29
|
+
fail: number;
|
|
30
|
+
unknown: number;
|
|
31
|
+
};
|
|
32
|
+
files_touched: string[];
|
|
33
|
+
kinds: Record<string, number>;
|
|
34
|
+
}
|
|
35
|
+
export declare function createSession(input: {
|
|
36
|
+
goal: string;
|
|
37
|
+
user_prompt?: string;
|
|
38
|
+
repo?: string;
|
|
39
|
+
branch?: string;
|
|
40
|
+
}): SessionState;
|
|
41
|
+
export declare function getActiveSession(): SessionState | undefined;
|
|
42
|
+
export declare function ensureActiveSession(): SessionState;
|
|
43
|
+
export declare function setActiveIntent(intentId: string): void;
|
|
44
|
+
export interface CreateEventInput {
|
|
45
|
+
session_id: string;
|
|
46
|
+
kind: string;
|
|
47
|
+
actor: {
|
|
48
|
+
type: ActorType;
|
|
49
|
+
id?: string;
|
|
50
|
+
};
|
|
51
|
+
payload: Record<string, unknown>;
|
|
52
|
+
ts?: string;
|
|
53
|
+
scope?: {
|
|
54
|
+
intent_id?: string;
|
|
55
|
+
file?: string;
|
|
56
|
+
module?: string;
|
|
57
|
+
};
|
|
58
|
+
derived?: boolean;
|
|
59
|
+
confidence?: number;
|
|
60
|
+
visibility?: "raw" | "review" | "debug";
|
|
61
|
+
}
|
|
62
|
+
export declare function createEvent(state: SessionState, input: CreateEventInput): CanonicalEvent;
|
|
63
|
+
export declare function persistEvent(event: CanonicalEvent): Promise<void>;
|
|
64
|
+
export declare function readSessionEvents(sessionId: string): CanonicalEvent[];
|
|
65
|
+
export declare function persistNormalizedSnapshot(session: SessionState): Promise<NormalizedSessionSnapshot>;
|
|
66
|
+
export declare function listSessionFiles(): SessionFileInfo[];
|
|
67
|
+
export declare function exportSessionJson(sessionId: string): string;
|
|
68
|
+
export declare function endActiveSession(endedAt?: string): Promise<SessionState>;
|
|
69
|
+
export declare function buildSessionLog(state: SessionState, events: CanonicalEvent[]): SessionLogFile;
|
|
70
|
+
export declare function initializeSessionLog(state: SessionState): void;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { getSessionsDir } from "./config.js";
|
|
5
|
+
import { EVENT_SCHEMA_VERSION, } from "./event-envelope.js";
|
|
6
|
+
const sessionStates = new Map();
|
|
7
|
+
let activeSessionId;
|
|
8
|
+
let writeQueue = Promise.resolve();
|
|
9
|
+
function withWriteLock(fn) {
|
|
10
|
+
const run = writeQueue.then(fn, fn);
|
|
11
|
+
writeQueue = run.then(() => undefined, () => undefined);
|
|
12
|
+
return run;
|
|
13
|
+
}
|
|
14
|
+
function safeFilename(input) {
|
|
15
|
+
return input.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
16
|
+
}
|
|
17
|
+
function getSessionLogPath(sessionId) {
|
|
18
|
+
const outDir = getSessionsDir();
|
|
19
|
+
if (!existsSync(outDir))
|
|
20
|
+
mkdirSync(outDir, { recursive: true });
|
|
21
|
+
return join(outDir, `${safeFilename(sessionId)}.jsonl`);
|
|
22
|
+
}
|
|
23
|
+
function getSessionSnapshotPath(sessionId) {
|
|
24
|
+
const outDir = getSessionsDir();
|
|
25
|
+
if (!existsSync(outDir))
|
|
26
|
+
mkdirSync(outDir, { recursive: true });
|
|
27
|
+
return join(outDir, `${safeFilename(sessionId)}.session.json`);
|
|
28
|
+
}
|
|
29
|
+
function toCanonicalTs(rawTs) {
|
|
30
|
+
if (!rawTs)
|
|
31
|
+
return new Date().toISOString();
|
|
32
|
+
const parsed = new Date(rawTs);
|
|
33
|
+
if (Number.isNaN(parsed.getTime()))
|
|
34
|
+
throw new Error(`Invalid timestamp: ${rawTs}`);
|
|
35
|
+
return parsed.toISOString();
|
|
36
|
+
}
|
|
37
|
+
function assertConfidence(confidence) {
|
|
38
|
+
if (confidence === undefined)
|
|
39
|
+
return;
|
|
40
|
+
if (Number.isNaN(confidence) || confidence < 0 || confidence > 1) {
|
|
41
|
+
throw new Error("confidence must be a number between 0 and 1");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function createSession(input) {
|
|
45
|
+
const startedAt = new Date().toISOString();
|
|
46
|
+
const sessionId = `sess_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
47
|
+
const state = {
|
|
48
|
+
session_id: sessionId,
|
|
49
|
+
goal: input.goal,
|
|
50
|
+
user_prompt: input.user_prompt,
|
|
51
|
+
repo: input.repo,
|
|
52
|
+
branch: input.branch,
|
|
53
|
+
started_at: startedAt,
|
|
54
|
+
next_seq: 1,
|
|
55
|
+
};
|
|
56
|
+
sessionStates.set(sessionId, state);
|
|
57
|
+
activeSessionId = sessionId;
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
export function getActiveSession() {
|
|
61
|
+
if (!activeSessionId)
|
|
62
|
+
return undefined;
|
|
63
|
+
return sessionStates.get(activeSessionId);
|
|
64
|
+
}
|
|
65
|
+
export function ensureActiveSession() {
|
|
66
|
+
const active = getActiveSession();
|
|
67
|
+
if (!active) {
|
|
68
|
+
throw new Error("No active session. Call record_session_start first.");
|
|
69
|
+
}
|
|
70
|
+
if (active.ended_at) {
|
|
71
|
+
throw new Error("Active session has ended. Call record_session_start to begin a new one.");
|
|
72
|
+
}
|
|
73
|
+
return active;
|
|
74
|
+
}
|
|
75
|
+
export function setActiveIntent(intentId) {
|
|
76
|
+
const s = ensureActiveSession();
|
|
77
|
+
s.active_intent_id = intentId;
|
|
78
|
+
}
|
|
79
|
+
export function createEvent(state, input) {
|
|
80
|
+
const seq = state.next_seq;
|
|
81
|
+
state.next_seq += 1;
|
|
82
|
+
const ts = toCanonicalTs(input.ts);
|
|
83
|
+
assertConfidence(input.confidence);
|
|
84
|
+
return {
|
|
85
|
+
id: `${state.session_id}:${seq}:${randomUUID().slice(0, 8)}`,
|
|
86
|
+
session_id: input.session_id,
|
|
87
|
+
seq,
|
|
88
|
+
ts,
|
|
89
|
+
kind: input.kind,
|
|
90
|
+
actor: input.actor,
|
|
91
|
+
scope: input.scope,
|
|
92
|
+
payload: input.payload,
|
|
93
|
+
derived: input.derived,
|
|
94
|
+
confidence: input.confidence,
|
|
95
|
+
visibility: input.visibility,
|
|
96
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export async function persistEvent(event) {
|
|
100
|
+
await withWriteLock(() => {
|
|
101
|
+
const path = getSessionLogPath(event.session_id);
|
|
102
|
+
appendFileSync(path, JSON.stringify(event) + "\n", "utf-8");
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export function readSessionEvents(sessionId) {
|
|
106
|
+
const path = getSessionLogPath(sessionId);
|
|
107
|
+
if (!existsSync(path))
|
|
108
|
+
return [];
|
|
109
|
+
const content = readFileSync(path, "utf-8").trim();
|
|
110
|
+
if (!content)
|
|
111
|
+
return [];
|
|
112
|
+
const lines = content
|
|
113
|
+
.split("\n")
|
|
114
|
+
.map((line) => line.trim())
|
|
115
|
+
.filter((line) => line.length > 0);
|
|
116
|
+
const events = lines.map((line, index) => {
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(line);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error(`Invalid JSONL in ${path} at line ${index + 1}`);
|
|
123
|
+
}
|
|
124
|
+
if (!parsed || typeof parsed !== "object") {
|
|
125
|
+
throw new Error(`Invalid event object in ${path} at line ${index + 1}`);
|
|
126
|
+
}
|
|
127
|
+
return parsed;
|
|
128
|
+
});
|
|
129
|
+
return events.sort((a, b) => (a.seq === b.seq ? a.ts.localeCompare(b.ts) : a.seq - b.seq));
|
|
130
|
+
}
|
|
131
|
+
function deriveSnapshot(session, events) {
|
|
132
|
+
const kinds = {};
|
|
133
|
+
let pass = 0;
|
|
134
|
+
let fail = 0;
|
|
135
|
+
let unknown = 0;
|
|
136
|
+
const files = new Set();
|
|
137
|
+
let intentCount = 0;
|
|
138
|
+
for (const event of events) {
|
|
139
|
+
kinds[event.kind] = (kinds[event.kind] ?? 0) + 1;
|
|
140
|
+
if (event.kind === "intent")
|
|
141
|
+
intentCount += 1;
|
|
142
|
+
if (event.kind === "file_op") {
|
|
143
|
+
const target = typeof event.payload.target === "string"
|
|
144
|
+
? event.payload.target
|
|
145
|
+
: event.scope?.file;
|
|
146
|
+
if (target && target.trim() !== "")
|
|
147
|
+
files.add(target);
|
|
148
|
+
}
|
|
149
|
+
if (event.kind === "verification") {
|
|
150
|
+
const result = event.payload.result;
|
|
151
|
+
if (result === "pass")
|
|
152
|
+
pass += 1;
|
|
153
|
+
else if (result === "fail")
|
|
154
|
+
fail += 1;
|
|
155
|
+
else
|
|
156
|
+
unknown += 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const end = [...events].reverse().find((event) => event.kind === "session_end");
|
|
160
|
+
const outcomeRaw = end?.payload?.outcome;
|
|
161
|
+
const outcome = outcomeRaw === "completed" || outcomeRaw === "partial" || outcomeRaw === "failed" || outcomeRaw === "aborted"
|
|
162
|
+
? outcomeRaw
|
|
163
|
+
: "unknown";
|
|
164
|
+
return {
|
|
165
|
+
session_id: session.session_id,
|
|
166
|
+
goal: session.goal,
|
|
167
|
+
started_at: session.started_at,
|
|
168
|
+
ended_at: session.ended_at,
|
|
169
|
+
outcome,
|
|
170
|
+
event_count: events.length,
|
|
171
|
+
intent_count: intentCount,
|
|
172
|
+
verification: { pass, fail, unknown },
|
|
173
|
+
files_touched: [...files].sort(),
|
|
174
|
+
kinds,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export async function persistNormalizedSnapshot(session) {
|
|
178
|
+
const events = readSessionEvents(session.session_id);
|
|
179
|
+
const snapshot = deriveSnapshot(session, events);
|
|
180
|
+
await withWriteLock(() => {
|
|
181
|
+
const path = getSessionSnapshotPath(session.session_id);
|
|
182
|
+
writeFileSync(path, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
183
|
+
});
|
|
184
|
+
return snapshot;
|
|
185
|
+
}
|
|
186
|
+
export function listSessionFiles() {
|
|
187
|
+
const dir = getSessionsDir();
|
|
188
|
+
if (!existsSync(dir))
|
|
189
|
+
return [];
|
|
190
|
+
const files = readdirSync(dir).filter((name) => name.endsWith(".jsonl"));
|
|
191
|
+
const out = files.map((name) => {
|
|
192
|
+
const path = join(dir, name);
|
|
193
|
+
const stats = statSync(path);
|
|
194
|
+
const session_id = name.replace(/\.jsonl$/, "");
|
|
195
|
+
return {
|
|
196
|
+
session_id,
|
|
197
|
+
path,
|
|
198
|
+
size_bytes: stats.size,
|
|
199
|
+
updated_at: stats.mtime.toISOString(),
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
out.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
export function exportSessionJson(sessionId) {
|
|
206
|
+
const state = sessionStates.get(sessionId);
|
|
207
|
+
const events = readSessionEvents(sessionId);
|
|
208
|
+
if (events.length === 0)
|
|
209
|
+
throw new Error(`No events found for session: ${sessionId}`);
|
|
210
|
+
const firstStart = events.find((event) => event.kind === "session_start");
|
|
211
|
+
const startPayload = (firstStart?.payload ?? {});
|
|
212
|
+
const inferredState = state ?? {
|
|
213
|
+
session_id: sessionId,
|
|
214
|
+
goal: typeof startPayload.goal === "string" ? startPayload.goal : "Unknown goal",
|
|
215
|
+
user_prompt: typeof startPayload.user_prompt === "string" ? startPayload.user_prompt : undefined,
|
|
216
|
+
repo: typeof startPayload.repo === "string" ? startPayload.repo : undefined,
|
|
217
|
+
branch: typeof startPayload.branch === "string" ? startPayload.branch : undefined,
|
|
218
|
+
started_at: firstStart?.ts ?? events[0].ts,
|
|
219
|
+
ended_at: [...events].reverse().find((event) => event.kind === "session_end")?.ts,
|
|
220
|
+
next_seq: (events[events.length - 1]?.seq ?? 0) + 1,
|
|
221
|
+
};
|
|
222
|
+
const snapshot = deriveSnapshot(inferredState, events);
|
|
223
|
+
return JSON.stringify({
|
|
224
|
+
...buildSessionLog(inferredState, events),
|
|
225
|
+
normalized: snapshot,
|
|
226
|
+
}, null, 2);
|
|
227
|
+
}
|
|
228
|
+
export async function endActiveSession(endedAt) {
|
|
229
|
+
const state = ensureActiveSession();
|
|
230
|
+
state.ended_at = toCanonicalTs(endedAt);
|
|
231
|
+
await withWriteLock(() => undefined);
|
|
232
|
+
if (activeSessionId === state.session_id) {
|
|
233
|
+
activeSessionId = undefined;
|
|
234
|
+
}
|
|
235
|
+
return state;
|
|
236
|
+
}
|
|
237
|
+
export function buildSessionLog(state, events) {
|
|
238
|
+
return {
|
|
239
|
+
session_id: state.session_id,
|
|
240
|
+
goal: state.goal,
|
|
241
|
+
user_prompt: state.user_prompt,
|
|
242
|
+
repo: state.repo,
|
|
243
|
+
branch: state.branch,
|
|
244
|
+
started_at: state.started_at,
|
|
245
|
+
ended_at: state.ended_at,
|
|
246
|
+
events,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export function initializeSessionLog(state) {
|
|
250
|
+
const path = getSessionLogPath(state.session_id);
|
|
251
|
+
if (existsSync(path))
|
|
252
|
+
return;
|
|
253
|
+
writeFileSync(path, "", "utf-8");
|
|
254
|
+
}
|