minovative-mind-cli 2.10.0 → 2.11.2

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 (34) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/chat.d.ts +1 -1
  3. package/dist/commands/chat.js +2 -1
  4. package/dist/services/agent/commandApproval.js +5 -2
  5. package/dist/services/agent/slashCommands.js +213 -51
  6. package/dist/services/agent-tools.d.ts +4 -3
  7. package/dist/services/agent-tools.js +32 -79
  8. package/dist/services/agent.d.ts +5 -6
  9. package/dist/services/agent.js +38 -15
  10. package/dist/services/ai.d.ts +25 -0
  11. package/dist/services/ai.js +253 -2
  12. package/dist/services/chatHistoryService.d.ts +95 -2
  13. package/dist/services/chatHistoryService.js +236 -9
  14. package/dist/services/contextAgent.js +184 -89
  15. package/dist/services/orchestration/investigationAgent.js +100 -84
  16. package/dist/services/orchestration/investigationOrchestrator.js +6 -2
  17. package/dist/services/orchestration/orchestrator.js +6 -3
  18. package/dist/services/orchestration/scopedTools.js +5 -0
  19. package/dist/services/orchestration/subAgent.d.ts +31 -1
  20. package/dist/services/orchestration/subAgent.js +153 -2
  21. package/dist/services/userProfileService.d.ts +97 -0
  22. package/dist/services/userProfileService.js +410 -0
  23. package/dist/utils/analysisRunner.d.ts +29 -0
  24. package/dist/utils/analysisRunner.js +200 -5
  25. package/dist/utils/contextPrompts.d.ts +19 -3
  26. package/dist/utils/contextPrompts.js +144 -26
  27. package/dist/utils/historyPrompt.d.ts +92 -1
  28. package/dist/utils/historyPrompt.js +166 -2
  29. package/dist/utils/symbolExtractor.d.ts +12 -0
  30. package/dist/utils/symbolExtractor.js +946 -0
  31. package/dist/utils/systemPrompts.d.ts +6 -4
  32. package/dist/utils/systemPrompts.js +77 -9
  33. package/oclif.manifest.json +2 -2
  34. package/package.json +1 -1
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @file userProfileService.ts
3
+ * @description Global Adaptive User Profile & Persona Memory Bank service for Minovative Mind CLI.
4
+ *
5
+ * Persists learned user traits, communication preferences, idea formulation styles, and agent observations
6
+ * globally at `~/.minovativemind/user_profile.json`.
7
+ *
8
+ * Features:
9
+ * - Zero-latency, non-blocking asynchronous insight extraction via `gemini-3.5-flash-lite`.
10
+ * - Bounded memory management (capped at 25 side-notes max) and atomic write operations.
11
+ * - Dynamic XML/Markdown prompt context formatting for real-time personalization.
12
+ * - Full transparency and data control for the user via `/profile` slash command.
13
+ */
14
+ /**
15
+ * Communication style traits observed by the agent.
16
+ */
17
+ export interface UserProfileCommunicationStyle {
18
+ tonePreference?: string;
19
+ verbosity?: string;
20
+ formulationStyle?: string;
21
+ }
22
+ /**
23
+ * Cognitive problem-solving, collaboration, and decision-making traits observed by the agent.
24
+ */
25
+ export interface UserProfileCognitiveTraits {
26
+ architecturalStyle?: string;
27
+ decisionPreference?: string;
28
+ riskTolerance?: string;
29
+ delegationDepth?: string;
30
+ debuggingStyle?: string;
31
+ explanationFormat?: string;
32
+ }
33
+ /**
34
+ * Technical habits and technology strengths observed by the agent.
35
+ */
36
+ export interface UserProfileTechnicalPreferences {
37
+ strengths?: string[];
38
+ conventions?: string[];
39
+ }
40
+ /**
41
+ * Global User Profile representation.
42
+ */
43
+ export interface UserProfile {
44
+ communicationStyle?: UserProfileCommunicationStyle;
45
+ cognitiveTraits?: UserProfileCognitiveTraits;
46
+ technicalPreferences?: UserProfileTechnicalPreferences;
47
+ agentNotes: string[];
48
+ lastUpdated?: number;
49
+ }
50
+ /** Maximum number of side-notes retained to prevent unbounded memory growth */
51
+ export declare const MAX_AGENT_SIDE_NOTES = 25;
52
+ /** Global directory path for .minovativemind data */
53
+ export declare function getGlobalMinovativeMindDir(): string;
54
+ /** Global file path for user_profile.json */
55
+ export declare function getUserProfilePath(): string;
56
+ /**
57
+ * Ensures that the global ~/.minovativemind directory exists with safe permissions.
58
+ */
59
+ export declare function ensureGlobalStorage(): void;
60
+ /**
61
+ * Loads the user profile from disk or returns a fresh default structure.
62
+ */
63
+ export declare function loadUserProfile(): Promise<UserProfile>;
64
+ /**
65
+ * Saves the user profile to disk using atomic temporary file write pattern.
66
+ */
67
+ export declare function saveUserProfile(profile: UserProfile): Promise<void>;
68
+ /**
69
+ * Deletes a specific side-note by index.
70
+ *
71
+ * @param index - The 0-based index of the side note to remove.
72
+ * @returns Promise resolving to true if deleted, false if index out of bounds.
73
+ */
74
+ export declare function deleteSideNote(index: number): Promise<boolean>;
75
+ /**
76
+ * Clears the user profile memory and all AI-generated side-notes.
77
+ */
78
+ export declare function clearUserProfile(): Promise<void>;
79
+ /**
80
+ * Formats the user profile into a lightweight prompt context injection string (<user_profile> block).
81
+ * Returns empty string if no meaningful profile traits or notes exist yet.
82
+ */
83
+ export declare function formatUserProfileForContext(profile: UserProfile): string;
84
+ /**
85
+ * Uses a dedicated Flash-Lite classifier agent with structured JSON output
86
+ * to determine whether a turn is trivial/low-signal (greetings, affirmations, confirmations, terminal commands)
87
+ * or contains substantive personal/technical traits.
88
+ */
89
+ export declare function classifyTurnSignal(userPrompt: string, aiResponse: string, abortSignal?: AbortSignal): Promise<{
90
+ isTrivial: boolean;
91
+ reason?: string;
92
+ }>;
93
+ /**
94
+ * Analyzes dialogue from a completed turn using Flash-Lite to extract subtle user observations
95
+ * and update the persistent global user profile in the background.
96
+ */
97
+ export declare function extractAndSaveUserInsights(userPrompt: string, aiResponse: string, abortSignal?: AbortSignal): Promise<UserProfile | null>;
@@ -0,0 +1,410 @@
1
+ /**
2
+ * @file userProfileService.ts
3
+ * @description Global Adaptive User Profile & Persona Memory Bank service for Minovative Mind CLI.
4
+ *
5
+ * Persists learned user traits, communication preferences, idea formulation styles, and agent observations
6
+ * globally at `~/.minovativemind/user_profile.json`.
7
+ *
8
+ * Features:
9
+ * - Zero-latency, non-blocking asynchronous insight extraction via `gemini-3.5-flash-lite`.
10
+ * - Bounded memory management (capped at 25 side-notes max) and atomic write operations.
11
+ * - Dynamic XML/Markdown prompt context formatting for real-time personalization.
12
+ * - Full transparency and data control for the user via `/profile` slash command.
13
+ */
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import * as os from 'os';
17
+ import { debugLog } from '../utils/logger.js';
18
+ import { createUserProfileExtractorSession, createTrivialMessageClassifierSession, } from './ai.js';
19
+ /** Maximum number of side-notes retained to prevent unbounded memory growth */
20
+ export const MAX_AGENT_SIDE_NOTES = 25;
21
+ /** Global directory path for .minovativemind data */
22
+ export function getGlobalMinovativeMindDir() {
23
+ return path.join(os.homedir(), '.minovativemind');
24
+ }
25
+ /** Global file path for user_profile.json */
26
+ export function getUserProfilePath() {
27
+ return path.join(getGlobalMinovativeMindDir(), 'user_profile.json');
28
+ }
29
+ /**
30
+ * Ensures that the global ~/.minovativemind directory exists with safe permissions.
31
+ */
32
+ export function ensureGlobalStorage() {
33
+ const globalDir = getGlobalMinovativeMindDir();
34
+ if (!fs.existsSync(globalDir)) {
35
+ try {
36
+ fs.mkdirSync(globalDir, { recursive: true, mode: 0o700 });
37
+ }
38
+ catch (err) {
39
+ debugLog(`Failed to create global directory ${globalDir}: ${err}`);
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * Loads the user profile from disk or returns a fresh default structure.
45
+ */
46
+ export async function loadUserProfile() {
47
+ ensureGlobalStorage();
48
+ const profilePath = getUserProfilePath();
49
+ if (!fs.existsSync(profilePath)) {
50
+ return {
51
+ communicationStyle: {},
52
+ cognitiveTraits: {},
53
+ technicalPreferences: { strengths: [], conventions: [] },
54
+ agentNotes: [],
55
+ lastUpdated: Date.now(),
56
+ };
57
+ }
58
+ try {
59
+ const raw = await fs.promises.readFile(profilePath, 'utf-8');
60
+ const parsed = JSON.parse(raw);
61
+ return {
62
+ communicationStyle: parsed.communicationStyle || {},
63
+ cognitiveTraits: parsed.cognitiveTraits || {},
64
+ technicalPreferences: {
65
+ strengths: Array.isArray(parsed.technicalPreferences?.strengths) ? parsed.technicalPreferences.strengths : [],
66
+ conventions: Array.isArray(parsed.technicalPreferences?.conventions)
67
+ ? parsed.technicalPreferences.conventions
68
+ : [],
69
+ },
70
+ agentNotes: Array.isArray(parsed.agentNotes) ? parsed.agentNotes : [],
71
+ lastUpdated: parsed.lastUpdated || Date.now(),
72
+ };
73
+ }
74
+ catch (err) {
75
+ debugLog(`Failed to read user profile: ${err}`);
76
+ return {
77
+ communicationStyle: {},
78
+ cognitiveTraits: {},
79
+ technicalPreferences: { strengths: [], conventions: [] },
80
+ agentNotes: [],
81
+ lastUpdated: Date.now(),
82
+ };
83
+ }
84
+ }
85
+ /**
86
+ * Saves the user profile to disk using atomic temporary file write pattern.
87
+ */
88
+ export async function saveUserProfile(profile) {
89
+ ensureGlobalStorage();
90
+ const profilePath = getUserProfilePath();
91
+ const tempPath = `${profilePath}.tmp.${Date.now()}`;
92
+ profile.lastUpdated = Date.now();
93
+ // Ensure side-notes do not exceed maximum cap
94
+ if (profile.agentNotes.length > MAX_AGENT_SIDE_NOTES) {
95
+ profile.agentNotes = profile.agentNotes.slice(-MAX_AGENT_SIDE_NOTES);
96
+ }
97
+ const payload = JSON.stringify(profile, null, 2);
98
+ try {
99
+ await fs.promises.writeFile(tempPath, payload, { encoding: 'utf-8', mode: 0o600 });
100
+ await fs.promises.rename(tempPath, profilePath);
101
+ debugLog(`User profile saved successfully to ${profilePath}`);
102
+ }
103
+ catch (err) {
104
+ debugLog(`Failed to save user profile: ${err}`);
105
+ if (fs.existsSync(tempPath)) {
106
+ try {
107
+ await fs.promises.unlink(tempPath);
108
+ }
109
+ catch {
110
+ // ignore cleanup error
111
+ }
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * Deletes a specific side-note by index.
117
+ *
118
+ * @param index - The 0-based index of the side note to remove.
119
+ * @returns Promise resolving to true if deleted, false if index out of bounds.
120
+ */
121
+ export async function deleteSideNote(index) {
122
+ const profile = await loadUserProfile();
123
+ if (index < 0 || index >= profile.agentNotes.length) {
124
+ return false;
125
+ }
126
+ profile.agentNotes.splice(index, 1);
127
+ await saveUserProfile(profile);
128
+ return true;
129
+ }
130
+ /**
131
+ * Clears the user profile memory and all AI-generated side-notes.
132
+ */
133
+ export async function clearUserProfile() {
134
+ const emptyProfile = {
135
+ communicationStyle: {},
136
+ technicalPreferences: { strengths: [], conventions: [] },
137
+ agentNotes: [],
138
+ lastUpdated: Date.now(),
139
+ };
140
+ await saveUserProfile(emptyProfile);
141
+ }
142
+ /**
143
+ * Formats the user profile into a lightweight prompt context injection string (<user_profile> block).
144
+ * Returns empty string if no meaningful profile traits or notes exist yet.
145
+ */
146
+ export function formatUserProfileForContext(profile) {
147
+ const hasStyle = profile.communicationStyle?.tonePreference ||
148
+ profile.communicationStyle?.verbosity ||
149
+ profile.communicationStyle?.formulationStyle;
150
+ const hasCognitive = profile.cognitiveTraits?.architecturalStyle ||
151
+ profile.cognitiveTraits?.decisionPreference ||
152
+ profile.cognitiveTraits?.riskTolerance ||
153
+ profile.cognitiveTraits?.delegationDepth ||
154
+ profile.cognitiveTraits?.debuggingStyle ||
155
+ profile.cognitiveTraits?.explanationFormat;
156
+ const hasStrengths = profile.technicalPreferences?.strengths && profile.technicalPreferences.strengths.length > 0;
157
+ const hasConventions = profile.technicalPreferences?.conventions && profile.technicalPreferences.conventions.length > 0;
158
+ const hasNotes = profile.agentNotes && profile.agentNotes.length > 0;
159
+ if (!hasStyle && !hasCognitive && !hasStrengths && !hasConventions && !hasNotes) {
160
+ return '';
161
+ }
162
+ let block = '<user_profile>\n';
163
+ block +=
164
+ 'The following are continuous, adaptive insights and preferences learned from past interactions with this user:\n';
165
+ if (profile.communicationStyle?.tonePreference) {
166
+ block += `- Tone & Demeanor: ${profile.communicationStyle.tonePreference}\n`;
167
+ }
168
+ if (profile.communicationStyle?.verbosity) {
169
+ block += `- Output & Verbosity: ${profile.communicationStyle.verbosity}\n`;
170
+ }
171
+ if (profile.communicationStyle?.formulationStyle) {
172
+ block += `- Prompting & Formulation: ${profile.communicationStyle.formulationStyle}\n`;
173
+ }
174
+ if (profile.cognitiveTraits?.architecturalStyle) {
175
+ block += `- Architectural Orientation: ${profile.cognitiveTraits.architecturalStyle}\n`;
176
+ }
177
+ if (profile.cognitiveTraits?.decisionPreference) {
178
+ block += `- Decision Autonomy: ${profile.cognitiveTraits.decisionPreference}\n`;
179
+ }
180
+ if (profile.cognitiveTraits?.riskTolerance) {
181
+ block += `- Risk & Velocity: ${profile.cognitiveTraits.riskTolerance}\n`;
182
+ }
183
+ if (profile.cognitiveTraits?.delegationDepth) {
184
+ block += `- Delegation Depth: ${profile.cognitiveTraits.delegationDepth}\n`;
185
+ }
186
+ if (profile.cognitiveTraits?.debuggingStyle) {
187
+ block += `- Debugging Style: ${profile.cognitiveTraits.debuggingStyle}\n`;
188
+ }
189
+ if (profile.cognitiveTraits?.explanationFormat) {
190
+ block += `- Explanation Preference: ${profile.cognitiveTraits.explanationFormat}\n`;
191
+ }
192
+ if (hasStrengths) {
193
+ block += `- Technical Strengths: ${profile.technicalPreferences.strengths.join(', ')}\n`;
194
+ }
195
+ if (hasConventions) {
196
+ block += `- Coding & Architecture Conventions: ${profile.technicalPreferences.conventions.join(', ')}\n`;
197
+ }
198
+ if (hasNotes) {
199
+ block += 'Agent Side-Notes:\n';
200
+ for (const note of profile.agentNotes.slice(-10)) {
201
+ block += ` * ${note}\n`;
202
+ }
203
+ }
204
+ block +=
205
+ 'Adopt these communication, cognitive, and technical preferences naturally without ever explicitly mentioning this profile block.\n';
206
+ block += '</user_profile>';
207
+ return block;
208
+ }
209
+ /**
210
+ * Uses a dedicated Flash-Lite classifier agent with structured JSON output
211
+ * to determine whether a turn is trivial/low-signal (greetings, affirmations, confirmations, terminal commands)
212
+ * or contains substantive personal/technical traits.
213
+ */
214
+ export async function classifyTurnSignal(userPrompt, aiResponse, abortSignal) {
215
+ const trimmed = userPrompt.trim();
216
+ if (trimmed.length === 0) {
217
+ return { isTrivial: true, reason: 'Empty user prompt' };
218
+ }
219
+ if (abortSignal?.aborted) {
220
+ return { isTrivial: true, reason: 'Aborted' };
221
+ }
222
+ try {
223
+ const session = createTrivialMessageClassifierSession();
224
+ const prompt = `Classify whether this interaction contains substantive developer/persona signal or is purely trivial/low-signal.
225
+
226
+ User Message:
227
+ "${trimmed.substring(0, 1500)}"
228
+
229
+ AI Response:
230
+ "${(aiResponse || '').substring(0, 1000)}"`;
231
+ const result = await session.sendMessage(prompt, undefined, abortSignal);
232
+ const text = result.response.text()?.trim() || '{}';
233
+ const parsed = JSON.parse(text);
234
+ debugLog(`Trivial Turn Classifier Result: isTrivial=${parsed.isTrivial} (reason: ${parsed.reason})`);
235
+ return {
236
+ isTrivial: Boolean(parsed.isTrivial),
237
+ reason: parsed.reason,
238
+ };
239
+ }
240
+ catch (err) {
241
+ debugLog(`Trivial Turn Classifier failed, defaulting to false: ${err}`);
242
+ return { isTrivial: false };
243
+ }
244
+ }
245
+ /**
246
+ * Analyzes dialogue from a completed turn using Flash-Lite to extract subtle user observations
247
+ * and update the persistent global user profile in the background.
248
+ */
249
+ export async function extractAndSaveUserInsights(userPrompt, aiResponse, abortSignal) {
250
+ if (!userPrompt || userPrompt.trim().length === 0 || !aiResponse || aiResponse.length < 20) {
251
+ return null;
252
+ }
253
+ if (abortSignal?.aborted) {
254
+ return null;
255
+ }
256
+ // Evaluate with dedicated Flash-Lite Trivial Turn Classifier Agent
257
+ const classification = await classifyTurnSignal(userPrompt, aiResponse, abortSignal);
258
+ if (classification.isTrivial) {
259
+ debugLog(`Skipping profile extraction for trivial turn: ${classification.reason || 'low-signal'}`);
260
+ return null;
261
+ }
262
+ try {
263
+ const currentProfile = await loadUserProfile();
264
+ const session = createUserProfileExtractorSession();
265
+ const existingNotesText = currentProfile.agentNotes.length > 0
266
+ ? currentProfile.agentNotes.map((n, idx) => `[${idx}] ${n}`).join('\n')
267
+ : 'None recorded yet.';
268
+ const prompt = `Analyze the following interaction to observe the user's communication style, personality, thought formulation, and technical habits.
269
+ Review the existing numbered side-notes. If any new statement or habit contradicts, supersedes, or invalidates an existing note, specify its index in "deleteNoteIndices" or "updateNotes".
270
+
271
+ Existing Agent Side-Notes:
272
+ ${existingNotesText}
273
+
274
+ User Message:
275
+ "${userPrompt.substring(0, 3000)}"
276
+
277
+ AI Response Summary:
278
+ "${aiResponse.substring(0, 1500)}"`;
279
+ const result = await session.sendMessage(prompt, undefined, abortSignal);
280
+ const text = result.response.text()?.trim() || '{}';
281
+ const parsed = JSON.parse(text);
282
+ debugLog(`User Profile Extractor Parsed: ${JSON.stringify(parsed)}`);
283
+ let changed = false;
284
+ if (parsed.tonePreference && parsed.tonePreference.trim().length > 0) {
285
+ currentProfile.communicationStyle = currentProfile.communicationStyle || {};
286
+ currentProfile.communicationStyle.tonePreference = parsed.tonePreference.trim();
287
+ changed = true;
288
+ }
289
+ if (parsed.verbosity && parsed.verbosity.trim().length > 0) {
290
+ currentProfile.communicationStyle = currentProfile.communicationStyle || {};
291
+ currentProfile.communicationStyle.verbosity = parsed.verbosity.trim();
292
+ changed = true;
293
+ }
294
+ if (parsed.formulationStyle && parsed.formulationStyle.trim().length > 0) {
295
+ currentProfile.communicationStyle = currentProfile.communicationStyle || {};
296
+ currentProfile.communicationStyle.formulationStyle = parsed.formulationStyle.trim();
297
+ changed = true;
298
+ }
299
+ // Process cognitive & decision-making traits
300
+ if (parsed.cognitiveTraits && typeof parsed.cognitiveTraits === 'object') {
301
+ currentProfile.cognitiveTraits = currentProfile.cognitiveTraits || {};
302
+ const keys = [
303
+ 'architecturalStyle',
304
+ 'decisionPreference',
305
+ 'riskTolerance',
306
+ 'delegationDepth',
307
+ 'debuggingStyle',
308
+ 'explanationFormat',
309
+ ];
310
+ for (const k of keys) {
311
+ if (typeof parsed.cognitiveTraits[k] === 'string' && parsed.cognitiveTraits[k].trim().length > 0) {
312
+ currentProfile.cognitiveTraits[k] = parsed.cognitiveTraits[k].trim();
313
+ changed = true;
314
+ }
315
+ }
316
+ }
317
+ if (Array.isArray(parsed.strengths) && parsed.strengths.length > 0) {
318
+ currentProfile.technicalPreferences = currentProfile.technicalPreferences || { strengths: [], conventions: [] };
319
+ const existing = new Set(currentProfile.technicalPreferences.strengths || []);
320
+ for (const s of parsed.strengths) {
321
+ if (typeof s === 'string' && s.trim().length > 0) {
322
+ existing.add(s.trim());
323
+ changed = true;
324
+ }
325
+ }
326
+ currentProfile.technicalPreferences.strengths = Array.from(existing).slice(0, 15);
327
+ }
328
+ if (Array.isArray(parsed.removeStrengths) && parsed.removeStrengths.length > 0) {
329
+ if (currentProfile.technicalPreferences?.strengths) {
330
+ const toRemove = new Set(parsed.removeStrengths.map((s) => s.toLowerCase().trim()));
331
+ const filtered = currentProfile.technicalPreferences.strengths.filter((s) => !toRemove.has(s.toLowerCase().trim()));
332
+ if (filtered.length !== currentProfile.technicalPreferences.strengths.length) {
333
+ currentProfile.technicalPreferences.strengths = filtered;
334
+ changed = true;
335
+ }
336
+ }
337
+ }
338
+ if (Array.isArray(parsed.conventions) && parsed.conventions.length > 0) {
339
+ currentProfile.technicalPreferences = currentProfile.technicalPreferences || { strengths: [], conventions: [] };
340
+ const existing = new Set(currentProfile.technicalPreferences.conventions || []);
341
+ for (const c of parsed.conventions) {
342
+ if (typeof c === 'string' && c.trim().length > 0) {
343
+ existing.add(c.trim());
344
+ changed = true;
345
+ }
346
+ }
347
+ currentProfile.technicalPreferences.conventions = Array.from(existing).slice(0, 15);
348
+ }
349
+ if (Array.isArray(parsed.removeConventions) && parsed.removeConventions.length > 0) {
350
+ if (currentProfile.technicalPreferences?.conventions) {
351
+ const toRemove = new Set(parsed.removeConventions.map((c) => c.toLowerCase().trim()));
352
+ const filtered = currentProfile.technicalPreferences.conventions.filter((c) => !toRemove.has(c.toLowerCase().trim()));
353
+ if (filtered.length !== currentProfile.technicalPreferences.conventions.length) {
354
+ currentProfile.technicalPreferences.conventions = filtered;
355
+ changed = true;
356
+ }
357
+ }
358
+ }
359
+ // Process targeted deletions of outdated/contradicted notes (in descending order)
360
+ if (Array.isArray(parsed.deleteNoteIndices) && parsed.deleteNoteIndices.length > 0) {
361
+ const validIndices = parsed.deleteNoteIndices
362
+ .filter((idx) => typeof idx === 'number' && idx >= 0 && idx < currentProfile.agentNotes.length)
363
+ .sort((a, b) => b - a);
364
+ for (const idx of validIndices) {
365
+ currentProfile.agentNotes.splice(idx, 1);
366
+ changed = true;
367
+ }
368
+ }
369
+ // Process targeted updates to existing notes
370
+ if (Array.isArray(parsed.updateNotes) && parsed.updateNotes.length > 0) {
371
+ for (const item of parsed.updateNotes) {
372
+ if (typeof item?.index === 'number' &&
373
+ item.index >= 0 &&
374
+ item.index < currentProfile.agentNotes.length &&
375
+ typeof item?.updatedText === 'string' &&
376
+ item.updatedText.trim().length > 0) {
377
+ currentProfile.agentNotes[item.index] = item.updatedText.trim();
378
+ changed = true;
379
+ }
380
+ }
381
+ }
382
+ // Process newly added side-notes
383
+ const notesToAdd = Array.isArray(parsed.addNotes)
384
+ ? parsed.addNotes
385
+ : Array.isArray(parsed.newSideNotes)
386
+ ? parsed.newSideNotes
387
+ : [];
388
+ if (notesToAdd.length > 0) {
389
+ for (const note of notesToAdd) {
390
+ if (typeof note === 'string' && note.trim().length > 0) {
391
+ const cleanNote = note.trim();
392
+ // Avoid duplicate or near-identical notes
393
+ if (!currentProfile.agentNotes.some((existing) => existing.toLowerCase() === cleanNote.toLowerCase())) {
394
+ currentProfile.agentNotes.push(cleanNote);
395
+ changed = true;
396
+ }
397
+ }
398
+ }
399
+ }
400
+ if (changed) {
401
+ await saveUserProfile(currentProfile);
402
+ return currentProfile;
403
+ }
404
+ return null;
405
+ }
406
+ catch (err) {
407
+ debugLog(`User Profile insight extraction skipped/failed: ${err}`);
408
+ return null;
409
+ }
410
+ }
@@ -10,6 +10,8 @@ export interface EphemeralScriptOptions {
10
10
  maxOutputChars?: number;
11
11
  /** AbortSignal to cancel execution. */
12
12
  abortSignal?: AbortSignal;
13
+ /** Optional custom environment variables to merge into execution environment. */
14
+ env?: Record<string, string>;
13
15
  }
14
16
  export interface PropertyTestConfig {
15
17
  numRuns?: number;
@@ -23,10 +25,37 @@ export interface PropertyTestResult extends EphemeralScriptResult {
23
25
  seed?: number;
24
26
  numRunsCompleted?: number;
25
27
  }
28
+ /** Supported canonical language names list. */
29
+ export declare const SUPPORTED_LANGUAGES: readonly ["node", "ts-node", "python", "bash", "go", "rust", "c", "cpp", "ruby", "php", "java"];
30
+ export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
26
31
  /**
27
32
  * Normalizes user/AI provided language string to a standard runtime identifier.
28
33
  */
29
34
  export declare function normalizeLanguage(lang: string): string;
35
+ /**
36
+ * Detects programming language heuristic markers directly from script source code syntax.
37
+ * Useful when language is omitted or set to 'auto'.
38
+ *
39
+ * @param code - The source code to analyze.
40
+ * @returns Detected runtime identifier (e.g., 'python', 'rust', 'go', 'cpp', 'c', 'bash', 'ruby', 'php', 'java', 'ts-node', 'node').
41
+ */
42
+ export declare function detectLanguageFromCode(code: string): string;
43
+ /**
44
+ * Detects the dominant programming language / runtime for a workspace based on project manifest files.
45
+ *
46
+ * @param workspaceRoot - Path to the workspace root directory.
47
+ * @returns Detected runtime identifier (e.g., 'rust', 'go', 'python', 'cpp', 'ts-node', 'node').
48
+ */
49
+ export declare function detectProjectRuntime(workspaceRoot: string): Promise<string>;
50
+ /**
51
+ * Resolves the effective runtime language by combining explicit language input,
52
+ * source code syntax heuristics, and workspace project manifests.
53
+ *
54
+ * @param workspaceRoot - Path to workspace root directory.
55
+ * @param language - Optional language parameter passed by user/agent.
56
+ * @param code - Optional source code string to inspect.
57
+ */
58
+ export declare function resolveEffectiveRuntime(workspaceRoot: string, language?: string, code?: string): Promise<string>;
30
59
  /**
31
60
  * Detects whether the workspace package.json specifies `"type": "module"`.
32
61
  * Returns `'module'` or `'commonjs'`.