minovative-mind-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +418 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/chat.d.ts +7 -0
- package/dist/commands/chat.js +30 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +18 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.js +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/agent-tools.d.ts +36 -0
- package/dist/services/agent-tools.js +764 -0
- package/dist/services/agent.d.ts +21 -0
- package/dist/services/agent.js +648 -0
- package/dist/services/ai.d.ts +60 -0
- package/dist/services/ai.js +331 -0
- package/dist/services/auth.d.ts +3 -0
- package/dist/services/auth.js +183 -0
- package/dist/services/changeLogger.d.ts +23 -0
- package/dist/services/changeLogger.js +57 -0
- package/dist/services/contextAgent.d.ts +20 -0
- package/dist/services/contextAgent.js +440 -0
- package/dist/services/proxyClient.d.ts +21 -0
- package/dist/services/proxyClient.js +119 -0
- package/dist/services/verificationService.d.ts +10 -0
- package/dist/services/verificationService.js +148 -0
- package/dist/utils/atomicWrite.d.ts +6 -0
- package/dist/utils/atomicWrite.js +29 -0
- package/dist/utils/config.d.ts +17 -0
- package/dist/utils/config.js +17 -0
- package/dist/utils/contextPrompts.d.ts +3 -0
- package/dist/utils/contextPrompts.js +34 -0
- package/dist/utils/dependencyTracer.d.ts +48 -0
- package/dist/utils/dependencyTracer.js +647 -0
- package/dist/utils/excludedExtensions.d.ts +8 -0
- package/dist/utils/excludedExtensions.js +125 -0
- package/dist/utils/fuzzyMatch.d.ts +21 -0
- package/dist/utils/fuzzyMatch.js +121 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +17 -0
- package/dist/utils/pathSecurity.d.ts +10 -0
- package/dist/utils/pathSecurity.js +26 -0
- package/dist/utils/symbolExtractor.d.ts +6 -0
- package/dist/utils/symbolExtractor.js +249 -0
- package/dist/utils/syntaxValidator.d.ts +5 -0
- package/dist/utils/syntaxValidator.js +81 -0
- package/dist/utils/systemPrompts.d.ts +5 -0
- package/dist/utils/systemPrompts.js +119 -0
- package/oclif.manifest.json +69 -0
- package/package.json +81 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { FunctionCall } from '@google/generative-ai';
|
|
2
|
+
export declare class ProxyChatSession {
|
|
3
|
+
private history;
|
|
4
|
+
private modelName;
|
|
5
|
+
private systemInstruction;
|
|
6
|
+
private tools;
|
|
7
|
+
private generationConfig;
|
|
8
|
+
constructor(modelName: string, systemInstruction: string, tools: any[], generationConfig: any);
|
|
9
|
+
setAgentConfig(systemInstruction: string, tools: any[]): void;
|
|
10
|
+
setModel(modelName: string): void;
|
|
11
|
+
getModel(): string;
|
|
12
|
+
clearHistory(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Retrieves the most recent conversation history as a formatted string.
|
|
15
|
+
* Useful for passing conversation context to stateless background agents.
|
|
16
|
+
* @param turns Number of back-and-forth turns (user + model pair = 1 turn) to retrieve.
|
|
17
|
+
*/
|
|
18
|
+
getRecentHistory(turns?: number): string;
|
|
19
|
+
/**
|
|
20
|
+
* Prunes the history to prevent unbounded memory growth.
|
|
21
|
+
* Keeps the most recent MAX_HISTORY_ENTRIES entries, preserving
|
|
22
|
+
* conversational context while preventing OOM crashes.
|
|
23
|
+
*/
|
|
24
|
+
private pruneHistory;
|
|
25
|
+
sendMessage(message: string | Array<{
|
|
26
|
+
functionResponse: {
|
|
27
|
+
name: string;
|
|
28
|
+
response: any;
|
|
29
|
+
};
|
|
30
|
+
}>, additionalText?: string, abortSignal?: AbortSignal): Promise<{
|
|
31
|
+
response: {
|
|
32
|
+
text: () => string;
|
|
33
|
+
functionCalls: () => FunctionCall[] | undefined;
|
|
34
|
+
usageMetadata?: () => any;
|
|
35
|
+
groundingMetadata?: () => any;
|
|
36
|
+
};
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates a new shared multi-turn chat session via the proxy.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createSharedChatSession(): any;
|
|
43
|
+
export declare function getGeneralChatConfig(): {
|
|
44
|
+
systemInstruction: string;
|
|
45
|
+
tools: {
|
|
46
|
+
googleSearch: {};
|
|
47
|
+
}[];
|
|
48
|
+
};
|
|
49
|
+
export declare function getPlanExecutionConfig(): {
|
|
50
|
+
systemInstruction: string;
|
|
51
|
+
tools: {
|
|
52
|
+
functionDeclarations: import("@google/generative-ai").FunctionDeclaration[];
|
|
53
|
+
}[];
|
|
54
|
+
};
|
|
55
|
+
export declare const CONTEXT_AGENT_MODEL: "gemini-2.5-flash";
|
|
56
|
+
export declare function createContextAgentSession(): any;
|
|
57
|
+
export declare const INTENT_ROUTER_MODEL: "gemini-2.5-flash";
|
|
58
|
+
export declare function createIntentRouterSession(): any;
|
|
59
|
+
export declare const WEB_SEARCH_AGENT_MODEL: "gemini-2.5-flash";
|
|
60
|
+
export declare function createWebSearchAgentSession(): any;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { DEFAULT_MODEL, GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../utils/config.js';
|
|
2
|
+
import { toolDeclarations } from './agent-tools.js';
|
|
3
|
+
import { ProxyClient } from './proxyClient.js';
|
|
4
|
+
import { getAuthorizedIdToken } from './auth.js';
|
|
5
|
+
import { GENERAL_CHAT_INSTRUCTION, PLAN_EXECUTION_INSTRUCTION, CONTEXT_SYSTEM_INSTRUCTION, INTENT_ROUTER_SYSTEM_INSTRUCTION, WEB_SEARCH_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
|
|
6
|
+
// ─── System Prompts ──────────────────────────────────────────────────
|
|
7
|
+
// Prompts have been moved to src/utils/systemPrompts.ts
|
|
8
|
+
// ─── AI Service ──────────────────────────────────────────────────────
|
|
9
|
+
const proxyClient = new ProxyClient();
|
|
10
|
+
// ─── History Limits ──────────────────────────────────────────────────
|
|
11
|
+
/** Maximum number of Content entries to keep in the sliding history window. */
|
|
12
|
+
const MAX_HISTORY_ENTRIES = 40;
|
|
13
|
+
/**
|
|
14
|
+
* Maximum character length for a single Part's text content.
|
|
15
|
+
* Anything beyond this is truncated with an ellipsis marker so the proxy
|
|
16
|
+
* payload stays within sane memory bounds.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_PART_TEXT_LENGTH = 60_000;
|
|
19
|
+
function truncatePartText(text) {
|
|
20
|
+
if (text.length <= MAX_PART_TEXT_LENGTH)
|
|
21
|
+
return text;
|
|
22
|
+
return text.substring(0, MAX_PART_TEXT_LENGTH) + '\n... (output truncated to prevent memory overflow)';
|
|
23
|
+
}
|
|
24
|
+
export class ProxyChatSession {
|
|
25
|
+
history = [];
|
|
26
|
+
modelName;
|
|
27
|
+
systemInstruction;
|
|
28
|
+
tools;
|
|
29
|
+
generationConfig;
|
|
30
|
+
constructor(modelName, systemInstruction, tools, generationConfig) {
|
|
31
|
+
this.modelName = modelName;
|
|
32
|
+
this.systemInstruction = systemInstruction;
|
|
33
|
+
this.tools = tools;
|
|
34
|
+
this.generationConfig = generationConfig;
|
|
35
|
+
}
|
|
36
|
+
setAgentConfig(systemInstruction, tools) {
|
|
37
|
+
this.systemInstruction = systemInstruction;
|
|
38
|
+
this.tools = tools;
|
|
39
|
+
}
|
|
40
|
+
setModel(modelName) {
|
|
41
|
+
this.modelName = modelName;
|
|
42
|
+
}
|
|
43
|
+
getModel() {
|
|
44
|
+
return this.modelName;
|
|
45
|
+
}
|
|
46
|
+
clearHistory() {
|
|
47
|
+
this.history = [];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Retrieves the most recent conversation history as a formatted string.
|
|
51
|
+
* Useful for passing conversation context to stateless background agents.
|
|
52
|
+
* @param turns Number of back-and-forth turns (user + model pair = 1 turn) to retrieve.
|
|
53
|
+
*/
|
|
54
|
+
getRecentHistory(turns = 2) {
|
|
55
|
+
if (this.history.length === 0)
|
|
56
|
+
return '';
|
|
57
|
+
// History is an array of Content { role: 'user' | 'model', parts: Part[] }
|
|
58
|
+
// A single turn is generally 2 items (user, then model). Sometimes tools are interspersed.
|
|
59
|
+
// We'll just grab the last N * 2 items.
|
|
60
|
+
const recentItems = this.history.slice(-(turns * 2));
|
|
61
|
+
let formattedHistory = '';
|
|
62
|
+
for (const item of recentItems) {
|
|
63
|
+
const role = item.role === 'user' ? 'User' : 'Assistant';
|
|
64
|
+
const textParts = item.parts.map(p => p.text).filter(Boolean);
|
|
65
|
+
if (textParts.length > 0) {
|
|
66
|
+
formattedHistory += `[${role}]: ${textParts.join(' ')}\n`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return formattedHistory.trim();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Prunes the history to prevent unbounded memory growth.
|
|
73
|
+
* Keeps the most recent MAX_HISTORY_ENTRIES entries, preserving
|
|
74
|
+
* conversational context while preventing OOM crashes.
|
|
75
|
+
*/
|
|
76
|
+
pruneHistory() {
|
|
77
|
+
if (this.history.length > MAX_HISTORY_ENTRIES) {
|
|
78
|
+
// Always keep pairs aligned (user/model), so trim from the front
|
|
79
|
+
const excess = this.history.length - MAX_HISTORY_ENTRIES;
|
|
80
|
+
// Round up to the nearest even number to keep user/model pairs intact
|
|
81
|
+
const trimCount = excess % 2 === 0 ? excess : excess + 1;
|
|
82
|
+
this.history = this.history.slice(trimCount);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async sendMessage(message, additionalText, abortSignal) {
|
|
86
|
+
const idToken = await getAuthorizedIdToken();
|
|
87
|
+
if (!idToken) {
|
|
88
|
+
throw new Error('You are not signed in. Please run `minovative-mind-cli login` first.');
|
|
89
|
+
}
|
|
90
|
+
// Convert message to Part, truncating text to prevent memory blowout
|
|
91
|
+
let newParts;
|
|
92
|
+
if (typeof message === 'string') {
|
|
93
|
+
newParts = [{ text: truncatePartText(message) }];
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// It's an array of function responses — truncate output strings
|
|
97
|
+
newParts = message.map((m) => ({
|
|
98
|
+
functionResponse: {
|
|
99
|
+
name: m.functionResponse.name,
|
|
100
|
+
response: {
|
|
101
|
+
...m.functionResponse.response,
|
|
102
|
+
output: typeof m.functionResponse.response.output === 'string'
|
|
103
|
+
? truncatePartText(m.functionResponse.response.output)
|
|
104
|
+
: m.functionResponse.response.output,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
if (additionalText) {
|
|
110
|
+
newParts.push({ text: truncatePartText(additionalText) });
|
|
111
|
+
}
|
|
112
|
+
this.history.push({
|
|
113
|
+
role: 'user',
|
|
114
|
+
parts: newParts,
|
|
115
|
+
});
|
|
116
|
+
// Prune old history before sending to keep payload bounded
|
|
117
|
+
this.pruneHistory();
|
|
118
|
+
const effectiveGenerationConfig = { ...this.generationConfig };
|
|
119
|
+
const result = await proxyClient.generateFunctionCallViaProxy(idToken, this.modelName, this.history, this.tools, undefined, this.systemInstruction, effectiveGenerationConfig, undefined, abortSignal);
|
|
120
|
+
// Append model response to history
|
|
121
|
+
const modelParts = [];
|
|
122
|
+
if (result.thought || (result.parts && result.parts.some((p) => p.text))) {
|
|
123
|
+
// Prioritize parts if available (from SSE parser), fallback to thought
|
|
124
|
+
const textPart = result.parts?.find((p) => p.text)?.text || result.thought || '';
|
|
125
|
+
if (textPart) {
|
|
126
|
+
modelParts.push({ text: textPart });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (result.functionCalls && result.functionCalls.length > 0) {
|
|
130
|
+
result.functionCalls.forEach((fc) => {
|
|
131
|
+
modelParts.push({ functionCall: fc });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (modelParts.length > 0) {
|
|
135
|
+
this.history.push({
|
|
136
|
+
role: 'model',
|
|
137
|
+
parts: modelParts,
|
|
138
|
+
});
|
|
139
|
+
// Prune again after model response to stay bounded
|
|
140
|
+
this.pruneHistory();
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
response: {
|
|
144
|
+
text: () => result.thought || result.parts?.find((p) => p.text)?.text || '',
|
|
145
|
+
functionCalls: () => (result.functionCalls.length > 0 ? result.functionCalls : undefined),
|
|
146
|
+
usageMetadata: () => result.usageMetadata,
|
|
147
|
+
groundingMetadata: () => result.groundingMetadata,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Creates a new shared multi-turn chat session via the proxy.
|
|
154
|
+
*/
|
|
155
|
+
export function createSharedChatSession() {
|
|
156
|
+
return new ProxyChatSession(DEFAULT_MODEL, GENERAL_CHAT_INSTRUCTION, [], {
|
|
157
|
+
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
158
|
+
temperature: 1,
|
|
159
|
+
topP: 0.95,
|
|
160
|
+
topK: 40,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
export function getGeneralChatConfig() {
|
|
164
|
+
return {
|
|
165
|
+
systemInstruction: GENERAL_CHAT_INSTRUCTION,
|
|
166
|
+
tools: [{ googleSearch: {} }],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function getPlanExecutionConfig() {
|
|
170
|
+
return {
|
|
171
|
+
systemInstruction: PLAN_EXECUTION_INSTRUCTION,
|
|
172
|
+
tools: [{ functionDeclarations: toolDeclarations }],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// ─── Context Agent Service ───────────────────────────────────────────
|
|
176
|
+
export const CONTEXT_AGENT_MODEL = DEFAULT_MODEL;
|
|
177
|
+
export function createContextAgentSession() {
|
|
178
|
+
const contextTools = [
|
|
179
|
+
{
|
|
180
|
+
functionDeclarations: [
|
|
181
|
+
{
|
|
182
|
+
name: 'select_files',
|
|
183
|
+
description: 'Select files to investigate further.',
|
|
184
|
+
parameters: {
|
|
185
|
+
type: 'OBJECT',
|
|
186
|
+
properties: {
|
|
187
|
+
files: {
|
|
188
|
+
type: 'ARRAY',
|
|
189
|
+
items: { type: 'STRING' },
|
|
190
|
+
description: 'Array of file paths to read',
|
|
191
|
+
},
|
|
192
|
+
reasoning: { type: 'STRING', description: 'Why you selected these files' },
|
|
193
|
+
},
|
|
194
|
+
required: ['files', 'reasoning'],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'search_codebase',
|
|
199
|
+
description: 'Search the codebase for a pattern.',
|
|
200
|
+
parameters: {
|
|
201
|
+
type: 'OBJECT',
|
|
202
|
+
properties: {
|
|
203
|
+
pattern: { type: 'STRING', description: 'Pattern to search for' },
|
|
204
|
+
fileGlob: { type: 'STRING', description: 'Optional glob to filter files' },
|
|
205
|
+
},
|
|
206
|
+
required: ['pattern'],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'read_file',
|
|
211
|
+
description: 'Read a single file. Use startLine and endLine to read specific chunks of massive files.',
|
|
212
|
+
parameters: {
|
|
213
|
+
type: 'OBJECT',
|
|
214
|
+
properties: {
|
|
215
|
+
filePath: { type: 'STRING', description: 'Path to the file' },
|
|
216
|
+
startLine: { type: 'NUMBER', description: 'Optional. 1-indexed starting line number.' },
|
|
217
|
+
endLine: { type: 'NUMBER', description: 'Optional. 1-indexed ending line number (inclusive).' },
|
|
218
|
+
targetElements: {
|
|
219
|
+
type: 'ARRAY',
|
|
220
|
+
items: { type: 'STRING' },
|
|
221
|
+
description: 'Optional. An array of specific function names, class names, or variables to extract. The tool will intelligently locate and return only the blocks defining these elements.',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ['filePath'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'list_directory',
|
|
229
|
+
description: 'List the contents of a directory. Returns files, folders, and their sizes.',
|
|
230
|
+
parameters: {
|
|
231
|
+
type: 'OBJECT',
|
|
232
|
+
properties: {
|
|
233
|
+
dirPath: { type: 'STRING', description: 'Path to the directory to list (e.g., "src/components")' },
|
|
234
|
+
maxDepth: { type: 'NUMBER', description: 'Maximum depth to traverse (default 1)' },
|
|
235
|
+
},
|
|
236
|
+
required: ['dirPath'],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'find_dependencies',
|
|
241
|
+
description: 'Trace the dependency graph for a file. Returns what the file imports (forward) and what files import it (reverse). Critical for understanding the blast radius before modifying, deleting, or renaming a file.',
|
|
242
|
+
parameters: {
|
|
243
|
+
type: 'OBJECT',
|
|
244
|
+
properties: {
|
|
245
|
+
filePath: { type: 'STRING', description: 'Relative path to the file to trace dependencies for.' },
|
|
246
|
+
direction: {
|
|
247
|
+
type: 'STRING',
|
|
248
|
+
description: 'Direction to trace: "both" (default), "forward", or "reverse".',
|
|
249
|
+
},
|
|
250
|
+
maxDepth: {
|
|
251
|
+
type: 'NUMBER',
|
|
252
|
+
description: 'Maximum traversal depth (default 3, max 5).',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
required: ['filePath'],
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'find_recent_changes',
|
|
260
|
+
description: 'Find files that have been modified recently within the workspace. Useful for understanding what the user was just working on if they ask vague questions like "why is it failing?". Automatically ignores .git, node_modules, etc.',
|
|
261
|
+
parameters: {
|
|
262
|
+
type: 'OBJECT',
|
|
263
|
+
properties: {
|
|
264
|
+
dirPath: {
|
|
265
|
+
type: 'STRING',
|
|
266
|
+
description: 'Relative path to directory to search from. Defaults to workspace root ".".',
|
|
267
|
+
},
|
|
268
|
+
minutes: {
|
|
269
|
+
type: 'NUMBER',
|
|
270
|
+
description: 'Look for files modified within this many minutes. Defaults to 60.',
|
|
271
|
+
},
|
|
272
|
+
maxDepth: {
|
|
273
|
+
type: 'NUMBER',
|
|
274
|
+
description: 'Maximum depth to traverse. Defaults to 5.',
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'finish_investigation',
|
|
281
|
+
description: 'Call this when you have gathered enough context.',
|
|
282
|
+
parameters: {
|
|
283
|
+
type: 'OBJECT',
|
|
284
|
+
properties: {
|
|
285
|
+
summary: { type: 'STRING', description: 'Summary of your findings' },
|
|
286
|
+
relevantFiles: {
|
|
287
|
+
type: 'ARRAY',
|
|
288
|
+
items: { type: 'STRING' },
|
|
289
|
+
description: 'Paths of files that are relevant to the user request',
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
required: ['summary', 'relevantFiles'],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: 'perform_web_search',
|
|
297
|
+
description: 'Search the web for documentation, solutions, or real-time information.',
|
|
298
|
+
parameters: {
|
|
299
|
+
type: 'OBJECT',
|
|
300
|
+
properties: {
|
|
301
|
+
query: { type: 'STRING', description: 'The search query to look up on the web' },
|
|
302
|
+
},
|
|
303
|
+
required: ['query'],
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
return new ProxyChatSession(CONTEXT_AGENT_MODEL, CONTEXT_SYSTEM_INSTRUCTION, contextTools, {
|
|
310
|
+
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
311
|
+
temperature: 1,
|
|
312
|
+
topP: 0.95,
|
|
313
|
+
topK: 40,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// ─── Intent Router Service ───────────────────────────────────────────
|
|
317
|
+
export const INTENT_ROUTER_MODEL = GEMINI_MODELS.FLASH_LATEST;
|
|
318
|
+
export function createIntentRouterSession() {
|
|
319
|
+
return new ProxyChatSession(INTENT_ROUTER_MODEL, INTENT_ROUTER_SYSTEM_INSTRUCTION, [], // no tools
|
|
320
|
+
{ temperature: 0, responseMimeType: 'application/json' });
|
|
321
|
+
}
|
|
322
|
+
// ─── Web Search Agent Service ───────────────────────────────────────────
|
|
323
|
+
export const WEB_SEARCH_AGENT_MODEL = DEFAULT_MODEL;
|
|
324
|
+
export function createWebSearchAgentSession() {
|
|
325
|
+
return new ProxyChatSession(WEB_SEARCH_AGENT_MODEL, WEB_SEARCH_SYSTEM_INSTRUCTION, [{ googleSearch: {} }], {
|
|
326
|
+
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
327
|
+
temperature: 1,
|
|
328
|
+
topP: 0.95,
|
|
329
|
+
topK: 40,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { FIREBASE_API_KEY, GITHUB_CLIENT_ID } from '../utils/config.js';
|
|
5
|
+
const CONFIG_FILE = path.join(os.homedir(), '.minovative-mind-cli.json');
|
|
6
|
+
function getAuthData() {
|
|
7
|
+
try {
|
|
8
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
9
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
// ignore
|
|
14
|
+
}
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
function saveAuthData(data) {
|
|
18
|
+
const current = getAuthData();
|
|
19
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...data }, null, 2));
|
|
20
|
+
}
|
|
21
|
+
export async function login() {
|
|
22
|
+
try {
|
|
23
|
+
// 1. Start GitHub Device Flow
|
|
24
|
+
const deviceFlowRes = await fetch('https://github.com/login/device/code', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
Accept: 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
client_id: GITHUB_CLIENT_ID,
|
|
32
|
+
scope: 'user:email',
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (!deviceFlowRes.ok) {
|
|
36
|
+
throw new Error('Failed to initiate GitHub Device Flow.');
|
|
37
|
+
}
|
|
38
|
+
const deviceData = await deviceFlowRes.json();
|
|
39
|
+
console.log(`\nPlease open: ${deviceData.verification_uri}`);
|
|
40
|
+
console.log(`And enter the code: ${deviceData.user_code}\n`);
|
|
41
|
+
// 2. Poll for the token
|
|
42
|
+
let githubAccessToken = null;
|
|
43
|
+
const pollInterval = deviceData.interval * 1000;
|
|
44
|
+
const maxAttempts = 20;
|
|
45
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
47
|
+
const pollRes = await fetch('https://github.com/login/oauth/access_token', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
Accept: 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
client_id: GITHUB_CLIENT_ID,
|
|
55
|
+
device_code: deviceData.device_code,
|
|
56
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
const pollData = await pollRes.json();
|
|
60
|
+
if (pollData.access_token) {
|
|
61
|
+
githubAccessToken = pollData.access_token;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
else if (pollData.error === 'authorization_pending') {
|
|
65
|
+
process.stdout.write('.');
|
|
66
|
+
}
|
|
67
|
+
else if (pollData.error === 'slow_down') {
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
throw new Error(`GitHub auth failed: ${pollData.error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!githubAccessToken) {
|
|
75
|
+
throw new Error('Timed out waiting for GitHub authorization.');
|
|
76
|
+
}
|
|
77
|
+
console.log('\nGitHub authorization successful!');
|
|
78
|
+
// 3. Check if account exists
|
|
79
|
+
const existsResponse = await fetch('https://exchangegithubtoken-6obg3e4zwa-uc.a.run.app', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
accessToken: githubAccessToken,
|
|
84
|
+
checkOnly: true,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
if (!existsResponse.ok) {
|
|
88
|
+
throw new Error('Failed to verify account status.');
|
|
89
|
+
}
|
|
90
|
+
const { exists } = (await existsResponse.json());
|
|
91
|
+
if (!exists) {
|
|
92
|
+
console.log('\nWelcome! It looks like you do not have a Minovative Mind account yet.');
|
|
93
|
+
console.log('Please sign up on our website first: https://minovativemind.dev/auth/signin\n');
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
// 4. Account exists — proceed with full sign-in
|
|
97
|
+
const firebaseTokenResponse = await exchangeGithubTokenForFirebase(githubAccessToken);
|
|
98
|
+
if (!firebaseTokenResponse.idToken) {
|
|
99
|
+
throw new Error('Failed to get Firebase ID token.');
|
|
100
|
+
}
|
|
101
|
+
// 5. Store the Firebase token securely
|
|
102
|
+
saveAuthData({
|
|
103
|
+
idToken: firebaseTokenResponse.idToken,
|
|
104
|
+
refreshToken: firebaseTokenResponse.refreshToken,
|
|
105
|
+
idTokenExpiry: Date.now() + 50 * 60 * 1000, // 50 mins
|
|
106
|
+
});
|
|
107
|
+
console.log('Successfully signed in to Minovative Mind!');
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.error('\nSign in failed:', err.message);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function exchangeGithubTokenForFirebase(githubAccessToken) {
|
|
116
|
+
const exchangeUrl = 'https://exchangegithubtoken-6obg3e4zwa-uc.a.run.app';
|
|
117
|
+
const exchangeResponse = await fetch(exchangeUrl, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ accessToken: githubAccessToken }),
|
|
121
|
+
});
|
|
122
|
+
if (!exchangeResponse.ok) {
|
|
123
|
+
const errorText = await exchangeResponse.text();
|
|
124
|
+
throw new Error(`Token Exchange Failed: ${errorText}`);
|
|
125
|
+
}
|
|
126
|
+
const { customToken } = (await exchangeResponse.json());
|
|
127
|
+
const signInUrl = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`;
|
|
128
|
+
const signInResponse = await fetch(signInUrl, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
token: customToken,
|
|
133
|
+
returnSecureToken: true,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
if (!signInResponse.ok) {
|
|
137
|
+
const errorData = await signInResponse.json();
|
|
138
|
+
throw new Error(`Firebase Custom Token Auth Error: \${JSON.stringify(errorData)}`);
|
|
139
|
+
}
|
|
140
|
+
return await signInResponse.json();
|
|
141
|
+
}
|
|
142
|
+
export function logout() {
|
|
143
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
144
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
145
|
+
}
|
|
146
|
+
console.log('Successfully signed out.');
|
|
147
|
+
}
|
|
148
|
+
export async function getAuthorizedIdToken() {
|
|
149
|
+
const data = getAuthData();
|
|
150
|
+
if (!data.idToken)
|
|
151
|
+
return undefined;
|
|
152
|
+
if (data.idTokenExpiry && Date.now() < data.idTokenExpiry) {
|
|
153
|
+
return data.idToken;
|
|
154
|
+
}
|
|
155
|
+
// Token expired, attempt refresh
|
|
156
|
+
if (!data.refreshToken)
|
|
157
|
+
return undefined;
|
|
158
|
+
try {
|
|
159
|
+
const url = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
|
|
160
|
+
const response = await fetch(url, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
163
|
+
body: `grant_type=refresh_token&refresh_token=${data.refreshToken}`,
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw new Error('Refresh token expired or invalid');
|
|
167
|
+
}
|
|
168
|
+
const refreshData = (await response.json());
|
|
169
|
+
const newIdToken = refreshData.id_token;
|
|
170
|
+
if (newIdToken) {
|
|
171
|
+
saveAuthData({
|
|
172
|
+
idToken: newIdToken,
|
|
173
|
+
refreshToken: refreshData.refresh_token || data.refreshToken,
|
|
174
|
+
idTokenExpiry: Date.now() + 50 * 60 * 1000,
|
|
175
|
+
});
|
|
176
|
+
return newIdToken;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
// If refresh fails, they need to sign in again
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface FileChange {
|
|
2
|
+
filePath: string;
|
|
3
|
+
originalContent: string | null;
|
|
4
|
+
action: 'create' | 'modify' | 'delete';
|
|
5
|
+
}
|
|
6
|
+
export interface ChangeSet {
|
|
7
|
+
timestamp: number;
|
|
8
|
+
description: string;
|
|
9
|
+
changes: FileChange[];
|
|
10
|
+
}
|
|
11
|
+
declare class ChangeLogger {
|
|
12
|
+
private changeStack;
|
|
13
|
+
private currentChangeSet;
|
|
14
|
+
startChangeSet(description: string): void;
|
|
15
|
+
logChange(filePath: string, originalContent: string | null, action: 'create' | 'modify' | 'delete'): void;
|
|
16
|
+
commitChangeSet(): void;
|
|
17
|
+
getLastChangeSet(): ChangeSet | null;
|
|
18
|
+
getCurrentChangeSet(): ChangeSet | null;
|
|
19
|
+
popLastChangeSet(): ChangeSet | null;
|
|
20
|
+
hasChanges(): boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare const changeLogger: ChangeLogger;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class ChangeLogger {
|
|
2
|
+
changeStack = [];
|
|
3
|
+
currentChangeSet = null;
|
|
4
|
+
startChangeSet(description) {
|
|
5
|
+
if (this.currentChangeSet) {
|
|
6
|
+
this.commitChangeSet();
|
|
7
|
+
}
|
|
8
|
+
this.currentChangeSet = {
|
|
9
|
+
timestamp: Date.now(),
|
|
10
|
+
description,
|
|
11
|
+
changes: [],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
logChange(filePath, originalContent, action) {
|
|
15
|
+
if (!this.currentChangeSet) {
|
|
16
|
+
// Create a default changeset if none was started explicitly
|
|
17
|
+
this.startChangeSet('Anonymous change');
|
|
18
|
+
}
|
|
19
|
+
// Check if we already have a change for this file in the current set
|
|
20
|
+
const existingChangeIndex = this.currentChangeSet.changes.findIndex((c) => c.filePath === filePath);
|
|
21
|
+
if (existingChangeIndex >= 0) {
|
|
22
|
+
// If we already logged a change for this file, we only care about the *first* original state
|
|
23
|
+
// (the state before this entire changeset began). So we don't update originalContent.
|
|
24
|
+
// We might update the action (e.g., create then modify is still fundamentally a create from
|
|
25
|
+
// the perspective of before the changeset).
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.currentChangeSet.changes.push({
|
|
29
|
+
filePath,
|
|
30
|
+
originalContent,
|
|
31
|
+
action,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
commitChangeSet() {
|
|
35
|
+
if (this.currentChangeSet && this.currentChangeSet.changes.length > 0) {
|
|
36
|
+
this.changeStack.push(this.currentChangeSet);
|
|
37
|
+
}
|
|
38
|
+
this.currentChangeSet = null;
|
|
39
|
+
}
|
|
40
|
+
getLastChangeSet() {
|
|
41
|
+
if (this.changeStack.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
return this.changeStack[this.changeStack.length - 1];
|
|
44
|
+
}
|
|
45
|
+
getCurrentChangeSet() {
|
|
46
|
+
return this.currentChangeSet;
|
|
47
|
+
}
|
|
48
|
+
popLastChangeSet() {
|
|
49
|
+
if (this.changeStack.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
return this.changeStack.pop() || null;
|
|
52
|
+
}
|
|
53
|
+
hasChanges() {
|
|
54
|
+
return this.changeStack.length > 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export const changeLogger = new ChangeLogger();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ContextAgentResult {
|
|
2
|
+
projectTree: string;
|
|
3
|
+
projectType: string;
|
|
4
|
+
relevantFiles: Map<string, string>;
|
|
5
|
+
summary: string;
|
|
6
|
+
webSearchSummary?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface IntentRoute {
|
|
9
|
+
needsContext: boolean;
|
|
10
|
+
targetAgent: 'CHAT' | 'EXECUTE';
|
|
11
|
+
}
|
|
12
|
+
export declare function routeIntent(userRequest: string, chatHistory?: string): Promise<IntentRoute>;
|
|
13
|
+
export declare function gatherContext(workspaceRoot: string, userRequest: string, chatHistory: string | undefined, inputHandler: {
|
|
14
|
+
getAndClear: () => string;
|
|
15
|
+
waitForPrompt: () => Promise<void>;
|
|
16
|
+
}, abortSignal: AbortSignal, onProgress?: (msg: string) => void): Promise<{
|
|
17
|
+
contextResult: ContextAgentResult | null;
|
|
18
|
+
targetAgent: 'CHAT' | 'EXECUTE';
|
|
19
|
+
chainedMessages: string[];
|
|
20
|
+
}>;
|