minovative-mind-cli 1.5.1 → 2.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.
- package/README.md +59 -45
- package/dist/commands/chat.js +10 -2
- package/dist/services/agent/slashCommands.js +369 -42
- package/dist/services/agent/toolLoop.d.ts +1 -1
- package/dist/services/agent/toolLoop.js +7 -2
- package/dist/services/agent/types.d.ts +2 -0
- package/dist/services/agent-tools.d.ts +9 -4
- package/dist/services/agent-tools.js +272 -34
- package/dist/services/agent.d.ts +8 -0
- package/dist/services/agent.js +288 -40
- package/dist/services/ai.d.ts +19 -5
- package/dist/services/ai.js +182 -36
- package/dist/services/changeLogger.d.ts +142 -0
- package/dist/services/changeLogger.js +132 -3
- package/dist/services/contextAgent.d.ts +6 -1
- package/dist/services/contextAgent.js +112 -19
- package/dist/services/embeddingIndex.d.ts +82 -0
- package/dist/services/embeddingIndex.js +613 -0
- package/dist/services/investigationComplexity.d.ts +45 -0
- package/dist/services/investigationComplexity.js +91 -0
- package/dist/services/metrics.d.ts +18 -0
- package/dist/services/metrics.js +7 -0
- package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
- package/dist/services/orchestration/fileLockRegistry.js +276 -0
- package/dist/services/orchestration/investigationAgent.d.ts +85 -0
- package/dist/services/orchestration/investigationAgent.js +362 -0
- package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
- package/dist/services/orchestration/investigationOrchestrator.js +180 -0
- package/dist/services/orchestration/messageBus.d.ts +162 -0
- package/dist/services/orchestration/messageBus.js +225 -0
- package/dist/services/orchestration/orchestrator.d.ts +45 -0
- package/dist/services/orchestration/orchestrator.js +217 -0
- package/dist/services/orchestration/readCache.d.ts +79 -0
- package/dist/services/orchestration/readCache.js +108 -0
- package/dist/services/orchestration/scopedTools.d.ts +57 -0
- package/dist/services/orchestration/scopedTools.js +172 -0
- package/dist/services/orchestration/subAgent.d.ts +58 -0
- package/dist/services/orchestration/subAgent.js +190 -0
- package/dist/services/orchestration/taskGraph.d.ts +129 -0
- package/dist/services/orchestration/taskGraph.js +254 -0
- package/dist/services/proxyClient.d.ts +25 -0
- package/dist/services/proxyClient.js +60 -0
- package/dist/services/workspaceRegistry.d.ts +137 -0
- package/dist/services/workspaceRegistry.js +270 -0
- package/dist/utils/asyncContext.d.ts +16 -0
- package/dist/utils/asyncContext.js +25 -0
- package/dist/utils/config.d.ts +3 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/contextPrompts.js +10 -3
- package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/api.js +62 -0
- package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/graph.js +23 -0
- package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
- package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
- package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
- package/dist/utils/dependencyTracer/modules/types.js +1 -0
- package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
- package/dist/utils/dependencyTracer/modules/walker.js +48 -0
- package/dist/utils/dependencyTracer.js +31 -17
- package/dist/utils/excludedExtensions.js +0 -1
- package/dist/utils/historyPrompt.d.ts +9 -0
- package/dist/utils/historyPrompt.js +87 -0
- package/dist/utils/logo.js +7 -7
- package/dist/utils/paste.d.ts +21 -0
- package/dist/utils/paste.js +22 -1
- package/dist/utils/pathSecurity.d.ts +31 -0
- package/dist/utils/pathSecurity.js +48 -0
- package/dist/utils/profiles.d.ts +2 -0
- package/dist/utils/profiles.js +44 -0
- package/dist/utils/projectStorage.js +10 -7
- package/dist/utils/systemPrompts.d.ts +6 -3
- package/dist/utils/systemPrompts.js +111 -6
- package/dist/utils/types.d.ts +33 -0
- package/dist/utils/types.js +1 -0
- package/oclif.manifest.json +2 -2
- package/package.json +4 -3
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { debugLog } from '../utils/logger.js';
|
|
4
|
+
/** Regex for valid alias names: lowercase alphanumeric, hyphens, underscores, 1–30 chars. */
|
|
5
|
+
const ALIAS_PATTERN = /^[a-z0-9][a-z0-9_-]{0,29}$/;
|
|
6
|
+
/** Global config directory for cross-project persistence. */
|
|
7
|
+
const GLOBAL_CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '~', '.minovative-mind-cli');
|
|
8
|
+
/** Path to the global workspace registry file. */
|
|
9
|
+
const REGISTRY_FILE = path.join(GLOBAL_CONFIG_DIR, 'workspaces.json');
|
|
10
|
+
/**
|
|
11
|
+
* Service that manages a global registry of external workspace roots.
|
|
12
|
+
*
|
|
13
|
+
* Workspaces are registered with short aliases (e.g., "backend") and referenced
|
|
14
|
+
* in file paths using the `@alias/path` prefix syntax. The registry is persisted
|
|
15
|
+
* globally at `~/.minovative-mind-cli/workspaces.json` so registrations carry
|
|
16
|
+
* across different primary workspaces.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* This is a singleton — use the exported `workspaceRegistry` instance.
|
|
20
|
+
* All filesystem access goes through this service to maintain the security
|
|
21
|
+
* boundary: the agent can only touch the primary workspace and explicitly
|
|
22
|
+
* registered secondary workspaces.
|
|
23
|
+
*/
|
|
24
|
+
class WorkspaceRegistry {
|
|
25
|
+
/** In-memory map of alias → registered workspace. */
|
|
26
|
+
workspaces = new Map();
|
|
27
|
+
/** Whether the registry has been loaded from disk. */
|
|
28
|
+
initialized = false;
|
|
29
|
+
/**
|
|
30
|
+
* Initializes the registry by loading persisted workspace entries from disk.
|
|
31
|
+
* Safe to call multiple times — subsequent calls are no-ops.
|
|
32
|
+
*/
|
|
33
|
+
init() {
|
|
34
|
+
if (this.initialized)
|
|
35
|
+
return;
|
|
36
|
+
this.initialized = true;
|
|
37
|
+
this.loadFromDisk();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Registers a new external workspace root with the given alias.
|
|
41
|
+
*
|
|
42
|
+
* @param alias - Short identifier for the workspace (e.g., "backend").
|
|
43
|
+
* Must be lowercase alphanumeric with hyphens/underscores, 1–30 chars.
|
|
44
|
+
* @param absolutePath - Absolute path to the workspace root directory.
|
|
45
|
+
* Must exist on disk and be a directory.
|
|
46
|
+
* @returns The created `RegisteredWorkspace`, or throws on validation failure.
|
|
47
|
+
* @throws Error if the alias is invalid, the path doesn't exist, or the alias is already taken.
|
|
48
|
+
*/
|
|
49
|
+
register(alias, absolutePath) {
|
|
50
|
+
const normalizedAlias = alias.toLowerCase().trim();
|
|
51
|
+
// Validate alias format
|
|
52
|
+
if (!ALIAS_PATTERN.test(normalizedAlias)) {
|
|
53
|
+
throw new Error(`Invalid workspace alias "${normalizedAlias}". Must be 1-30 characters, ` +
|
|
54
|
+
`lowercase alphanumeric, hyphens, or underscores, starting with a letter or digit.`);
|
|
55
|
+
}
|
|
56
|
+
// Check for duplicate aliases
|
|
57
|
+
if (this.workspaces.has(normalizedAlias)) {
|
|
58
|
+
throw new Error(`Workspace alias "${normalizedAlias}" is already registered ` +
|
|
59
|
+
`(→ ${this.workspaces.get(normalizedAlias).absolutePath}). ` +
|
|
60
|
+
`Remove it first with /workspaces → Remove.`);
|
|
61
|
+
}
|
|
62
|
+
// Normalize and validate the path
|
|
63
|
+
const normalizedPath = path.resolve(absolutePath);
|
|
64
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
65
|
+
throw new Error(`Path does not exist: "${normalizedPath}"`);
|
|
66
|
+
}
|
|
67
|
+
const stats = fs.statSync(normalizedPath);
|
|
68
|
+
if (!stats.isDirectory()) {
|
|
69
|
+
throw new Error(`Path is not a directory: "${normalizedPath}"`);
|
|
70
|
+
}
|
|
71
|
+
// Prevent registering sensitive system directories
|
|
72
|
+
const blockedRoots = ['/', '/etc', '/usr', '/bin', '/sbin', '/var', '/tmp', '/System', '/Library'];
|
|
73
|
+
if (blockedRoots.includes(normalizedPath) || normalizedPath === path.parse(normalizedPath).root) {
|
|
74
|
+
throw new Error(`Cannot register system root directory "${normalizedPath}". ` +
|
|
75
|
+
`Only project directories are allowed.`);
|
|
76
|
+
}
|
|
77
|
+
// Prevent duplicate paths under different aliases
|
|
78
|
+
for (const [existingAlias, existing] of this.workspaces) {
|
|
79
|
+
if (existing.absolutePath === normalizedPath) {
|
|
80
|
+
throw new Error(`This path is already registered under alias "${existingAlias}". ` +
|
|
81
|
+
`Remove it first if you want to re-register with a different alias.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const workspace = {
|
|
85
|
+
alias: normalizedAlias,
|
|
86
|
+
absolutePath: normalizedPath,
|
|
87
|
+
registeredAt: Date.now(),
|
|
88
|
+
};
|
|
89
|
+
this.workspaces.set(normalizedAlias, workspace);
|
|
90
|
+
this.saveToDisk();
|
|
91
|
+
debugLog(`Registered workspace: @${normalizedAlias} → ${normalizedPath}`);
|
|
92
|
+
return workspace;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Removes a registered workspace by alias.
|
|
96
|
+
*
|
|
97
|
+
* @param alias - The alias to unregister.
|
|
98
|
+
* @returns `true` if the workspace was found and removed, `false` otherwise.
|
|
99
|
+
*/
|
|
100
|
+
unregister(alias) {
|
|
101
|
+
const normalizedAlias = alias.toLowerCase().trim();
|
|
102
|
+
const existed = this.workspaces.delete(normalizedAlias);
|
|
103
|
+
if (existed) {
|
|
104
|
+
this.saveToDisk();
|
|
105
|
+
debugLog(`Unregistered workspace: @${normalizedAlias}`);
|
|
106
|
+
}
|
|
107
|
+
return existed;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Returns all registered workspaces as an array, sorted by alias.
|
|
111
|
+
*/
|
|
112
|
+
list() {
|
|
113
|
+
return Array.from(this.workspaces.values()).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Returns the number of registered workspaces.
|
|
117
|
+
*/
|
|
118
|
+
get size() {
|
|
119
|
+
return this.workspaces.size;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Checks whether any external workspaces are registered.
|
|
123
|
+
*/
|
|
124
|
+
hasWorkspaces() {
|
|
125
|
+
return this.workspaces.size > 0;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Looks up a workspace by alias.
|
|
129
|
+
*
|
|
130
|
+
* @param alias - The alias to find (without the `@` prefix).
|
|
131
|
+
* @returns The registered workspace, or `undefined` if not found.
|
|
132
|
+
*/
|
|
133
|
+
get(alias) {
|
|
134
|
+
return this.workspaces.get(alias.toLowerCase().trim());
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Returns all workspace roots (primary + registered secondaries).
|
|
138
|
+
*
|
|
139
|
+
* @param primaryRoot - The primary workspace root (from `process.cwd()`).
|
|
140
|
+
* @returns An array of `{ alias: string | null, root: string }` entries.
|
|
141
|
+
* The primary workspace has `alias: null`.
|
|
142
|
+
*/
|
|
143
|
+
getAllRoots(primaryRoot) {
|
|
144
|
+
const roots = [{ alias: null, root: primaryRoot }];
|
|
145
|
+
for (const ws of this.workspaces.values()) {
|
|
146
|
+
// Skip if a registered workspace happens to be the same as the primary
|
|
147
|
+
if (ws.absolutePath !== path.resolve(primaryRoot)) {
|
|
148
|
+
roots.push({ alias: ws.alias, root: ws.absolutePath });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return roots;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolves an `@alias/relative/path` string into its constituent parts.
|
|
155
|
+
*
|
|
156
|
+
* @param filePath - A file path that may or may not start with `@alias/`.
|
|
157
|
+
* @returns A `ResolvedWorkspacePath` if the path has a valid `@alias/` prefix
|
|
158
|
+
* and the alias is registered, or `null` if the path is a standard
|
|
159
|
+
* workspace-relative path (no `@` prefix or unrecognized alias).
|
|
160
|
+
* @throws Error if the path has an `@` prefix but the alias is not registered.
|
|
161
|
+
*/
|
|
162
|
+
resolve(filePath) {
|
|
163
|
+
if (!filePath.startsWith('@')) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
// Extract alias and relative path from "@alias/relative/path"
|
|
167
|
+
const withoutAt = filePath.substring(1);
|
|
168
|
+
const slashIndex = withoutAt.indexOf('/');
|
|
169
|
+
const alias = slashIndex === -1 ? withoutAt : withoutAt.substring(0, slashIndex);
|
|
170
|
+
const relativePath = slashIndex === -1 ? '.' : withoutAt.substring(slashIndex + 1);
|
|
171
|
+
const workspace = this.workspaces.get(alias.toLowerCase());
|
|
172
|
+
if (!workspace) {
|
|
173
|
+
throw new Error(`Unknown workspace alias "@${alias}". ` +
|
|
174
|
+
`Registered workspaces: ${this.list().map((w) => `@${w.alias}`).join(', ') || '(none)'}. ` +
|
|
175
|
+
`Use /workspaces to add one.`);
|
|
176
|
+
}
|
|
177
|
+
const absolutePath = path.resolve(workspace.absolutePath, relativePath);
|
|
178
|
+
return {
|
|
179
|
+
alias: workspace.alias,
|
|
180
|
+
workspaceRoot: workspace.absolutePath,
|
|
181
|
+
relativePath,
|
|
182
|
+
absolutePath,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Checks if a given file path uses the `@alias/` prefix syntax.
|
|
187
|
+
*
|
|
188
|
+
* @param filePath - The path to check.
|
|
189
|
+
* @returns `true` if the path starts with `@`.
|
|
190
|
+
*/
|
|
191
|
+
isAliasedPath(filePath) {
|
|
192
|
+
return filePath.startsWith('@');
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Builds a formatted summary of all registered workspaces for system prompt injection.
|
|
196
|
+
*
|
|
197
|
+
* @returns A human-readable string listing all aliases and their paths,
|
|
198
|
+
* or an empty string if no workspaces are registered.
|
|
199
|
+
*/
|
|
200
|
+
buildPromptSummary() {
|
|
201
|
+
if (this.workspaces.size === 0)
|
|
202
|
+
return '';
|
|
203
|
+
const lines = this.list().map((ws) => `- @${ws.alias} → ${ws.absolutePath}`);
|
|
204
|
+
return lines.join('\n');
|
|
205
|
+
}
|
|
206
|
+
// ─── Persistence ───────────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Loads the workspace registry from the global config file.
|
|
209
|
+
* Silently handles missing or corrupted files.
|
|
210
|
+
*/
|
|
211
|
+
loadFromDisk() {
|
|
212
|
+
try {
|
|
213
|
+
if (!fs.existsSync(REGISTRY_FILE)) {
|
|
214
|
+
debugLog('Workspace registry file not found, starting fresh.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const raw = fs.readFileSync(REGISTRY_FILE, 'utf-8');
|
|
218
|
+
const data = JSON.parse(raw);
|
|
219
|
+
if (!Array.isArray(data)) {
|
|
220
|
+
debugLog('Workspace registry file has invalid format, starting fresh.');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Validate each entry and skip invalid ones
|
|
224
|
+
for (const entry of data) {
|
|
225
|
+
if (typeof entry.alias === 'string' &&
|
|
226
|
+
typeof entry.absolutePath === 'string' &&
|
|
227
|
+
ALIAS_PATTERN.test(entry.alias)) {
|
|
228
|
+
// Only load if the directory still exists
|
|
229
|
+
if (fs.existsSync(entry.absolutePath)) {
|
|
230
|
+
this.workspaces.set(entry.alias, entry);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
debugLog(`Skipping stale workspace @${entry.alias} — path no longer exists: ${entry.absolutePath}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
debugLog(`Loaded ${this.workspaces.size} workspace(s) from global registry.`);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
debugLog(`Failed to load workspace registry: ${err instanceof Error ? err.message : String(err)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Persists the current workspace registry to the global config file.
|
|
245
|
+
* Creates the config directory if it doesn't exist.
|
|
246
|
+
*/
|
|
247
|
+
saveToDisk() {
|
|
248
|
+
try {
|
|
249
|
+
// Ensure the global config directory exists
|
|
250
|
+
if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
|
|
251
|
+
fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
const data = Array.from(this.workspaces.values());
|
|
254
|
+
const tempPath = `${REGISTRY_FILE}.${Date.now()}.tmp`;
|
|
255
|
+
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
256
|
+
// Atomic rename to prevent corruption
|
|
257
|
+
fs.renameSync(tempPath, REGISTRY_FILE);
|
|
258
|
+
debugLog(`Saved ${data.length} workspace(s) to global registry.`);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
debugLog(`Failed to save workspace registry: ${err instanceof Error ? err.message : String(err)}`);
|
|
262
|
+
// Non-fatal: the in-memory state is still correct
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Singleton instance of the WorkspaceRegistry service.
|
|
268
|
+
* Initialize with `workspaceRegistry.init()` during CLI startup.
|
|
269
|
+
*/
|
|
270
|
+
export const workspaceRegistry = new WorkspaceRegistry();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a callback within the context of a specific agent/thread ID.
|
|
3
|
+
* All downstream async operations will be able to access this ID.
|
|
4
|
+
*
|
|
5
|
+
* @param agentId The ID of the agent/thread to track.
|
|
6
|
+
* @param callback The function to execute.
|
|
7
|
+
* @returns The result of the callback.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runWithAgentId<T>(agentId: string, callback: () => T): T;
|
|
10
|
+
/**
|
|
11
|
+
* Retrieves the ID of the currently active agent/thread from the async context.
|
|
12
|
+
* Returns 'main' if no context is active (e.g., fallback for single-agent or orchestrator).
|
|
13
|
+
*
|
|
14
|
+
* @returns The current agent ID, or 'main' as fallback.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCurrentAgentId(): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
/**
|
|
3
|
+
* AsyncLocalStorage instance to track the current active agent/thread ID.
|
|
4
|
+
*/
|
|
5
|
+
const agentContext = new AsyncLocalStorage();
|
|
6
|
+
/**
|
|
7
|
+
* Runs a callback within the context of a specific agent/thread ID.
|
|
8
|
+
* All downstream async operations will be able to access this ID.
|
|
9
|
+
*
|
|
10
|
+
* @param agentId The ID of the agent/thread to track.
|
|
11
|
+
* @param callback The function to execute.
|
|
12
|
+
* @returns The result of the callback.
|
|
13
|
+
*/
|
|
14
|
+
export function runWithAgentId(agentId, callback) {
|
|
15
|
+
return agentContext.run(agentId, callback);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves the ID of the currently active agent/thread from the async context.
|
|
19
|
+
* Returns 'main' if no context is active (e.g., fallback for single-agent or orchestrator).
|
|
20
|
+
*
|
|
21
|
+
* @returns The current agent ID, or 'main' as fallback.
|
|
22
|
+
*/
|
|
23
|
+
export function getCurrentAgentId() {
|
|
24
|
+
return agentContext.getStore() || 'main';
|
|
25
|
+
}
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare const GEMINI_MODELS: {
|
|
|
17
17
|
readonly PRO_3_1: "gemini-3.1-pro-preview";
|
|
18
18
|
readonly FLASH_3_5: "gemini-3.5-flash";
|
|
19
19
|
readonly FLASH_LITE_3_1: "gemini-3.1-flash-lite";
|
|
20
|
+
readonly EMBEDDING: "text-embedding-004";
|
|
21
|
+
readonly AUTO: "auto";
|
|
20
22
|
};
|
|
21
23
|
/**
|
|
22
24
|
* Supported Anthropic Claude models.
|
|
@@ -26,6 +28,6 @@ export declare const CLAUDE_MODELS: {
|
|
|
26
28
|
readonly SONNET: "claude-sonnet-4-6";
|
|
27
29
|
};
|
|
28
30
|
/** Default Gemini model for the coding agent. */
|
|
29
|
-
export declare const DEFAULT_MODEL: "
|
|
31
|
+
export declare const DEFAULT_MODEL: "auto";
|
|
30
32
|
/** Maximum tokens the model can output per response. */
|
|
31
33
|
export declare const MAX_OUTPUT_TOKENS = 60000;
|
package/dist/utils/config.js
CHANGED
|
@@ -17,6 +17,8 @@ export const GEMINI_MODELS = {
|
|
|
17
17
|
PRO_3_1: 'gemini-3.1-pro-preview',
|
|
18
18
|
FLASH_3_5: 'gemini-3.5-flash',
|
|
19
19
|
FLASH_LITE_3_1: 'gemini-3.1-flash-lite',
|
|
20
|
+
EMBEDDING: 'text-embedding-004',
|
|
21
|
+
AUTO: 'auto',
|
|
20
22
|
};
|
|
21
23
|
/**
|
|
22
24
|
* Supported Anthropic Claude models.
|
|
@@ -26,6 +28,6 @@ export const CLAUDE_MODELS = {
|
|
|
26
28
|
SONNET: 'claude-sonnet-4-6',
|
|
27
29
|
};
|
|
28
30
|
/** Default Gemini model for the coding agent. */
|
|
29
|
-
export const DEFAULT_MODEL = GEMINI_MODELS.
|
|
31
|
+
export const DEFAULT_MODEL = GEMINI_MODELS.AUTO;
|
|
30
32
|
/** Maximum tokens the model can output per response. */
|
|
31
33
|
export const MAX_OUTPUT_TOKENS = 60_000;
|
|
@@ -41,10 +41,17 @@ ${context.webSearchSummary}
|
|
|
41
41
|
}
|
|
42
42
|
if (context.relevantFiles.size > 0) {
|
|
43
43
|
injection += `\n## Relevant File Contents\n`;
|
|
44
|
-
for (const [filePath,
|
|
45
|
-
|
|
44
|
+
for (const [filePath, contentObj] of context.relevantFiles.entries()) {
|
|
45
|
+
const contentText = contentObj.text;
|
|
46
|
+
let aliasAttr = '';
|
|
47
|
+
if (filePath.startsWith('@')) {
|
|
48
|
+
const slashIndex = filePath.indexOf('/');
|
|
49
|
+
const alias = slashIndex === -1 ? filePath.substring(1) : filePath.substring(1, slashIndex);
|
|
50
|
+
aliasAttr = ` workspace="${alias}"`;
|
|
51
|
+
}
|
|
52
|
+
injection += `<workspace_file path="${filePath}"${aliasAttr}>
|
|
46
53
|
<content_data><![CDATA[
|
|
47
|
-
${sanitizeForCDATA(
|
|
54
|
+
${sanitizeForCDATA(contentText)}
|
|
48
55
|
]]\\u200B></content_data>
|
|
49
56
|
</workspace_file>\n`;
|
|
50
57
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface FindDependenciesResult {
|
|
2
|
+
filePath: string;
|
|
3
|
+
forwardDeps: string[];
|
|
4
|
+
reverseDeps: string[];
|
|
5
|
+
forwardTree: string[];
|
|
6
|
+
reverseTree: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function extractImports(content: string, ext: string): string[];
|
|
9
|
+
export declare function resolveImportPath(workspaceRoot: string, sourceFile: string, specifier: string): Promise<string | null>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PROFILE_BY_EXT } from './profiles.js';
|
|
2
|
+
import { loadPathAliases } from './resolver.js';
|
|
3
|
+
import { probeFilePath } from './walker.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export function extractImports(content, ext) {
|
|
6
|
+
const profiles = PROFILE_BY_EXT.get(ext);
|
|
7
|
+
if (!profiles || profiles.length === 0)
|
|
8
|
+
return [];
|
|
9
|
+
const specifiers = new Set();
|
|
10
|
+
for (const profile of profiles) {
|
|
11
|
+
for (const pattern of profile.patterns) {
|
|
12
|
+
pattern.lastIndex = 0;
|
|
13
|
+
for (const match of content.matchAll(pattern)) {
|
|
14
|
+
const raw = match.groups?.specifier;
|
|
15
|
+
if (raw && raw.trim().length > 0) {
|
|
16
|
+
const normalized = profile.normalizeSpecifier ? profile.normalizeSpecifier(raw.trim(), '') : raw.trim();
|
|
17
|
+
specifiers.add(normalized);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return Array.from(specifiers);
|
|
23
|
+
}
|
|
24
|
+
export async function resolveImportPath(workspaceRoot, sourceFile, specifier) {
|
|
25
|
+
if (specifier.startsWith('node:') ||
|
|
26
|
+
specifier.startsWith('bun:') ||
|
|
27
|
+
specifier.startsWith('deno:') ||
|
|
28
|
+
specifier.startsWith('package:') ||
|
|
29
|
+
specifier.startsWith('dart:') ||
|
|
30
|
+
specifier.startsWith('http://') ||
|
|
31
|
+
specifier.startsWith('https://')) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const isRelative = specifier.startsWith('.') || specifier.startsWith('/');
|
|
35
|
+
if (!isRelative) {
|
|
36
|
+
const aliases = await loadPathAliases(workspaceRoot);
|
|
37
|
+
for (const alias of aliases) {
|
|
38
|
+
if (specifier.startsWith(alias.prefix)) {
|
|
39
|
+
const remainder = specifier.slice(alias.prefix.length);
|
|
40
|
+
for (const target of alias.targets) {
|
|
41
|
+
const candidate = path.join(workspaceRoot, target, remainder);
|
|
42
|
+
const resolved = await probeFilePath(candidate);
|
|
43
|
+
if (resolved)
|
|
44
|
+
return path.relative(workspaceRoot, resolved).replace(/\\/g, '/');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const fromRoot = path.join(workspaceRoot, specifier);
|
|
49
|
+
const rootResolved = await probeFilePath(fromRoot);
|
|
50
|
+
if (rootResolved)
|
|
51
|
+
return path.relative(workspaceRoot, rootResolved).replace(/\\/g, '/');
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const sourceDir = path.dirname(path.join(workspaceRoot, sourceFile));
|
|
55
|
+
const absoluteCandidate = path.resolve(sourceDir, specifier);
|
|
56
|
+
if (!absoluteCandidate.startsWith(workspaceRoot))
|
|
57
|
+
return null;
|
|
58
|
+
const resolved = await probeFilePath(absoluteCandidate);
|
|
59
|
+
if (resolved)
|
|
60
|
+
return path.relative(workspaceRoot, resolved).replace(/\\/g, '/');
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DependencyNode } from './types.js';
|
|
2
|
+
export interface DependencyGraph {
|
|
3
|
+
getImports(filePath: string): string[];
|
|
4
|
+
getImportedBy(filePath: string): string[];
|
|
5
|
+
getReverseDependencyTree(filePath: string, maxDepth?: number): string[];
|
|
6
|
+
getForwardDependencyTree(filePath: string, maxDepth?: number): string[];
|
|
7
|
+
readonly nodes: ReadonlyMap<string, DependencyNode>;
|
|
8
|
+
}
|
|
9
|
+
export declare function bfsTraverse(nodes: Map<string, DependencyNode>, startFile: string, direction: 'imports' | 'importedBy', maxDepth: number): string[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function bfsTraverse(nodes, startFile, direction, maxDepth) {
|
|
2
|
+
const visited = new Set();
|
|
3
|
+
visited.add(startFile);
|
|
4
|
+
const queue = [{ file: startFile, depth: 0 }];
|
|
5
|
+
const result = [];
|
|
6
|
+
while (queue.length > 0) {
|
|
7
|
+
const { file, depth } = queue.shift();
|
|
8
|
+
if (depth >= maxDepth)
|
|
9
|
+
continue;
|
|
10
|
+
const node = nodes.get(file);
|
|
11
|
+
if (!node)
|
|
12
|
+
continue;
|
|
13
|
+
const neighbors = node[direction];
|
|
14
|
+
for (const neighbor of neighbors) {
|
|
15
|
+
if (!visited.has(neighbor)) {
|
|
16
|
+
visited.add(neighbor);
|
|
17
|
+
result.push(neighbor);
|
|
18
|
+
queue.push({ file: neighbor, depth: depth + 1 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface LanguageProfile {
|
|
2
|
+
extensions: string[];
|
|
3
|
+
patterns: RegExp[];
|
|
4
|
+
normalizeSpecifier?: (specifier: string, sourceFile: string) => string;
|
|
5
|
+
}
|
|
6
|
+
export declare const LANGUAGE_PROFILES: LanguageProfile[];
|
|
7
|
+
export declare const PROFILE_BY_EXT: Map<string, LanguageProfile[]>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const LANGUAGE_PROFILES = [
|
|
2
|
+
{
|
|
3
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts'],
|
|
4
|
+
patterns: [
|
|
5
|
+
/import\s+(?:[\s\S]*?\s+from\s+)?['"](?<specifier>[^'"]+)['"]/gm,
|
|
6
|
+
/import\s*\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gm,
|
|
7
|
+
/require\s*\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gm,
|
|
8
|
+
/export\s+(?:[\s\S]*?\s+from\s+)['"](?<specifier>[^'"]+)['"]/gm,
|
|
9
|
+
],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
extensions: ['.py'],
|
|
13
|
+
patterns: [
|
|
14
|
+
/^from\s+(?<specifier>[^\s]+)\s+import\s/gm,
|
|
15
|
+
/^import\s+(?!.*\sfrom\s)(?<specifier>[^\s,]+)/gm,
|
|
16
|
+
],
|
|
17
|
+
normalizeSpecifier: (spec) => {
|
|
18
|
+
if (spec.startsWith('.')) {
|
|
19
|
+
const dots = spec.match(/^\.+/)[0];
|
|
20
|
+
const rest = spec.slice(dots.length);
|
|
21
|
+
const prefix = dots.length === 1 ? './' : '../'.repeat(dots.length - 1);
|
|
22
|
+
return rest ? `${prefix}${rest.replace(/\./g, '/')}.py` : prefix.slice(0, -1);
|
|
23
|
+
}
|
|
24
|
+
return spec.replace(/\./g, '/') + '.py';
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
extensions: ['.rs'],
|
|
29
|
+
patterns: [
|
|
30
|
+
/use\s+(?<specifier>(?:crate|self|super)(?:::[a-zA-Z_][a-zA-Z0-9_]*)+)/gm,
|
|
31
|
+
/mod\s+(?<specifier>[a-zA-Z_][a-zA-Z0-9_]*)\s*;/gm,
|
|
32
|
+
],
|
|
33
|
+
normalizeSpecifier: (spec) => {
|
|
34
|
+
if (!spec.includes('::')) {
|
|
35
|
+
return spec + '.rs';
|
|
36
|
+
}
|
|
37
|
+
const parts = spec
|
|
38
|
+
.replace(/^crate::/, '')
|
|
39
|
+
.replace(/^(self|super)::/, '')
|
|
40
|
+
.split('::');
|
|
41
|
+
return parts.join('/') + '.rs';
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
extensions: ['.go'],
|
|
46
|
+
patterns: [
|
|
47
|
+
/import\s+(?:[a-zA-Z_][a-zA-Z0-9_]*\s+)?["'](?<specifier>[^"']+)["']/gm,
|
|
48
|
+
/^\s*(?:[a-zA-Z_][a-zA-Z0-9_]*\s+)?["'](?<specifier>[^"']+)["']\s*$/gm,
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
extensions: ['.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hh', '.hxx'],
|
|
53
|
+
patterns: [
|
|
54
|
+
/^\s*#\s*include\s+"(?<specifier>[^"]+)"/gm,
|
|
55
|
+
/^\s*#\s*include\s+<(?<specifier>[^>]+)>/gm,
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
extensions: ['.java', '.kt', '.kts'],
|
|
60
|
+
patterns: [
|
|
61
|
+
/^import\s+(?:static\s+)?(?<specifier>[a-zA-Z_][a-zA-Z0-9_.]*\.[a-zA-Z_][a-zA-Z0-9_]*)/gm,
|
|
62
|
+
],
|
|
63
|
+
normalizeSpecifier: (spec) => {
|
|
64
|
+
return spec.replace(/\./g, '/') + '.java';
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
extensions: ['.css', '.scss', '.sass', '.less', '.styl'],
|
|
69
|
+
patterns: [
|
|
70
|
+
/@import\s+(?:url\s*\(\s*)?['"](?<specifier>[^'"]+)['"]/gm,
|
|
71
|
+
/@use\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
72
|
+
/@forward\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
extensions: ['.rb'],
|
|
77
|
+
patterns: [
|
|
78
|
+
/require\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
79
|
+
/require_relative\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
80
|
+
/load\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
extensions: ['.php'],
|
|
85
|
+
patterns: [
|
|
86
|
+
/use\s+(?<specifier>[A-Z][a-zA-Z0-9_\\]+)/gm,
|
|
87
|
+
/(?:include|require)(?:_once)?\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
88
|
+
/(?:include|require)(?:_once)?\s*\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gm,
|
|
89
|
+
],
|
|
90
|
+
normalizeSpecifier: (spec) => {
|
|
91
|
+
if (spec.includes('\\')) {
|
|
92
|
+
return spec.replace(/\\/g, '/') + '.php';
|
|
93
|
+
}
|
|
94
|
+
return spec;
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
extensions: ['.swift'],
|
|
99
|
+
patterns: [
|
|
100
|
+
/^import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|typealias\s+|func\s+|var\s+|let\s+)?(?<specifier>[a-zA-Z_][a-zA-Z0-9_.]*)/gm,
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
extensions: ['.dart'],
|
|
105
|
+
patterns: [
|
|
106
|
+
/import\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
107
|
+
/export\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
108
|
+
/part\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
109
|
+
/part\s+of\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
export const PROFILE_BY_EXT = new Map();
|
|
114
|
+
for (const profile of LANGUAGE_PROFILES) {
|
|
115
|
+
for (const ext of profile.extensions) {
|
|
116
|
+
const existing = PROFILE_BY_EXT.get(ext) || [];
|
|
117
|
+
existing.push(profile);
|
|
118
|
+
PROFILE_BY_EXT.set(ext, existing);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface AliasMapping {
|
|
2
|
+
prefix: string;
|
|
3
|
+
targets: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function loadPathAliases(workspaceRoot: string): Promise<AliasMapping[]>;
|
|
6
|
+
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
7
|
+
export declare function normalizeSlashes(p: string): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
let cachedAliases = null;
|
|
4
|
+
let cachedAliasRoot = null;
|
|
5
|
+
export async function loadPathAliases(workspaceRoot) {
|
|
6
|
+
if (cachedAliasRoot === workspaceRoot && cachedAliases !== null) {
|
|
7
|
+
return cachedAliases;
|
|
8
|
+
}
|
|
9
|
+
const aliases = [];
|
|
10
|
+
for (const configName of ['tsconfig.json', 'jsconfig.json']) {
|
|
11
|
+
try {
|
|
12
|
+
const configPath = path.join(workspaceRoot, configName);
|
|
13
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
14
|
+
const cleaned = raw.replace(/\/\/.*$/gm, '').replace(/,\s*([\]}])/g, '');
|
|
15
|
+
const config = JSON.parse(cleaned);
|
|
16
|
+
const paths = config?.compilerOptions?.paths;
|
|
17
|
+
const baseUrl = config?.compilerOptions?.baseUrl || '.';
|
|
18
|
+
if (paths && typeof paths === 'object') {
|
|
19
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
20
|
+
if (!Array.isArray(targets))
|
|
21
|
+
continue;
|
|
22
|
+
const prefix = pattern.replace(/\*$/, '');
|
|
23
|
+
const resolvedTargets = targets.map((t) => {
|
|
24
|
+
const targetBase = t.replace(/\*$/, '');
|
|
25
|
+
return path.join(baseUrl, targetBase);
|
|
26
|
+
});
|
|
27
|
+
aliases.push({ prefix, targets: resolvedTargets });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Config doesn't exist or is invalid — try the next one
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
cachedAliases = aliases;
|
|
37
|
+
cachedAliasRoot = workspaceRoot;
|
|
38
|
+
return aliases;
|
|
39
|
+
}
|
|
40
|
+
export async function fileExists(filePath) {
|
|
41
|
+
try {
|
|
42
|
+
await fs.access(filePath);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function normalizeSlashes(p) {
|
|
50
|
+
return p.replace(/\\/g, '/');
|
|
51
|
+
}
|