@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.
Files changed (53) hide show
  1. package/dist/ai/engine.d.ts +37 -0
  2. package/dist/ai/engine.d.ts.map +1 -0
  3. package/dist/ai/engine.js +137 -0
  4. package/dist/ai/engine.js.map +1 -0
  5. package/dist/ai/index.d.ts +9 -0
  6. package/dist/ai/index.d.ts.map +1 -0
  7. package/dist/ai/index.js +7 -0
  8. package/dist/ai/index.js.map +1 -0
  9. package/dist/ai/output-parser.d.ts +19 -0
  10. package/dist/ai/output-parser.d.ts.map +1 -0
  11. package/dist/ai/output-parser.js +154 -0
  12. package/dist/ai/output-parser.js.map +1 -0
  13. package/dist/ai/session-manager.d.ts +81 -0
  14. package/dist/ai/session-manager.d.ts.map +1 -0
  15. package/dist/ai/session-manager.js +193 -0
  16. package/dist/ai/session-manager.js.map +1 -0
  17. package/dist/bin.d.ts +12 -0
  18. package/dist/bin.d.ts.map +1 -0
  19. package/dist/bin.js +75 -0
  20. package/dist/bin.js.map +1 -0
  21. package/dist/index.d.ts +61 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +153 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/observe/index.d.ts +6 -0
  26. package/dist/observe/index.d.ts.map +1 -0
  27. package/dist/observe/index.js +5 -0
  28. package/dist/observe/index.js.map +1 -0
  29. package/dist/observe/logger.d.ts +28 -0
  30. package/dist/observe/logger.d.ts.map +1 -0
  31. package/dist/observe/logger.js +80 -0
  32. package/dist/observe/logger.js.map +1 -0
  33. package/dist/observe/types.d.ts +26 -0
  34. package/dist/observe/types.d.ts.map +1 -0
  35. package/dist/observe/types.js +7 -0
  36. package/dist/observe/types.js.map +1 -0
  37. package/dist/routes/data.d.ts +13 -0
  38. package/dist/routes/data.d.ts.map +1 -0
  39. package/dist/routes/data.js +95 -0
  40. package/dist/routes/data.js.map +1 -0
  41. package/dist/routes/health.d.ts +7 -0
  42. package/dist/routes/health.d.ts.map +1 -0
  43. package/dist/routes/health.js +15 -0
  44. package/dist/routes/health.js.map +1 -0
  45. package/dist/routes/index.d.ts +6 -0
  46. package/dist/routes/index.d.ts.map +1 -0
  47. package/dist/routes/index.js +6 -0
  48. package/dist/routes/index.js.map +1 -0
  49. package/dist/websocket/index.d.ts +32 -0
  50. package/dist/websocket/index.d.ts.map +1 -0
  51. package/dist/websocket/index.js +320 -0
  52. package/dist/websocket/index.js.map +1 -0
  53. 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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * AI Module - Barrel Export
3
+ */
4
+ export { ClaudeCodeEngine } from './engine.js';
5
+ export { SessionManager } from './session-manager.js';
6
+ export { parseClaudeOutput } from './output-parser.js';
7
+ //# sourceMappingURL=index.js.map
@@ -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":"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"}
@@ -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