@loom-framework/backend 0.1.0-alpha.1
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/dist/ai/engine.d.ts +37 -0
- package/dist/ai/engine.d.ts.map +1 -0
- package/dist/ai/engine.js +137 -0
- package/dist/ai/engine.js.map +1 -0
- package/dist/ai/index.d.ts +9 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +7 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/output-parser.d.ts +19 -0
- package/dist/ai/output-parser.d.ts.map +1 -0
- package/dist/ai/output-parser.js +154 -0
- package/dist/ai/output-parser.js.map +1 -0
- package/dist/ai/session-manager.d.ts +81 -0
- package/dist/ai/session-manager.d.ts.map +1 -0
- package/dist/ai/session-manager.js +193 -0
- package/dist/ai/session-manager.js.map +1 -0
- package/dist/bin.d.ts +12 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +75 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/observe/index.d.ts +6 -0
- package/dist/observe/index.d.ts.map +1 -0
- package/dist/observe/index.js +5 -0
- package/dist/observe/index.js.map +1 -0
- package/dist/observe/logger.d.ts +28 -0
- package/dist/observe/logger.d.ts.map +1 -0
- package/dist/observe/logger.js +80 -0
- package/dist/observe/logger.js.map +1 -0
- package/dist/observe/types.d.ts +26 -0
- package/dist/observe/types.d.ts.map +1 -0
- package/dist/observe/types.js +7 -0
- package/dist/observe/types.js.map +1 -0
- package/dist/routes/data.d.ts +13 -0
- package/dist/routes/data.d.ts.map +1 -0
- package/dist/routes/data.js +95 -0
- package/dist/routes/data.js.map +1 -0
- package/dist/routes/health.d.ts +7 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +15 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +6 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +6 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/websocket/index.d.ts +32 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +320 -0
- package/dist/websocket/index.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Engine - Core abstraction for AI communication
|
|
3
|
+
*
|
|
4
|
+
* ClaudeCodeEngine: spawns `claude -p --output-format json` processes
|
|
5
|
+
* with support for --resume, --model, and MCP config.
|
|
6
|
+
*/
|
|
7
|
+
import type { AIEngine, AICallOptions, AIChunk, ClaudeCodeConfig } from '@loom-framework/core';
|
|
8
|
+
export interface ClaudeCodeEngineOptions {
|
|
9
|
+
config: ClaudeCodeConfig;
|
|
10
|
+
projectRoot: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* ClaudeCodeEngine - Spawns Claude Code in headless mode
|
|
14
|
+
*
|
|
15
|
+
* Uses `claude -p --output-format json --mcp-config .loom/mcp.json`
|
|
16
|
+
* to invoke AI capabilities. Supports session resume and model selection.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ClaudeCodeEngine implements AIEngine {
|
|
19
|
+
readonly name = "claude-code";
|
|
20
|
+
private config;
|
|
21
|
+
private projectRoot;
|
|
22
|
+
private activeProcesses;
|
|
23
|
+
constructor(options: ClaudeCodeEngineOptions);
|
|
24
|
+
/**
|
|
25
|
+
* Call Claude Code with a prompt and stream back AIChunk objects
|
|
26
|
+
*/
|
|
27
|
+
call(prompt: string, options: AICallOptions): AsyncGenerator<AIChunk>;
|
|
28
|
+
/**
|
|
29
|
+
* Kill an active process by session ID
|
|
30
|
+
*/
|
|
31
|
+
killSession(sessionId: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Kill all active processes (for graceful shutdown)
|
|
34
|
+
*/
|
|
35
|
+
killAll(): void;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/ai/engine.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAM/F,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,gBAAgB,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,QAAQ;IAC/C,QAAQ,CAAC,IAAI,iBAAiB;IAE9B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,eAAe,CAAwC;gBAEnD,OAAO,EAAE,uBAAuB;IAK5C;;OAEG;IACI,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,cAAc,CAAC,OAAO,CAAC;IAgG5E;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAUvC;;OAEG;IACH,OAAO,IAAI,IAAI;CAQhB"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Engine - Core abstraction for AI communication
|
|
3
|
+
*
|
|
4
|
+
* ClaudeCodeEngine: spawns `claude -p --output-format json` processes
|
|
5
|
+
* with support for --resume, --model, and MCP config.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { parseClaudeOutput } from './output-parser.js';
|
|
10
|
+
/** Default timeout for Claude Code processes (2 minutes) */
|
|
11
|
+
const DEFAULT_PROCESS_TIMEOUT_MS = 120_000;
|
|
12
|
+
/**
|
|
13
|
+
* ClaudeCodeEngine - Spawns Claude Code in headless mode
|
|
14
|
+
*
|
|
15
|
+
* Uses `claude -p --output-format json --mcp-config .loom/mcp.json`
|
|
16
|
+
* to invoke AI capabilities. Supports session resume and model selection.
|
|
17
|
+
*/
|
|
18
|
+
export class ClaudeCodeEngine {
|
|
19
|
+
name = 'claude-code';
|
|
20
|
+
config;
|
|
21
|
+
projectRoot;
|
|
22
|
+
activeProcesses = new Map();
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.config = options.config;
|
|
25
|
+
this.projectRoot = options.projectRoot;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Call Claude Code with a prompt and stream back AIChunk objects
|
|
29
|
+
*/
|
|
30
|
+
async *call(prompt, options) {
|
|
31
|
+
const claudePath = this.config.path || process.env.CLAUDE_CODE_PATH || 'claude';
|
|
32
|
+
const timeout = options.timeout ?? this.config.timeout ?? DEFAULT_PROCESS_TIMEOUT_MS;
|
|
33
|
+
const pluginRoot = this.config.pluginRoot || path.join(this.projectRoot, '.claude');
|
|
34
|
+
const mcpConfigPath = path.join(this.projectRoot, '.loom', 'mcp.json');
|
|
35
|
+
const args = [
|
|
36
|
+
'-p',
|
|
37
|
+
'--output-format', 'json',
|
|
38
|
+
'--mcp-config', mcpConfigPath,
|
|
39
|
+
];
|
|
40
|
+
if (this.config.skipPermissions !== false) {
|
|
41
|
+
args.push('--dangerously-skip-permissions');
|
|
42
|
+
}
|
|
43
|
+
if (options.model || this.config.defaultModel) {
|
|
44
|
+
args.push('--model', options.model || this.config.defaultModel);
|
|
45
|
+
}
|
|
46
|
+
if (options.resumeSessionId) {
|
|
47
|
+
args.push('--resume', options.resumeSessionId);
|
|
48
|
+
}
|
|
49
|
+
// Read prompt from stdin to avoid ARG_MAX
|
|
50
|
+
args.push('-');
|
|
51
|
+
const childProcess = spawn(claudePath, args, {
|
|
52
|
+
cwd: this.projectRoot,
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
env: {
|
|
55
|
+
...process.env,
|
|
56
|
+
CLAUDE_PLUGIN_ROOT: pluginRoot,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
// Track active process for cleanup
|
|
60
|
+
this.activeProcesses.set(options.sessionId, childProcess);
|
|
61
|
+
// Send prompt via stdin then close
|
|
62
|
+
childProcess.stdin.write(prompt);
|
|
63
|
+
childProcess.stdin.end();
|
|
64
|
+
// Parse output stream
|
|
65
|
+
try {
|
|
66
|
+
for await (const chunk of parseClaudeOutput(childProcess.stdout)) {
|
|
67
|
+
// For result-type content chunks that carry sessionId, split into two chunks
|
|
68
|
+
if (chunk.type === 'content' && chunk.sessionId) {
|
|
69
|
+
const { sessionId, usage, ...contentChunk } = chunk;
|
|
70
|
+
yield contentChunk;
|
|
71
|
+
if (sessionId) {
|
|
72
|
+
yield {
|
|
73
|
+
type: 'session_info',
|
|
74
|
+
sessionId,
|
|
75
|
+
usage,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
yield chunk;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
yield {
|
|
86
|
+
type: 'error',
|
|
87
|
+
error: error instanceof Error ? error.message : String(error),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Wait for process completion with timeout
|
|
91
|
+
await new Promise((resolve, reject) => {
|
|
92
|
+
const timeoutHandle = setTimeout(() => {
|
|
93
|
+
childProcess.kill('SIGTERM');
|
|
94
|
+
// Force kill after 5s
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
childProcess.kill('SIGKILL');
|
|
97
|
+
}, 5_000);
|
|
98
|
+
reject(new Error(`Claude Code process timed out after ${timeout}ms`));
|
|
99
|
+
}, timeout);
|
|
100
|
+
childProcess.on('close', () => {
|
|
101
|
+
clearTimeout(timeoutHandle);
|
|
102
|
+
this.activeProcesses.delete(options.sessionId);
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
childProcess.on('error', (err) => {
|
|
106
|
+
clearTimeout(timeoutHandle);
|
|
107
|
+
this.activeProcesses.delete(options.sessionId);
|
|
108
|
+
reject(err);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
yield { type: 'done' };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Kill an active process by session ID
|
|
115
|
+
*/
|
|
116
|
+
killSession(sessionId) {
|
|
117
|
+
const proc = this.activeProcesses.get(sessionId);
|
|
118
|
+
if (proc && !proc.killed) {
|
|
119
|
+
proc.kill('SIGTERM');
|
|
120
|
+
this.activeProcesses.delete(sessionId);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Kill all active processes (for graceful shutdown)
|
|
127
|
+
*/
|
|
128
|
+
killAll() {
|
|
129
|
+
for (const [sessionId, proc] of this.activeProcesses) {
|
|
130
|
+
if (!proc.killed) {
|
|
131
|
+
proc.kill('SIGTERM');
|
|
132
|
+
}
|
|
133
|
+
this.activeProcesses.delete(sessionId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/ai/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Module - Barrel Export
|
|
3
|
+
*/
|
|
4
|
+
export { ClaudeCodeEngine } from './engine.js';
|
|
5
|
+
export type { ClaudeCodeEngineOptions } from './engine.js';
|
|
6
|
+
export { SessionManager } from './session-manager.js';
|
|
7
|
+
export type { SessionRecord, SessionMessage, SessionFileRef } from './session-manager.js';
|
|
8
|
+
export { parseClaudeOutput } from './output-parser.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Output Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses `claude -p --output-format json` output stream into AIChunk objects.
|
|
5
|
+
* Handles both newline-delimited JSON (streaming) and single JSON object (batch) modes.
|
|
6
|
+
*/
|
|
7
|
+
import type { AIChunk } from '@loom-framework/core';
|
|
8
|
+
/**
|
|
9
|
+
* Parse Claude Code stdout stream into typed AIChunk objects
|
|
10
|
+
*
|
|
11
|
+
* Strategy:
|
|
12
|
+
* 1. Collect all stdout into a buffer
|
|
13
|
+
* 2. Strip ANSI escape codes
|
|
14
|
+
* 3. Try parsing as newline-delimited JSON lines first
|
|
15
|
+
* 4. Fall back to parsing the entire buffer as a single JSON object
|
|
16
|
+
* 5. Ultimate fallback: yield raw text as content chunk
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseClaudeOutput(stdout: NodeJS.ReadableStream): AsyncGenerator<AIChunk>;
|
|
19
|
+
//# sourceMappingURL=output-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../../src/ai/output-parser.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAW,MAAM,sBAAsB,CAAC;AA6B7D;;;;;;;;;GASG;AACH,wBAAuB,iBAAiB,CACtC,MAAM,EAAE,MAAM,CAAC,cAAc,GAC5B,cAAc,CAAC,OAAO,CAAC,CAiCzB"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Output Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses `claude -p --output-format json` output stream into AIChunk objects.
|
|
5
|
+
* Handles both newline-delimited JSON (streaming) and single JSON object (batch) modes.
|
|
6
|
+
*/
|
|
7
|
+
/** ANSI escape code pattern */
|
|
8
|
+
const ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
|
|
9
|
+
/**
|
|
10
|
+
* Parse Claude Code stdout stream into typed AIChunk objects
|
|
11
|
+
*
|
|
12
|
+
* Strategy:
|
|
13
|
+
* 1. Collect all stdout into a buffer
|
|
14
|
+
* 2. Strip ANSI escape codes
|
|
15
|
+
* 3. Try parsing as newline-delimited JSON lines first
|
|
16
|
+
* 4. Fall back to parsing the entire buffer as a single JSON object
|
|
17
|
+
* 5. Ultimate fallback: yield raw text as content chunk
|
|
18
|
+
*/
|
|
19
|
+
export async function* parseClaudeOutput(stdout) {
|
|
20
|
+
let buffer = '';
|
|
21
|
+
for await (const chunk of stdout) {
|
|
22
|
+
buffer += chunk.toString();
|
|
23
|
+
}
|
|
24
|
+
const cleanOutput = buffer.replace(ANSI_ESCAPE, '').trim();
|
|
25
|
+
if (!cleanOutput)
|
|
26
|
+
return;
|
|
27
|
+
// Strategy 1: Try newline-delimited JSON
|
|
28
|
+
const lines = cleanOutput.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
29
|
+
const parsedChunks = tryParseJsonLines(lines);
|
|
30
|
+
if (parsedChunks.length > 0) {
|
|
31
|
+
for (const chunk of parsedChunks) {
|
|
32
|
+
yield chunk;
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Strategy 2: Try single JSON object
|
|
37
|
+
const singleChunk = tryParseSingleJson(cleanOutput);
|
|
38
|
+
if (singleChunk) {
|
|
39
|
+
yield singleChunk;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Strategy 3: Raw text fallback
|
|
43
|
+
yield {
|
|
44
|
+
type: 'content',
|
|
45
|
+
content: cleanOutput,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Attempt to parse multiple JSON lines into AIChunk objects
|
|
50
|
+
*/
|
|
51
|
+
function tryParseJsonLines(lines) {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let hasValidJson = false;
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
try {
|
|
56
|
+
const data = JSON.parse(line);
|
|
57
|
+
hasValidJson = true;
|
|
58
|
+
const chunk = mapJsonToChunk(data);
|
|
59
|
+
if (chunk)
|
|
60
|
+
chunks.push(chunk);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Not valid JSON on this line, skip
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return hasValidJson ? chunks : [];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Attempt to parse the entire output as a single JSON object
|
|
70
|
+
*/
|
|
71
|
+
function tryParseSingleJson(output) {
|
|
72
|
+
try {
|
|
73
|
+
const data = JSON.parse(output);
|
|
74
|
+
return mapJsonToChunk(data);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Map a parsed JSON object to an AIChunk
|
|
82
|
+
*/
|
|
83
|
+
function mapJsonToChunk(data) {
|
|
84
|
+
switch (data.type) {
|
|
85
|
+
case 'result': {
|
|
86
|
+
if (data.is_error) {
|
|
87
|
+
return {
|
|
88
|
+
type: 'error',
|
|
89
|
+
error: data.result || 'Unknown error from Claude Code',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// For single-result mode, emit content + session_info together
|
|
93
|
+
// The caller (engine) will handle splitting them
|
|
94
|
+
if (data.result) {
|
|
95
|
+
return {
|
|
96
|
+
type: 'content',
|
|
97
|
+
content: data.result,
|
|
98
|
+
sessionId: data.session_id,
|
|
99
|
+
usage: extractUsage(data),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// No content but has session info
|
|
103
|
+
return extractSessionInfo(data);
|
|
104
|
+
}
|
|
105
|
+
case 'assistant': {
|
|
106
|
+
if (data.subtype === 'text' && data.content) {
|
|
107
|
+
return { type: 'content', content: data.content };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extract session metadata from a result object
|
|
117
|
+
*/
|
|
118
|
+
function extractSessionInfo(data) {
|
|
119
|
+
if (!data.session_id)
|
|
120
|
+
return null;
|
|
121
|
+
const usage = extractUsage(data);
|
|
122
|
+
return {
|
|
123
|
+
type: 'session_info',
|
|
124
|
+
sessionId: data.session_id,
|
|
125
|
+
usage,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Extract usage info, dynamically handling any model name key
|
|
130
|
+
*/
|
|
131
|
+
function extractUsage(data) {
|
|
132
|
+
// Try modelUsage first (dynamic keys)
|
|
133
|
+
if (data.modelUsage) {
|
|
134
|
+
const firstKey = Object.keys(data.modelUsage)[0];
|
|
135
|
+
const modelUsage = firstKey ? data.modelUsage[firstKey] : undefined;
|
|
136
|
+
if (modelUsage) {
|
|
137
|
+
return {
|
|
138
|
+
inputTokens: modelUsage.inputTokens,
|
|
139
|
+
outputTokens: modelUsage.outputTokens,
|
|
140
|
+
contextWindow: modelUsage.contextWindow,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Fallback to top-level usage
|
|
145
|
+
if (data.usage) {
|
|
146
|
+
return {
|
|
147
|
+
inputTokens: data.usage.input_tokens,
|
|
148
|
+
outputTokens: data.usage.output_tokens,
|
|
149
|
+
contextWindow: 0,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=output-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-parser.js","sourceRoot":"","sources":["../../src/ai/output-parser.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA4BH,+BAA+B;AAC/B,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAEtC;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,iBAAiB,CACtC,MAA6B;IAE7B,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC7B,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3D,IAAI,CAAC,WAAW;QAAE,OAAO;IAEzB,yCAAyC;IACzC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC3E,MAAM,YAAY,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAE9C,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,KAAK,MAAM,KAAK,IAAI,YAAY,EAAE,CAAC;YACjC,MAAM,KAAK,CAAC;QACd,CAAC;QACD,OAAO;IACT,CAAC;IAED,qCAAqC;IACrC,MAAM,WAAW,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;IACpD,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,WAAW,CAAC;QAClB,OAAO;IACT,CAAC;IAED,gCAAgC;IAChC,MAAM;QACJ,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,WAAW;KACrB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,iBAAiB,CAAC,KAAe;IACxC,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,IAAI,YAAY,GAAG,KAAK,CAAC;IAEzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAqB,CAAC;YAClD,YAAY,GAAG,IAAI,CAAC;YACpB,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,KAAK;gBAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,oCAAoC;QACtC,CAAC;IACH,CAAC;IAED,OAAO,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,MAAc;IACxC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAqB,CAAC;QACpD,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,cAAc,CAAC,IAAsB;IAC5C,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,OAAO;oBACL,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,IAAI,CAAC,MAAM,IAAI,gCAAgC;iBACvD,CAAC;YACJ,CAAC;YAED,+DAA+D;YAC/D,iDAAiD;YACjD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,OAAO;oBACL,IAAI,EAAE,SAAS;oBACf,OAAO,EAAE,IAAI,CAAC,MAAM;oBACpB,SAAS,EAAE,IAAI,CAAC,UAAU;oBAC1B,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC;iBAC1B,CAAC;YACJ,CAAC;YAED,kCAAkC;YAClC,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC;QAED,KAAK,WAAW,CAAC,CAAC,CAAC;YACjB,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC5C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;YACpD,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAED;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,IAAsB;IAChD,IAAI,CAAC,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAElC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,SAAS,EAAE,IAAI,CAAC,UAAU;QAC1B,KAAK;KACN,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,IAAsB;IAC1C,sCAAsC;IACtC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACpB,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACpE,IAAI,UAAU,EAAE,CAAC;YACf,OAAO;gBACL,WAAW,EAAE,UAAU,CAAC,WAAW;gBACnC,YAAY,EAAE,UAAU,CAAC,YAAY;gBACrC,aAAa,EAAE,UAAU,CAAC,aAAa;aACxC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,8BAA8B;IAC9B,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,OAAO;YACL,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY;YACpC,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa;YACtC,aAAa,EAAE,CAAC;SACjB,CAAC;IACJ,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages AI sessions with lazy creation and 30-minute stale cleanup.
|
|
5
|
+
* Sessions are persisted to .loom/sessions/ as JSON files.
|
|
6
|
+
*/
|
|
7
|
+
import type { AIUsage } from '@loom-framework/core';
|
|
8
|
+
/** On-disk session record */
|
|
9
|
+
export interface SessionRecord {
|
|
10
|
+
id: string;
|
|
11
|
+
claudeSessionId?: string;
|
|
12
|
+
title: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
messages: SessionMessage[];
|
|
16
|
+
usage?: AIUsage;
|
|
17
|
+
}
|
|
18
|
+
export interface SessionMessage {
|
|
19
|
+
id: string;
|
|
20
|
+
role: 'user' | 'assistant';
|
|
21
|
+
content: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
files?: SessionFileRef[];
|
|
24
|
+
}
|
|
25
|
+
export interface SessionFileRef {
|
|
26
|
+
uid: string;
|
|
27
|
+
name: string;
|
|
28
|
+
size: number;
|
|
29
|
+
type: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare class SessionManager {
|
|
33
|
+
private sessionsDir;
|
|
34
|
+
/** In-memory index of active sessions for stale detection */
|
|
35
|
+
private activeIndex;
|
|
36
|
+
private cleanupTimer?;
|
|
37
|
+
constructor(projectRoot: string);
|
|
38
|
+
/**
|
|
39
|
+
* Ensure sessions directory exists
|
|
40
|
+
*/
|
|
41
|
+
initialize(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Start the stale session cleanup timer
|
|
44
|
+
*/
|
|
45
|
+
startCleanup(intervalMs?: number): void;
|
|
46
|
+
/**
|
|
47
|
+
* Stop the cleanup timer
|
|
48
|
+
*/
|
|
49
|
+
stopCleanup(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Create a new session on demand (lazy, no pre-allocation)
|
|
52
|
+
*/
|
|
53
|
+
createSession(id?: string): Promise<SessionRecord>;
|
|
54
|
+
/**
|
|
55
|
+
* Read a session by ID
|
|
56
|
+
*/
|
|
57
|
+
readSession(id: string): Promise<SessionRecord | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Add messages to a session and update claudeSessionId
|
|
60
|
+
*/
|
|
61
|
+
addMessages(id: string, messages: SessionMessage[], claudeSessionId?: string, usage?: AIUsage): Promise<SessionRecord>;
|
|
62
|
+
/**
|
|
63
|
+
* List all sessions sorted by most recent first
|
|
64
|
+
*/
|
|
65
|
+
listSessions(): Promise<SessionRecord[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Delete a session
|
|
68
|
+
*/
|
|
69
|
+
deleteSession(id: string): Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* Get or create a session by ID
|
|
72
|
+
*/
|
|
73
|
+
getOrCreate(id: string): Promise<SessionRecord>;
|
|
74
|
+
/**
|
|
75
|
+
* Clean up stale sessions from the active index
|
|
76
|
+
*/
|
|
77
|
+
private cleanStaleSessions;
|
|
78
|
+
private getSessionPath;
|
|
79
|
+
private writeSessionFile;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=session-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/ai/session-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAiB,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAKnE,6BAA6B;AAC7B,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,WAAW,CAAS;IAC5B,6DAA6D;IAC7D,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,YAAY,CAAC,CAAiC;gBAE1C,WAAW,EAAE,MAAM;IAI/B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAejC;;OAEG;IACH,YAAY,CAAC,UAAU,GAAE,MAAe,GAAG,IAAI;IAM/C;;OAEG;IACH,WAAW,IAAI,IAAI;IAOnB;;OAEG;IACG,aAAa,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAsBxD;;OAEG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAU5D;;OAEG;IACG,WAAW,CACf,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,cAAc,EAAE,EAC1B,eAAe,CAAC,EAAE,MAAM,EACxB,KAAK,CAAC,EAAE,OAAO,GACd,OAAO,CAAC,aAAa,CAAC;IAqCzB;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IA2B9C;;OAEG;IACG,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWjD;;OAEG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAMrD;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,cAAc;YAIR,gBAAgB;CAK/B"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages AI sessions with lazy creation and 30-minute stale cleanup.
|
|
5
|
+
* Sessions are persisted to .loom/sessions/ as JSON files.
|
|
6
|
+
*/
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
/** Stale session threshold: 30 minutes */
|
|
10
|
+
const STALE_SESSION_MS = 30 * 60 * 1000;
|
|
11
|
+
export class SessionManager {
|
|
12
|
+
sessionsDir;
|
|
13
|
+
/** In-memory index of active sessions for stale detection */
|
|
14
|
+
activeIndex = new Map();
|
|
15
|
+
cleanupTimer;
|
|
16
|
+
constructor(projectRoot) {
|
|
17
|
+
this.sessionsDir = path.join(projectRoot, '.loom', 'sessions');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Ensure sessions directory exists
|
|
21
|
+
*/
|
|
22
|
+
async initialize() {
|
|
23
|
+
await fs.mkdir(this.sessionsDir, { recursive: true });
|
|
24
|
+
// Load existing sessions into active index
|
|
25
|
+
const sessions = await this.listSessions();
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
for (const session of sessions) {
|
|
28
|
+
this.activeIndex.set(session.id, {
|
|
29
|
+
id: session.id,
|
|
30
|
+
claudeSessionId: session.claudeSessionId,
|
|
31
|
+
lastActivity: new Date(session.updatedAt).getTime(),
|
|
32
|
+
createdAt: new Date(session.createdAt).getTime(),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Start the stale session cleanup timer
|
|
38
|
+
*/
|
|
39
|
+
startCleanup(intervalMs = 60_000) {
|
|
40
|
+
this.cleanupTimer = setInterval(() => {
|
|
41
|
+
this.cleanStaleSessions();
|
|
42
|
+
}, intervalMs);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Stop the cleanup timer
|
|
46
|
+
*/
|
|
47
|
+
stopCleanup() {
|
|
48
|
+
if (this.cleanupTimer) {
|
|
49
|
+
clearInterval(this.cleanupTimer);
|
|
50
|
+
this.cleanupTimer = undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a new session on demand (lazy, no pre-allocation)
|
|
55
|
+
*/
|
|
56
|
+
async createSession(id) {
|
|
57
|
+
const sessionId = id || generateSessionId();
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
const session = {
|
|
60
|
+
id: sessionId,
|
|
61
|
+
title: 'New Session',
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now,
|
|
64
|
+
messages: [],
|
|
65
|
+
};
|
|
66
|
+
await this.writeSessionFile(session);
|
|
67
|
+
this.activeIndex.set(sessionId, {
|
|
68
|
+
id: sessionId,
|
|
69
|
+
lastActivity: Date.now(),
|
|
70
|
+
createdAt: Date.now(),
|
|
71
|
+
});
|
|
72
|
+
return session;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read a session by ID
|
|
76
|
+
*/
|
|
77
|
+
async readSession(id) {
|
|
78
|
+
const filePath = this.getSessionPath(id);
|
|
79
|
+
try {
|
|
80
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
81
|
+
return JSON.parse(content);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Add messages to a session and update claudeSessionId
|
|
89
|
+
*/
|
|
90
|
+
async addMessages(id, messages, claudeSessionId, usage) {
|
|
91
|
+
const session = await this.readSession(id);
|
|
92
|
+
if (!session) {
|
|
93
|
+
throw new Error(`Session not found: ${id}`);
|
|
94
|
+
}
|
|
95
|
+
session.messages.push(...messages);
|
|
96
|
+
session.updatedAt = new Date().toISOString();
|
|
97
|
+
if (claudeSessionId) {
|
|
98
|
+
session.claudeSessionId = claudeSessionId;
|
|
99
|
+
}
|
|
100
|
+
if (usage) {
|
|
101
|
+
session.usage = usage;
|
|
102
|
+
}
|
|
103
|
+
// Auto-title from first user message
|
|
104
|
+
if (session.title === 'New Session' && session.messages.length > 0) {
|
|
105
|
+
const firstUser = session.messages.find((m) => m.role === 'user');
|
|
106
|
+
if (firstUser) {
|
|
107
|
+
session.title = firstUser.content.slice(0, 80) + (firstUser.content.length > 80 ? '...' : '');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await this.writeSessionFile(session);
|
|
111
|
+
// Update active index
|
|
112
|
+
this.activeIndex.set(id, {
|
|
113
|
+
id,
|
|
114
|
+
claudeSessionId: session.claudeSessionId,
|
|
115
|
+
lastActivity: Date.now(),
|
|
116
|
+
createdAt: new Date(session.createdAt).getTime(),
|
|
117
|
+
});
|
|
118
|
+
return session;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* List all sessions sorted by most recent first
|
|
122
|
+
*/
|
|
123
|
+
async listSessions() {
|
|
124
|
+
try {
|
|
125
|
+
const files = await fs.readdir(this.sessionsDir);
|
|
126
|
+
const sessions = [];
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
if (!file.endsWith('.json'))
|
|
129
|
+
continue;
|
|
130
|
+
const filePath = path.join(this.sessionsDir, file);
|
|
131
|
+
try {
|
|
132
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
133
|
+
sessions.push(JSON.parse(content));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Skip malformed files
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Sort by updatedAt descending
|
|
140
|
+
sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
141
|
+
return sessions;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Delete a session
|
|
149
|
+
*/
|
|
150
|
+
async deleteSession(id) {
|
|
151
|
+
const filePath = this.getSessionPath(id);
|
|
152
|
+
try {
|
|
153
|
+
await fs.unlink(filePath);
|
|
154
|
+
this.activeIndex.delete(id);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get or create a session by ID
|
|
163
|
+
*/
|
|
164
|
+
async getOrCreate(id) {
|
|
165
|
+
const existing = await this.readSession(id);
|
|
166
|
+
if (existing)
|
|
167
|
+
return existing;
|
|
168
|
+
return this.createSession(id);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Clean up stale sessions from the active index
|
|
172
|
+
*/
|
|
173
|
+
cleanStaleSessions() {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
for (const [id, session] of this.activeIndex) {
|
|
176
|
+
if (now - session.lastActivity > STALE_SESSION_MS) {
|
|
177
|
+
this.activeIndex.delete(id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
getSessionPath(id) {
|
|
182
|
+
return path.join(this.sessionsDir, `${id}.json`);
|
|
183
|
+
}
|
|
184
|
+
async writeSessionFile(session) {
|
|
185
|
+
const filePath = this.getSessionPath(session.id);
|
|
186
|
+
await fs.mkdir(this.sessionsDir, { recursive: true });
|
|
187
|
+
await fs.writeFile(filePath, JSON.stringify(session, null, 2), 'utf-8');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function generateSessionId() {
|
|
191
|
+
return `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=session-manager.js.map
|