neuro-cli 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,144 @@
1
+ import { EventEmitter } from 'events';
2
+ export interface SessionConfig {
3
+ model?: string;
4
+ systemPrompt?: string;
5
+ workingDirectory?: string;
6
+ maxTokens?: number;
7
+ maxIterations?: number;
8
+ temperature?: number;
9
+ autoApprove?: boolean;
10
+ tags?: string[];
11
+ /** Optional cost rate overrides (USD per 1M tokens) */
12
+ inputPricePerMillion?: number;
13
+ outputPricePerMillion?: number;
14
+ }
15
+ export interface SessionInfo {
16
+ id: string;
17
+ name: string;
18
+ status: 'active' | 'idle' | 'closed';
19
+ createdAt: number;
20
+ lastActivityAt: number;
21
+ messageCount: number;
22
+ totalInputTokens: number;
23
+ totalOutputTokens: number;
24
+ tags: string[];
25
+ model: string;
26
+ }
27
+ export interface CostBreakdown {
28
+ inputTokens: number;
29
+ outputTokens: number;
30
+ inputCost: number;
31
+ outputCost: number;
32
+ totalCost: number;
33
+ currency: string;
34
+ }
35
+ export interface SessionMessage {
36
+ role: 'system' | 'user' | 'assistant' | 'tool';
37
+ content: string;
38
+ timestamp: number;
39
+ tokenCount?: number;
40
+ }
41
+ export interface InterSessionMessage {
42
+ id: string;
43
+ fromSession: string;
44
+ toSession: string;
45
+ payload: unknown;
46
+ timestamp: number;
47
+ read: boolean;
48
+ }
49
+ export declare class MultiSessionManager extends EventEmitter {
50
+ private sessions;
51
+ private currentSessionId;
52
+ private maxConcurrent;
53
+ private globalTokenBudget;
54
+ private totalTokensConsumed;
55
+ constructor(options?: {
56
+ maxConcurrent?: number;
57
+ globalTokenBudget?: number;
58
+ });
59
+ /**
60
+ * Create a new independent agent session. Returns the session ID.
61
+ */
62
+ createSession(name: string, config?: SessionConfig): Promise<string>;
63
+ /**
64
+ * Close a session by ID. Releases resources and rejects any pending replies.
65
+ */
66
+ closeSession(id: string): Promise<void>;
67
+ /**
68
+ * List all sessions (including closed ones) with summary info.
69
+ */
70
+ listSessions(): SessionInfo[];
71
+ /**
72
+ * Switch the active / focused session.
73
+ */
74
+ switchSession(id: string): void;
75
+ /**
76
+ * Get the current session ID (or null if none).
77
+ */
78
+ getCurrentSession(): string | null;
79
+ /**
80
+ * Send a prompt to a specific session and return the response.
81
+ * Simulates an LLM interaction: appends the prompt, generates a
82
+ * simulated response, and returns it.
83
+ */
84
+ sendToSession(id: string, prompt: string): Promise<string>;
85
+ /**
86
+ * Broadcast a prompt to all active sessions. Returns a map of
87
+ * session ID → response string.
88
+ */
89
+ broadcast(prompt: string): Promise<Map<string, string>>;
90
+ /**
91
+ * Get the conversation history for a session.
92
+ */
93
+ getSessionHistory(id: string): SessionMessage[];
94
+ /**
95
+ * Get the cost breakdown for a specific session.
96
+ */
97
+ getSessionCost(id: string): CostBreakdown;
98
+ /**
99
+ * Post a message from one session to another's message queue.
100
+ * The receiving session can read it via getMessageQueue().
101
+ */
102
+ postMessage(fromSession: string, toSession: string, message: unknown): void;
103
+ /**
104
+ * Get the pending message queue for a session.
105
+ */
106
+ getMessageQueue(sessionId: string): InterSessionMessage[];
107
+ /**
108
+ * Drain (retrieve and clear) the message queue for a session.
109
+ */
110
+ drainMessageQueue(sessionId: string): InterSessionMessage[];
111
+ /**
112
+ * Set the maximum number of concurrent sessions.
113
+ */
114
+ setMaxConcurrent(max: number): void;
115
+ /**
116
+ * Get total token usage across all sessions.
117
+ */
118
+ getTotalTokenUsage(): number;
119
+ /**
120
+ * Get aggregate cost across all sessions.
121
+ */
122
+ getTotalCost(): CostBreakdown;
123
+ /**
124
+ * Get the number of active sessions.
125
+ */
126
+ getActiveSessionCount(): number;
127
+ /**
128
+ * Clean up closed sessions, freeing memory.
129
+ */
130
+ cleanup(): number;
131
+ private countActive;
132
+ private findFirstActiveSession;
133
+ private recordToInfo;
134
+ /**
135
+ * Rough token estimation: ~4 characters per token.
136
+ */
137
+ private estimateTokens;
138
+ /**
139
+ * Generate a simulated LLM response for a session.
140
+ * In production, this would call the actual model API.
141
+ */
142
+ private generateSimulatedResponse;
143
+ }
144
+ //# sourceMappingURL=multi-session.d.ts.map
@@ -0,0 +1,442 @@
1
+ // ============================================================
2
+ // NeuroCLI - Multi-Session Parallel Agents (GAP-39)
3
+ // Multiple agent sessions running in parallel with independent
4
+ // state, inter-session communication, and resource management.
5
+ // Uses only Node.js built-in modules.
6
+ // ============================================================
7
+ import { EventEmitter } from 'events';
8
+ import { randomUUID } from 'crypto';
9
+ // ---- Constants ----
10
+ const DEFAULT_MAX_CONCURRENT = 10;
11
+ const DEFAULT_MAX_TOKENS = 200_000;
12
+ const DEFAULT_MAX_ITERATIONS = 50;
13
+ const DEFAULT_INPUT_PRICE = 3.0; // USD per 1M tokens
14
+ const DEFAULT_OUTPUT_PRICE = 15.0; // USD per 1M tokens
15
+ const REPLY_TIMEOUT_MS = 120_000; // 2 minutes
16
+ const TOKEN_CHARS_RATIO = 4; // ~4 characters per token (heuristic)
17
+ // ============================================================
18
+ // MultiSessionManager Class
19
+ // ============================================================
20
+ export class MultiSessionManager extends EventEmitter {
21
+ sessions = new Map();
22
+ currentSessionId = null;
23
+ maxConcurrent;
24
+ globalTokenBudget;
25
+ totalTokensConsumed = 0;
26
+ constructor(options) {
27
+ super();
28
+ this.maxConcurrent = options?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
29
+ this.globalTokenBudget = options?.globalTokenBudget ?? 0; // 0 = unlimited
30
+ }
31
+ // ============================================================
32
+ // Session Lifecycle
33
+ // ============================================================
34
+ /**
35
+ * Create a new independent agent session. Returns the session ID.
36
+ */
37
+ async createSession(name, config) {
38
+ const activeCount = this.countActive();
39
+ if (activeCount >= this.maxConcurrent) {
40
+ throw new Error(`Maximum concurrent sessions reached (${this.maxConcurrent}). ` +
41
+ `Close an existing session before creating a new one.`);
42
+ }
43
+ const id = randomUUID();
44
+ const now = Date.now();
45
+ const record = {
46
+ id,
47
+ name,
48
+ config: config ?? {},
49
+ history: [],
50
+ status: 'active',
51
+ createdAt: now,
52
+ lastActivityAt: now,
53
+ totalInputTokens: 0,
54
+ totalOutputTokens: 0,
55
+ messageQueue: [],
56
+ abortController: new AbortController(),
57
+ pendingReplies: new Map(),
58
+ };
59
+ // Add system message if a system prompt was provided
60
+ if (record.config.systemPrompt) {
61
+ record.history.push({
62
+ role: 'system',
63
+ content: record.config.systemPrompt,
64
+ timestamp: now,
65
+ });
66
+ }
67
+ this.sessions.set(id, record);
68
+ // If this is the first session, auto-switch to it
69
+ if (this.sessions.size === 1) {
70
+ this.currentSessionId = id;
71
+ }
72
+ this.emit('session:created', id, name);
73
+ return id;
74
+ }
75
+ /**
76
+ * Close a session by ID. Releases resources and rejects any pending replies.
77
+ */
78
+ async closeSession(id) {
79
+ const record = this.sessions.get(id);
80
+ if (!record) {
81
+ throw new Error(`Session "${id}" not found`);
82
+ }
83
+ // Cancel any pending operations
84
+ record.abortController.abort();
85
+ // Reject all pending replies
86
+ for (const [correlationId, pending] of record.pendingReplies) {
87
+ clearTimeout(pending.timeout);
88
+ pending.reject(new Error(`Session "${id}" was closed`));
89
+ record.pendingReplies.delete(correlationId);
90
+ }
91
+ record.status = 'closed';
92
+ record.lastActivityAt = Date.now();
93
+ // If the closed session was current, switch to another active one
94
+ if (this.currentSessionId === id) {
95
+ this.currentSessionId = this.findFirstActiveSession();
96
+ }
97
+ this.emit('session:closed', id);
98
+ }
99
+ /**
100
+ * List all sessions (including closed ones) with summary info.
101
+ */
102
+ listSessions() {
103
+ const result = [];
104
+ const records = Array.from(this.sessions.values());
105
+ for (const r of records) {
106
+ result.push(this.recordToInfo(r));
107
+ }
108
+ return result.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
109
+ }
110
+ /**
111
+ * Switch the active / focused session.
112
+ */
113
+ switchSession(id) {
114
+ const record = this.sessions.get(id);
115
+ if (!record) {
116
+ throw new Error(`Session "${id}" not found`);
117
+ }
118
+ if (record.status === 'closed') {
119
+ throw new Error(`Session "${id}" is closed and cannot be switched to`);
120
+ }
121
+ this.currentSessionId = id;
122
+ this.emit('session:switched', id);
123
+ }
124
+ /**
125
+ * Get the current session ID (or null if none).
126
+ */
127
+ getCurrentSession() {
128
+ return this.currentSessionId;
129
+ }
130
+ // ============================================================
131
+ // Session Communication
132
+ // ============================================================
133
+ /**
134
+ * Send a prompt to a specific session and return the response.
135
+ * Simulates an LLM interaction: appends the prompt, generates a
136
+ * simulated response, and returns it.
137
+ */
138
+ async sendToSession(id, prompt) {
139
+ const record = this.sessions.get(id);
140
+ if (!record) {
141
+ throw new Error(`Session "${id}" not found`);
142
+ }
143
+ if (record.status === 'closed') {
144
+ throw new Error(`Session "${id}" is closed`);
145
+ }
146
+ // Check global token budget
147
+ if (this.globalTokenBudget > 0 && this.totalTokensConsumed >= this.globalTokenBudget) {
148
+ throw new Error('Global token budget exhausted. Cannot send more messages.');
149
+ }
150
+ const now = Date.now();
151
+ // Append user message
152
+ const inputTokenEstimate = this.estimateTokens(prompt);
153
+ record.history.push({
154
+ role: 'user',
155
+ content: prompt,
156
+ timestamp: now,
157
+ tokenCount: inputTokenEstimate,
158
+ });
159
+ record.totalInputTokens += inputTokenEstimate;
160
+ this.totalTokensConsumed += inputTokenEstimate;
161
+ // Simulate assistant response (in production this would call an LLM)
162
+ const response = this.generateSimulatedResponse(record, prompt);
163
+ const outputTokenEstimate = this.estimateTokens(response);
164
+ record.history.push({
165
+ role: 'assistant',
166
+ content: response,
167
+ timestamp: Date.now(),
168
+ tokenCount: outputTokenEstimate,
169
+ });
170
+ record.totalOutputTokens += outputTokenEstimate;
171
+ this.totalTokensConsumed += outputTokenEstimate;
172
+ record.lastActivityAt = Date.now();
173
+ record.status = 'active';
174
+ this.emit('session:message', id, { role: 'user', content: prompt });
175
+ this.emit('session:message', id, { role: 'assistant', content: response });
176
+ return response;
177
+ }
178
+ /**
179
+ * Broadcast a prompt to all active sessions. Returns a map of
180
+ * session ID → response string.
181
+ */
182
+ async broadcast(prompt) {
183
+ const results = new Map();
184
+ const activeIds = [];
185
+ const records = Array.from(this.sessions.values());
186
+ for (const record of records) {
187
+ if (record.status !== 'closed') {
188
+ activeIds.push(record.id);
189
+ }
190
+ }
191
+ // Send to all active sessions concurrently
192
+ const promises = activeIds.map(async (id) => {
193
+ try {
194
+ const response = await this.sendToSession(id, prompt);
195
+ results.set(id, response);
196
+ }
197
+ catch (err) {
198
+ results.set(id, `ERROR: ${err instanceof Error ? err.message : String(err)}`);
199
+ }
200
+ });
201
+ await Promise.allSettled(promises);
202
+ this.emit('broadcast', prompt, activeIds);
203
+ return results;
204
+ }
205
+ // ============================================================
206
+ // Session History & Cost
207
+ // ============================================================
208
+ /**
209
+ * Get the conversation history for a session.
210
+ */
211
+ getSessionHistory(id) {
212
+ const record = this.sessions.get(id);
213
+ if (!record) {
214
+ throw new Error(`Session "${id}" not found`);
215
+ }
216
+ return [...record.history];
217
+ }
218
+ /**
219
+ * Get the cost breakdown for a specific session.
220
+ */
221
+ getSessionCost(id) {
222
+ const record = this.sessions.get(id);
223
+ if (!record) {
224
+ throw new Error(`Session "${id}" not found`);
225
+ }
226
+ const inputPrice = record.config.inputPricePerMillion ?? DEFAULT_INPUT_PRICE;
227
+ const outputPrice = record.config.outputPricePerMillion ?? DEFAULT_OUTPUT_PRICE;
228
+ const inputCost = (record.totalInputTokens / 1_000_000) * inputPrice;
229
+ const outputCost = (record.totalOutputTokens / 1_000_000) * outputPrice;
230
+ return {
231
+ inputTokens: record.totalInputTokens,
232
+ outputTokens: record.totalOutputTokens,
233
+ inputCost: Math.round(inputCost * 10000) / 10000,
234
+ outputCost: Math.round(outputCost * 10000) / 10000,
235
+ totalCost: Math.round((inputCost + outputCost) * 10000) / 10000,
236
+ currency: 'USD',
237
+ };
238
+ }
239
+ // ============================================================
240
+ // Inter-Session Messaging
241
+ // ============================================================
242
+ /**
243
+ * Post a message from one session to another's message queue.
244
+ * The receiving session can read it via getMessageQueue().
245
+ */
246
+ postMessage(fromSession, toSession, message) {
247
+ const fromRecord = this.sessions.get(fromSession);
248
+ if (!fromRecord) {
249
+ throw new Error(`Source session "${fromSession}" not found`);
250
+ }
251
+ const toRecord = this.sessions.get(toSession);
252
+ if (!toRecord) {
253
+ throw new Error(`Target session "${toSession}" not found`);
254
+ }
255
+ if (toRecord.status === 'closed') {
256
+ throw new Error(`Target session "${toSession}" is closed`);
257
+ }
258
+ const msg = {
259
+ id: randomUUID(),
260
+ fromSession,
261
+ toSession,
262
+ payload: message,
263
+ timestamp: Date.now(),
264
+ read: false,
265
+ };
266
+ toRecord.messageQueue.push(msg);
267
+ toRecord.lastActivityAt = Date.now();
268
+ this.emit('session:intermessage', fromSession, toSession, msg);
269
+ }
270
+ /**
271
+ * Get the pending message queue for a session.
272
+ */
273
+ getMessageQueue(sessionId) {
274
+ const record = this.sessions.get(sessionId);
275
+ if (!record) {
276
+ throw new Error(`Session "${sessionId}" not found`);
277
+ }
278
+ // Mark all as read
279
+ const messages = [...record.messageQueue];
280
+ for (const msg of messages) {
281
+ msg.read = true;
282
+ }
283
+ return messages;
284
+ }
285
+ /**
286
+ * Drain (retrieve and clear) the message queue for a session.
287
+ */
288
+ drainMessageQueue(sessionId) {
289
+ const record = this.sessions.get(sessionId);
290
+ if (!record) {
291
+ throw new Error(`Session "${sessionId}" not found`);
292
+ }
293
+ const messages = [...record.messageQueue];
294
+ for (const msg of messages) {
295
+ msg.read = true;
296
+ }
297
+ record.messageQueue = [];
298
+ return messages;
299
+ }
300
+ // ============================================================
301
+ // Resource Management
302
+ // ============================================================
303
+ /**
304
+ * Set the maximum number of concurrent sessions.
305
+ */
306
+ setMaxConcurrent(max) {
307
+ if (max < 1) {
308
+ throw new Error('Max concurrent sessions must be at least 1');
309
+ }
310
+ this.maxConcurrent = max;
311
+ this.emit('config:maxConcurrent', max);
312
+ }
313
+ /**
314
+ * Get total token usage across all sessions.
315
+ */
316
+ getTotalTokenUsage() {
317
+ return this.totalTokensConsumed;
318
+ }
319
+ /**
320
+ * Get aggregate cost across all sessions.
321
+ */
322
+ getTotalCost() {
323
+ let totalInputTokens = 0;
324
+ let totalOutputTokens = 0;
325
+ let totalInputCost = 0;
326
+ let totalOutputCost = 0;
327
+ const records = Array.from(this.sessions.values());
328
+ for (const record of records) {
329
+ const inputPrice = record.config.inputPricePerMillion ?? DEFAULT_INPUT_PRICE;
330
+ const outputPrice = record.config.outputPricePerMillion ?? DEFAULT_OUTPUT_PRICE;
331
+ totalInputTokens += record.totalInputTokens;
332
+ totalOutputTokens += record.totalOutputTokens;
333
+ totalInputCost += (record.totalInputTokens / 1_000_000) * inputPrice;
334
+ totalOutputCost += (record.totalOutputTokens / 1_000_000) * outputPrice;
335
+ }
336
+ return {
337
+ inputTokens: totalInputTokens,
338
+ outputTokens: totalOutputTokens,
339
+ inputCost: Math.round(totalInputCost * 10000) / 10000,
340
+ outputCost: Math.round(totalOutputCost * 10000) / 10000,
341
+ totalCost: Math.round((totalInputCost + totalOutputCost) * 10000) / 10000,
342
+ currency: 'USD',
343
+ };
344
+ }
345
+ /**
346
+ * Get the number of active sessions.
347
+ */
348
+ getActiveSessionCount() {
349
+ return this.countActive();
350
+ }
351
+ /**
352
+ * Clean up closed sessions, freeing memory.
353
+ */
354
+ cleanup() {
355
+ let cleaned = 0;
356
+ const entries = Array.from(this.sessions.entries());
357
+ for (const [id, record] of entries) {
358
+ if (record.status === 'closed') {
359
+ // Reject any remaining pending replies
360
+ for (const [, pending] of record.pendingReplies) {
361
+ clearTimeout(pending.timeout);
362
+ pending.reject(new Error('Session cleaned up'));
363
+ }
364
+ this.sessions.delete(id);
365
+ cleaned++;
366
+ }
367
+ }
368
+ this.emit('cleanup', cleaned);
369
+ return cleaned;
370
+ }
371
+ // ============================================================
372
+ // Private Helpers
373
+ // ============================================================
374
+ countActive() {
375
+ let count = 0;
376
+ const records = Array.from(this.sessions.values());
377
+ for (const record of records) {
378
+ if (record.status !== 'closed') {
379
+ count++;
380
+ }
381
+ }
382
+ return count;
383
+ }
384
+ findFirstActiveSession() {
385
+ const records = Array.from(this.sessions.values());
386
+ for (const record of records) {
387
+ if (record.status !== 'closed') {
388
+ return record.id;
389
+ }
390
+ }
391
+ return null;
392
+ }
393
+ recordToInfo(record) {
394
+ return {
395
+ id: record.id,
396
+ name: record.name,
397
+ status: record.status,
398
+ createdAt: record.createdAt,
399
+ lastActivityAt: record.lastActivityAt,
400
+ messageCount: record.history.filter((m) => m.role !== 'system').length,
401
+ totalInputTokens: record.totalInputTokens,
402
+ totalOutputTokens: record.totalOutputTokens,
403
+ tags: record.config.tags ?? [],
404
+ model: record.config.model ?? 'default',
405
+ };
406
+ }
407
+ /**
408
+ * Rough token estimation: ~4 characters per token.
409
+ */
410
+ estimateTokens(text) {
411
+ return Math.ceil(text.length / TOKEN_CHARS_RATIO);
412
+ }
413
+ /**
414
+ * Generate a simulated LLM response for a session.
415
+ * In production, this would call the actual model API.
416
+ */
417
+ generateSimulatedResponse(record, prompt) {
418
+ const sessionName = record.name;
419
+ const historyLength = record.history.length;
420
+ const maxIterations = record.config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
421
+ // Build context from recent history (last 5 non-system messages)
422
+ const recentMessages = record.history
423
+ .filter((m) => m.role !== 'system')
424
+ .slice(-5);
425
+ const contextSummary = recentMessages
426
+ .map((m) => `[${m.role}]: ${m.content.substring(0, 80)}...`)
427
+ .join('\n');
428
+ const lines = [
429
+ `[Session "${sessionName}" — response #${Math.floor(historyLength / 2)}]`,
430
+ '',
431
+ `Received prompt (${prompt.length} chars). Context window contains ${historyLength} messages.`,
432
+ '',
433
+ `Recent context:`,
434
+ contextSummary,
435
+ '',
436
+ `Remaining iterations: ${Math.max(0, maxIterations - Math.floor(historyLength / 2))}`,
437
+ `Session tokens: ${record.totalInputTokens + record.totalOutputTokens} (est.)`,
438
+ ];
439
+ return lines.join('\n');
440
+ }
441
+ }
442
+ //# sourceMappingURL=multi-session.js.map
@@ -0,0 +1,125 @@
1
+ export type ImageFormat = 'png' | 'jpeg' | 'gif' | 'webp';
2
+ export interface MultimodalConfig {
3
+ /** Max image file size in bytes (default: 20MB) */
4
+ maxImageSize: number;
5
+ /** Supported formats */
6
+ supportedFormats: ImageFormat[];
7
+ /** Whether to auto-detect images in prompts */
8
+ autoDetectImages: boolean;
9
+ /** Whether to resize large images */
10
+ autoResize: boolean;
11
+ /** Maximum dimension for resized images (default: 2048) */
12
+ maxDimension: number;
13
+ /** Image quality for JPEG compression (0-100) */
14
+ jpegQuality: number;
15
+ }
16
+ export interface ImageContent {
17
+ /** Base64 encoded image data */
18
+ base64: string;
19
+ /** MIME type */
20
+ mimeType: string;
21
+ /** Original file path */
22
+ filePath: string;
23
+ /** File size in bytes */
24
+ size: number;
25
+ /** Image dimensions if available */
26
+ width?: number;
27
+ height?: number;
28
+ /** Format */
29
+ format: ImageFormat;
30
+ }
31
+ export interface MultimodalMessage {
32
+ role: 'user' | 'assistant';
33
+ content: MultimodalContentPart[];
34
+ }
35
+ export type MultimodalContentPart = {
36
+ type: 'text';
37
+ text: string;
38
+ } | {
39
+ type: 'image_url';
40
+ image_url: {
41
+ url: string;
42
+ detail?: 'auto' | 'low' | 'high';
43
+ };
44
+ };
45
+ export interface VisionModelInfo {
46
+ modelId: string;
47
+ supportsVision: boolean;
48
+ maxImages: number;
49
+ }
50
+ export declare class MultimodalSupport {
51
+ private config;
52
+ private loadedImages;
53
+ constructor(config?: Partial<MultimodalConfig>);
54
+ /**
55
+ * Load an image from a file path and return base64 encoded content
56
+ */
57
+ loadImage(filePath: string): ImageContent;
58
+ /**
59
+ * Load multiple images from file paths
60
+ */
61
+ loadImages(filePaths: string[]): ImageContent[];
62
+ /**
63
+ * Build a multimodal message with text and images
64
+ */
65
+ buildMessage(text: string, images: ImageContent[], detail?: 'auto' | 'low' | 'high'): MultimodalMessage;
66
+ /**
67
+ * Build a multimodal message from text and image file paths
68
+ */
69
+ buildMessageFromPaths(text: string, imagePaths: string[], detail?: 'auto' | 'low' | 'high'): MultimodalMessage;
70
+ /**
71
+ * Detect image references in a text prompt
72
+ * Supports: @image:path, ![alt](path), file paths ending in image extensions
73
+ */
74
+ detectImageReferences(text: string): string[];
75
+ /**
76
+ * Process a user prompt that may contain image references
77
+ * Returns the cleaned text and loaded images
78
+ */
79
+ processPrompt(text: string): {
80
+ text: string;
81
+ images: ImageContent[];
82
+ };
83
+ /**
84
+ * Check if a model supports vision
85
+ */
86
+ isVisionModel(modelId: string): boolean;
87
+ /**
88
+ * Get vision model info
89
+ */
90
+ getVisionModelInfo(modelId: string): VisionModelInfo | null;
91
+ /**
92
+ * Check if a file is a supported image
93
+ */
94
+ isImageFile(filePath: string): boolean;
95
+ /**
96
+ * Get the format of an image file
97
+ */
98
+ getImageFormat(filePath: string): ImageFormat | null;
99
+ /**
100
+ * Get base64 data URL for an image
101
+ */
102
+ getDataUrl(image: ImageContent): string;
103
+ /**
104
+ * Get all loaded images
105
+ */
106
+ getLoadedImages(): ImageContent[];
107
+ /**
108
+ * Clear the image cache
109
+ */
110
+ clearCache(): void;
111
+ /**
112
+ * Get supported formats
113
+ */
114
+ getSupportedFormats(): ImageFormat[];
115
+ /**
116
+ * Get config
117
+ */
118
+ getConfig(): MultimodalConfig;
119
+ /**
120
+ * Print multimodal status
121
+ */
122
+ printStatus(): void;
123
+ private resolvePath;
124
+ }
125
+ //# sourceMappingURL=multimodal.d.ts.map