vigthoria-cli 1.10.36 → 1.10.37
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/dist/commands/chat.d.ts +6 -0
- package/dist/commands/chat.js +170 -5
- package/dist/utils/brain-hub-client.d.ts +32 -0
- package/dist/utils/brain-hub-client.js +48 -0
- package/dist/utils/codebase-indexer.d.ts +59 -0
- package/dist/utils/codebase-indexer.js +314 -0
- package/dist/utils/tools.d.ts +2 -0
- package/dist/utils/tools.js +14 -0
- package/dist/utils/workspace-brain-service.d.ts +43 -0
- package/dist/utils/workspace-brain-service.js +121 -0
- package/package.json +1 -1
- package/scripts/release/validate-no-go-gates.sh +1 -1
package/dist/commands/chat.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class ChatCommand {
|
|
|
26
26
|
private tools;
|
|
27
27
|
private sessionManager;
|
|
28
28
|
private projectMemory;
|
|
29
|
+
private workspaceBrain;
|
|
29
30
|
private currentSession;
|
|
30
31
|
private agentMode;
|
|
31
32
|
private currentProjectPath;
|
|
@@ -103,6 +104,10 @@ export declare class ChatCommand {
|
|
|
103
104
|
private isProjectBrainRuntimeDisabled;
|
|
104
105
|
private buildProjectBrainRuntimeContext;
|
|
105
106
|
private rememberBrainEvent;
|
|
107
|
+
private initializeWorkspaceBrain;
|
|
108
|
+
private bootstrapWorkspaceBrain;
|
|
109
|
+
private reindexWorkspaceBrain;
|
|
110
|
+
private showBrainIndexStatus;
|
|
106
111
|
private getPromptRuntimeContext;
|
|
107
112
|
private v3IterationCount;
|
|
108
113
|
private v3ToolCallCount;
|
|
@@ -167,6 +172,7 @@ export declare class ChatCommand {
|
|
|
167
172
|
private buildLocalAgentChatOptions;
|
|
168
173
|
private printChatModelPreflight;
|
|
169
174
|
private runLocalAgentLoop;
|
|
175
|
+
private primeAgentWorkspaceDiscovery;
|
|
170
176
|
private primeBypassedTargetFileContext;
|
|
171
177
|
private tryDirectSingleFileFlow;
|
|
172
178
|
private isConfirmationFollowUp;
|
package/dist/commands/chat.js
CHANGED
|
@@ -11,6 +11,7 @@ import { BridgeClient, getBridgeClient } from '../utils/bridge-client.js';
|
|
|
11
11
|
import { WorkspaceWatcher, WorkspaceWSClient } from '../utils/workspace-stream.js';
|
|
12
12
|
import { TaskDisplay } from '../utils/task-display.js';
|
|
13
13
|
import { ProjectMemoryService } from '../utils/project-memory.js';
|
|
14
|
+
import { WorkspaceBrainService } from '../utils/workspace-brain-service.js';
|
|
14
15
|
import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
|
|
15
16
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
16
17
|
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
@@ -44,6 +45,7 @@ export class ChatCommand {
|
|
|
44
45
|
tools = null;
|
|
45
46
|
sessionManager;
|
|
46
47
|
projectMemory = null;
|
|
48
|
+
workspaceBrain = null;
|
|
47
49
|
currentSession = null;
|
|
48
50
|
agentMode = false;
|
|
49
51
|
currentProjectPath = process.cwd();
|
|
@@ -333,6 +335,17 @@ export class ChatCommand {
|
|
|
333
335
|
messages.splice(insertionIndex, 0, memoryMessage);
|
|
334
336
|
}
|
|
335
337
|
}
|
|
338
|
+
const codebaseContext = this.workspaceBrain?.buildCodebaseContext(this.getLastUserPrompt()) || '';
|
|
339
|
+
if (codebaseContext && !messages.some((message) => message.role === 'system' && message.content.includes('Vigthoria codebase index context.'))) {
|
|
340
|
+
const insertionIndex = messages.findIndex((message) => message.role !== 'system');
|
|
341
|
+
const codebaseMessage = { role: 'system', content: codebaseContext };
|
|
342
|
+
if (insertionIndex === -1) {
|
|
343
|
+
messages.push(codebaseMessage);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
messages.splice(insertionIndex, 0, codebaseMessage);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
336
349
|
if (options?.compact) {
|
|
337
350
|
const compactLimit = 6000;
|
|
338
351
|
const systemMessages = messages.filter((message) => message.role === 'system').map((message) => ({
|
|
@@ -879,6 +892,82 @@ export class ChatCommand {
|
|
|
879
892
|
// Project Brain memory must not break chat, GoA, or operator execution.
|
|
880
893
|
}
|
|
881
894
|
}
|
|
895
|
+
initializeWorkspaceBrain() {
|
|
896
|
+
if (this.isProjectBrainRuntimeDisabled()) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
this.workspaceBrain = new WorkspaceBrainService({
|
|
900
|
+
workspacePath: this.currentProjectPath,
|
|
901
|
+
apiBase: String(this.config.get('apiUrl') || 'https://coder.vigthoria.io'),
|
|
902
|
+
getAuthToken: () => this.config.get('authToken'),
|
|
903
|
+
});
|
|
904
|
+
this.tools?.setIndexedCodebaseSearch((query, maxResults) => (this.workspaceBrain?.searchCodebase(query, maxResults) || ''));
|
|
905
|
+
}
|
|
906
|
+
async bootstrapWorkspaceBrain(interactive) {
|
|
907
|
+
if (!this.workspaceBrain) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const promptIfMissing = interactive
|
|
911
|
+
&& !this.jsonOutput
|
|
912
|
+
&& process.stdin.isTTY
|
|
913
|
+
&& !/^(0|false|no)$/i.test(String(process.env.VIGTHORIA_PROMPT_INDEX || '1'));
|
|
914
|
+
const result = await this.workspaceBrain.ensureIndexed({
|
|
915
|
+
promptIfMissing,
|
|
916
|
+
askToIndex: promptIfMissing
|
|
917
|
+
? async (fileCount, workspaceName) => new Promise((resolve) => {
|
|
918
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
919
|
+
rl.question(chalk.cyan(`Index "${workspaceName}" for Vigthoria Brain? (~${fileCount} source files) [Y/n] `), (answer) => {
|
|
920
|
+
rl.close();
|
|
921
|
+
const normalized = answer.trim().toLowerCase();
|
|
922
|
+
resolve(normalized === 'n' || normalized === 'no' ? 'later' : 'index_now');
|
|
923
|
+
});
|
|
924
|
+
})
|
|
925
|
+
: undefined,
|
|
926
|
+
});
|
|
927
|
+
if (result.indexed && result.fileCount > 0 && !this.jsonOutput) {
|
|
928
|
+
console.log(chalk.gray(`Brain index ready: ${result.fileCount} files, ${result.chunkCount} chunks`));
|
|
929
|
+
}
|
|
930
|
+
else if (result.prompted === 'later' && !this.jsonOutput) {
|
|
931
|
+
console.log(chalk.gray('Workspace indexing skipped. Use /index anytime.'));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async reindexWorkspaceBrain() {
|
|
935
|
+
if (!this.workspaceBrain) {
|
|
936
|
+
this.initializeWorkspaceBrain();
|
|
937
|
+
}
|
|
938
|
+
if (!this.workspaceBrain) {
|
|
939
|
+
console.log(chalk.yellow('Workspace Brain is disabled.'));
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const spinner = createSpinner({ text: 'Indexing workspace for Vigthoria Brain...', spinner: 'clock' }).start();
|
|
943
|
+
try {
|
|
944
|
+
const meta = await this.workspaceBrain.reindexWorkspace();
|
|
945
|
+
spinner.stop();
|
|
946
|
+
console.log(chalk.green(`Indexed ${meta.indexedFileCount} files (${meta.totalChunks} chunks). Brain Hub sync attempted.`));
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
spinner.stop();
|
|
950
|
+
this.logger.error(error instanceof Error ? error.message : String(error));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
showBrainIndexStatus() {
|
|
954
|
+
if (!this.workspaceBrain) {
|
|
955
|
+
console.log(chalk.yellow('Workspace Brain is disabled (VIGTHORIA_NO_BRAIN=1).'));
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const status = this.workspaceBrain.getStatus();
|
|
959
|
+
console.log();
|
|
960
|
+
console.log(chalk.white('Workspace Brain Index:'));
|
|
961
|
+
console.log(chalk.gray(`Workspace: ${status.workspacePath}`));
|
|
962
|
+
console.log(chalk.gray(`Files indexed: ${status.indexedFileCount}`));
|
|
963
|
+
console.log(chalk.gray(`Chunks: ${status.totalChunks}`));
|
|
964
|
+
if (status.meta?.indexedAt) {
|
|
965
|
+
console.log(chalk.gray(`Last indexed: ${status.meta.indexedAt}`));
|
|
966
|
+
}
|
|
967
|
+
if (status.meta?.indexHash) {
|
|
968
|
+
console.log(chalk.gray(`Index hash: ${status.meta.indexHash}`));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
882
971
|
async getPromptRuntimeContext(prompt) {
|
|
883
972
|
const runtimeContext = {
|
|
884
973
|
agentRuntime: this.getRuntimeEnvironmentContext(),
|
|
@@ -887,6 +976,22 @@ export class ChatCommand {
|
|
|
887
976
|
if (brainContext) {
|
|
888
977
|
runtimeContext.vigthoriaBrain = brainContext;
|
|
889
978
|
}
|
|
979
|
+
if (this.workspaceBrain) {
|
|
980
|
+
const accountBrain = await this.workspaceBrain.fetchAccountBrainContext();
|
|
981
|
+
if (accountBrain) {
|
|
982
|
+
runtimeContext.accountBrainContext = accountBrain;
|
|
983
|
+
}
|
|
984
|
+
const indexStatus = this.workspaceBrain.getStatus();
|
|
985
|
+
runtimeContext.codebaseIndex = {
|
|
986
|
+
indexedFileCount: indexStatus.indexedFileCount,
|
|
987
|
+
totalChunks: indexStatus.totalChunks,
|
|
988
|
+
indexHash: indexStatus.meta?.indexHash || null,
|
|
989
|
+
};
|
|
990
|
+
const indexedContext = this.workspaceBrain.buildCodebaseContext(prompt);
|
|
991
|
+
if (indexedContext) {
|
|
992
|
+
runtimeContext.codebaseContext = indexedContext;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
890
995
|
if (!this.isBrowserTaskPrompt(prompt)) {
|
|
891
996
|
return runtimeContext;
|
|
892
997
|
}
|
|
@@ -1508,6 +1613,8 @@ export class ChatCommand {
|
|
|
1508
1613
|
this.directToolContinuationCount = 0;
|
|
1509
1614
|
this.tools = new AgenticTools(this.logger, this.currentProjectPath, async (action) => this.requestPermission(action), this.autoApprove);
|
|
1510
1615
|
this.initializeSession(options.resume === true);
|
|
1616
|
+
this.initializeWorkspaceBrain();
|
|
1617
|
+
await this.bootstrapWorkspaceBrain(!options.prompt);
|
|
1511
1618
|
// ── Commando Bridge: connect if --bridge was specified ──────────
|
|
1512
1619
|
if (options.bridge) {
|
|
1513
1620
|
const bridgeClient = new BridgeClient({
|
|
@@ -2344,7 +2451,9 @@ export class ChatCommand {
|
|
|
2344
2451
|
this.tools.clearSessionApprovals();
|
|
2345
2452
|
getBridgeClient()?.emitPrompt({ prompt, mode: this.operatorMode ? 'operator' : 'agent', model: this.currentModel });
|
|
2346
2453
|
this.ensureAgentSystemPrompt();
|
|
2347
|
-
|
|
2454
|
+
const scopedPrompt = this.buildScopedUserPrompt(this.buildContextualAgentPrompt(prompt));
|
|
2455
|
+
this.messages.push({ role: 'user', content: scopedPrompt });
|
|
2456
|
+
await this.primeAgentWorkspaceDiscovery(prompt);
|
|
2348
2457
|
this.saveSession();
|
|
2349
2458
|
const preflightSpinner = this.jsonOutput
|
|
2350
2459
|
? null
|
|
@@ -2511,18 +2620,25 @@ export class ChatCommand {
|
|
|
2511
2620
|
// Detect resignation: model gives up saying files/things were "not found"
|
|
2512
2621
|
// without having tried list_dir to discover the correct path.
|
|
2513
2622
|
const isResignation = /(?:not found|cannot be (?:determined|compared|completed)|do not exist|does not exist|unable to locate|neither.*exist|could not (?:find|locate)|no (?:such|matching) file)/i.test(sanitized) && this.agentToolEvidence.discovery < 4;
|
|
2623
|
+
const actionablePrompt = this.lastActionableUserInput || prompt;
|
|
2624
|
+
const needsRepoWork = this.isDiagnosticPrompt(actionablePrompt) || this.isImplementationPrompt(actionablePrompt);
|
|
2514
2625
|
// Gate 1: First turn with no discovery at all
|
|
2515
|
-
const gate1 = turn === 0 && this.agentToolEvidence.discovery === 0 && (this.
|
|
2626
|
+
const gate1 = turn === 0 && this.agentToolEvidence.discovery === 0 && (needsRepoWork || this.directPromptMode || this.agentMode || isPolicyAck);
|
|
2516
2627
|
// Gate 2: Any turn where the response is just a follow-up question,
|
|
2517
2628
|
// tool-failure echoes, or premature resignation (the model gave up
|
|
2518
2629
|
// instead of retrying with list_dir to find the correct paths)
|
|
2519
|
-
const gate2 = this.directPromptMode && turn < 6 && (isPolicyAck || isFollowUp || isEmptyAfterSanitize || isResignation);
|
|
2630
|
+
const gate2 = (this.directPromptMode || this.agentMode) && turn < 6 && (isPolicyAck || isFollowUp || isEmptyAfterSanitize || isResignation);
|
|
2631
|
+
const gateNoToolsNoWork = needsRepoWork
|
|
2632
|
+
&& this.agentToolEvidence.discovery === 0
|
|
2633
|
+
&& this.agentToolEvidence.mutation === 0
|
|
2634
|
+
&& turn < maxTurns - 1
|
|
2635
|
+
&& (isEmptyAfterSanitize || isPolicyAck || isFollowUp);
|
|
2520
2636
|
// Gate 3: Model outputs code blocks as text instead of using write_file.
|
|
2521
2637
|
// If the response contains ``` code fences but no write_file was called,
|
|
2522
2638
|
// reject and instruct the model to use write_file.
|
|
2523
2639
|
const hasCodeBlocks = (sanitized.match(/```/g) || []).length >= 2;
|
|
2524
2640
|
const gate3 = hasCodeBlocks && this.agentToolEvidence.mutation === 0 && turn < 6;
|
|
2525
|
-
if (gate1 || gate2 || gate3) {
|
|
2641
|
+
if (gate1 || gate2 || gate3 || gateNoToolsNoWork) {
|
|
2526
2642
|
// Remove the useless response from history
|
|
2527
2643
|
if (isPolicyAck || isFollowUp || isEmptyAfterSanitize || isResignation || gate3) {
|
|
2528
2644
|
this.messages.pop();
|
|
@@ -2618,6 +2734,36 @@ export class ChatCommand {
|
|
|
2618
2734
|
this.saveSession();
|
|
2619
2735
|
return true;
|
|
2620
2736
|
}
|
|
2737
|
+
async primeAgentWorkspaceDiscovery(prompt) {
|
|
2738
|
+
if (!this.tools || this.agentToolEvidence.discovery > 0) {
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
const actionablePrompt = this.buildContextualAgentPrompt(prompt);
|
|
2742
|
+
if (!this.isDiagnosticPrompt(actionablePrompt) && !this.isImplementationPrompt(actionablePrompt)) {
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
const listCall = {
|
|
2746
|
+
tool: 'list_dir',
|
|
2747
|
+
args: { path: '.' },
|
|
2748
|
+
};
|
|
2749
|
+
if (!this.jsonOutput) {
|
|
2750
|
+
console.log(chalk.cyan('⚙ Executing: list_dir → workspace root (agent bootstrap)'));
|
|
2751
|
+
}
|
|
2752
|
+
const result = await this.tools.execute(listCall);
|
|
2753
|
+
const summary = this.formatToolResult(listCall, result);
|
|
2754
|
+
if (!this.jsonOutput) {
|
|
2755
|
+
console.log(result.success ? chalk.gray(summary) : chalk.red(summary));
|
|
2756
|
+
}
|
|
2757
|
+
this.messages.push({ role: 'system', content: summary });
|
|
2758
|
+
getBridgeClient()?.emitToolResult({
|
|
2759
|
+
tool: listCall.tool,
|
|
2760
|
+
success: result.success,
|
|
2761
|
+
preview: (result.output || result.error || '').slice(0, 300),
|
|
2762
|
+
});
|
|
2763
|
+
if (result.success) {
|
|
2764
|
+
this.agentToolEvidence.discovery += 1;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2621
2767
|
async primeBypassedTargetFileContext(prompt) {
|
|
2622
2768
|
if (!this.directPromptMode || !this.tools) {
|
|
2623
2769
|
return;
|
|
@@ -2763,7 +2909,7 @@ export class ChatCommand {
|
|
|
2763
2909
|
}
|
|
2764
2910
|
isConfirmationFollowUp(prompt) {
|
|
2765
2911
|
const normalized = prompt.trim().toLowerCase().replace(/[.!?]+$/g, '').replace(/\s+/g, ' ');
|
|
2766
|
-
return /^(ja|ja bitte|ja bitte mach das|mach das|bitte mach das|genau|ok|okay|yes|yes please|please do|do it|go ahead|continue|proceed|make it so)$/.test(normalized);
|
|
2912
|
+
return /^(ja|ja bitte|ja bitte mach das|mach das|bitte mach das|genau|ok|okay|yes|yes please|please do|do it|go ahead|go on|continue|proceed|make it so|keep going|next)$/.test(normalized);
|
|
2767
2913
|
}
|
|
2768
2914
|
getPreviousActionablePrompt() {
|
|
2769
2915
|
if (this.lastActionableUserInput && !this.isConfirmationFollowUp(this.lastActionableUserInput)) {
|
|
@@ -3481,6 +3627,14 @@ export class ChatCommand {
|
|
|
3481
3627
|
this.showProjectMemory();
|
|
3482
3628
|
continue;
|
|
3483
3629
|
}
|
|
3630
|
+
if (trimmed === '/index') {
|
|
3631
|
+
await this.reindexWorkspaceBrain();
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
if (trimmed === '/brain') {
|
|
3635
|
+
this.showBrainIndexStatus();
|
|
3636
|
+
continue;
|
|
3637
|
+
}
|
|
3484
3638
|
if (trimmed === '/compact') {
|
|
3485
3639
|
this.compactCurrentSession();
|
|
3486
3640
|
continue;
|
|
@@ -3629,6 +3783,8 @@ export class ChatCommand {
|
|
|
3629
3783
|
console.log(' /operator Toggle BMAD operator mode');
|
|
3630
3784
|
console.log(' /context Show current session and project memory');
|
|
3631
3785
|
console.log(' /memory Show Vigthoria project brain status');
|
|
3786
|
+
console.log(' /brain Show workspace codebase index status');
|
|
3787
|
+
console.log(' /index Re-index workspace and sync to Brain Hub');
|
|
3632
3788
|
console.log(' /compact Compact current session into memory summary');
|
|
3633
3789
|
console.log(' /clear Clear conversation');
|
|
3634
3790
|
console.log(' /save Save session');
|
|
@@ -4357,6 +4513,15 @@ Now implement the missing fixes by editing the local workspace files. Use write/
|
|
|
4357
4513
|
if (fallback) {
|
|
4358
4514
|
return fallback;
|
|
4359
4515
|
}
|
|
4516
|
+
if (this.agentToolEvidence.discovery === 0 && this.agentToolEvidence.mutation === 0) {
|
|
4517
|
+
if (this.isImplementationPrompt(prompt) || this.isDiagnosticPrompt(prompt)) {
|
|
4518
|
+
return [
|
|
4519
|
+
'The agent did not inspect the workspace or run any tools before finishing.',
|
|
4520
|
+
'This usually means the model backend returned an empty response.',
|
|
4521
|
+
'Try the request again. If it keeps happening, check Vigthoria Coder / vLLM logs on the server.',
|
|
4522
|
+
].join(' ');
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4360
4525
|
return sanitized || 'Task complete.';
|
|
4361
4526
|
}
|
|
4362
4527
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain Hub client — sync workspace index + fetch account brain context
|
|
3
|
+
* via coder.vigthoria.io (JWT-authenticated).
|
|
4
|
+
*/
|
|
5
|
+
export type BrainSyncPayload = {
|
|
6
|
+
workspaceName: string;
|
|
7
|
+
workspacePath: string;
|
|
8
|
+
fileCount: number;
|
|
9
|
+
chunkCount: number;
|
|
10
|
+
topFiles: string[];
|
|
11
|
+
summary: string;
|
|
12
|
+
indexHash: string;
|
|
13
|
+
};
|
|
14
|
+
export type BrainContextResponse = {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
formattedText?: string;
|
|
17
|
+
memories?: Array<Record<string, unknown>>;
|
|
18
|
+
error?: string;
|
|
19
|
+
skipped?: boolean;
|
|
20
|
+
reason?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare class BrainHubClient {
|
|
23
|
+
private apiBase;
|
|
24
|
+
private getAuthToken;
|
|
25
|
+
constructor(options: {
|
|
26
|
+
apiBase?: string;
|
|
27
|
+
getAuthToken: () => string | null | Promise<string | null>;
|
|
28
|
+
});
|
|
29
|
+
private headers;
|
|
30
|
+
syncWorkspaceIndex(payload: BrainSyncPayload): Promise<Record<string, unknown>>;
|
|
31
|
+
fetchAccountContext(limit?: number): Promise<BrainContextResponse>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain Hub client — sync workspace index + fetch account brain context
|
|
3
|
+
* via coder.vigthoria.io (JWT-authenticated).
|
|
4
|
+
*/
|
|
5
|
+
export class BrainHubClient {
|
|
6
|
+
apiBase;
|
|
7
|
+
getAuthToken;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.apiBase = (options.apiBase || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
10
|
+
this.getAuthToken = options.getAuthToken;
|
|
11
|
+
}
|
|
12
|
+
async headers() {
|
|
13
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
14
|
+
const token = await this.getAuthToken();
|
|
15
|
+
if (token) {
|
|
16
|
+
headers.Authorization = `Bearer ${token}`;
|
|
17
|
+
}
|
|
18
|
+
return headers;
|
|
19
|
+
}
|
|
20
|
+
async syncWorkspaceIndex(payload) {
|
|
21
|
+
const headers = await this.headers();
|
|
22
|
+
if (!headers.Authorization) {
|
|
23
|
+
return { ok: false, skipped: true, reason: 'not_authenticated' };
|
|
24
|
+
}
|
|
25
|
+
const resp = await fetch(`${this.apiBase}/api/brain/sync-index`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers,
|
|
28
|
+
body: JSON.stringify(payload),
|
|
29
|
+
});
|
|
30
|
+
const data = await resp.json().catch(() => ({}));
|
|
31
|
+
if (!resp.ok) {
|
|
32
|
+
return { ok: false, error: data.error || `HTTP ${resp.status}` };
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
async fetchAccountContext(limit = 25) {
|
|
37
|
+
const headers = await this.headers();
|
|
38
|
+
if (!headers.Authorization) {
|
|
39
|
+
return { ok: false, formattedText: '', memories: [] };
|
|
40
|
+
}
|
|
41
|
+
const resp = await fetch(`${this.apiBase}/api/brain/context?limit=${limit}`, { headers });
|
|
42
|
+
const data = await resp.json().catch(() => ({}));
|
|
43
|
+
if (!resp.ok) {
|
|
44
|
+
return { ok: false, formattedText: '', error: data.error || `HTTP ${resp.status}` };
|
|
45
|
+
}
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local TF-IDF codebase indexer for CLI (mirrors Vigthoria Code extension indexer).
|
|
3
|
+
*/
|
|
4
|
+
export type CodebaseSearchResult = {
|
|
5
|
+
filePath: string;
|
|
6
|
+
relativePath: string;
|
|
7
|
+
startLine: number;
|
|
8
|
+
endLine: number;
|
|
9
|
+
content: string;
|
|
10
|
+
language: string;
|
|
11
|
+
score: number;
|
|
12
|
+
symbol: string | null;
|
|
13
|
+
};
|
|
14
|
+
export type IndexMeta = {
|
|
15
|
+
indexedFileCount: number;
|
|
16
|
+
totalChunks: number;
|
|
17
|
+
indexHash: string;
|
|
18
|
+
topFiles: string[];
|
|
19
|
+
indexedAt: string;
|
|
20
|
+
};
|
|
21
|
+
export declare class CodebaseIndexer {
|
|
22
|
+
private workspaceRoot;
|
|
23
|
+
private index;
|
|
24
|
+
private invertedIndex;
|
|
25
|
+
private chunkStore;
|
|
26
|
+
private fileHashes;
|
|
27
|
+
private indexedFileCount;
|
|
28
|
+
private totalChunks;
|
|
29
|
+
private isIndexing;
|
|
30
|
+
private readonly maxFileSize;
|
|
31
|
+
private readonly chunkSize;
|
|
32
|
+
private readonly chunkOverlap;
|
|
33
|
+
private readonly maxResults;
|
|
34
|
+
private readonly ignorePatterns;
|
|
35
|
+
private readonly supportedExtensions;
|
|
36
|
+
constructor(workspaceRoot: string);
|
|
37
|
+
getStatus(): {
|
|
38
|
+
indexedFileCount: number;
|
|
39
|
+
totalChunks: number;
|
|
40
|
+
isIndexing: boolean;
|
|
41
|
+
};
|
|
42
|
+
static metaPath(workspaceRoot: string): string;
|
|
43
|
+
static loadMeta(workspaceRoot: string): IndexMeta | null;
|
|
44
|
+
private saveMeta;
|
|
45
|
+
hasLocalIndex(): boolean;
|
|
46
|
+
countCandidateFiles(): number;
|
|
47
|
+
countIndexableFiles(): number;
|
|
48
|
+
indexWorkspace(): Promise<IndexMeta>;
|
|
49
|
+
search(query: string, maxResults?: number): CodebaseSearchResult[];
|
|
50
|
+
getContextForQuery(query: string, maxTokens?: number): string;
|
|
51
|
+
formatSearchResults(query: string, maxResults?: number): string;
|
|
52
|
+
private findFiles;
|
|
53
|
+
private indexFile;
|
|
54
|
+
private chunkFile;
|
|
55
|
+
private tokenize;
|
|
56
|
+
private isStopWord;
|
|
57
|
+
private getLanguage;
|
|
58
|
+
private shouldIgnore;
|
|
59
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local TF-IDF codebase indexer for CLI (mirrors Vigthoria Code extension indexer).
|
|
3
|
+
*/
|
|
4
|
+
import * as crypto from 'crypto';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
export class CodebaseIndexer {
|
|
8
|
+
workspaceRoot;
|
|
9
|
+
index = new Map();
|
|
10
|
+
invertedIndex = new Map();
|
|
11
|
+
chunkStore = new Map();
|
|
12
|
+
fileHashes = new Map();
|
|
13
|
+
indexedFileCount = 0;
|
|
14
|
+
totalChunks = 0;
|
|
15
|
+
isIndexing = false;
|
|
16
|
+
maxFileSize = 1024 * 1024;
|
|
17
|
+
chunkSize = 50;
|
|
18
|
+
chunkOverlap = 5;
|
|
19
|
+
maxResults = 10;
|
|
20
|
+
ignorePatterns = [
|
|
21
|
+
'node_modules', '.git', 'dist', 'build', 'out', '.next',
|
|
22
|
+
'__pycache__', '.pytest_cache', '.mypy_cache', 'venv', 'env',
|
|
23
|
+
'.DS_Store', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
|
|
24
|
+
'.vsix', '.map', '.min.js', '.min.css', 'coverage',
|
|
25
|
+
'.cache', '.tmp', 'tmp', 'temp', '.idea', '.vscode',
|
|
26
|
+
];
|
|
27
|
+
supportedExtensions = new Set([
|
|
28
|
+
'.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rs',
|
|
29
|
+
'.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.swift',
|
|
30
|
+
'.kt', '.scala', '.vue', '.svelte', '.astro',
|
|
31
|
+
'.html', '.css', '.scss', '.less', '.sass',
|
|
32
|
+
'.json', '.yaml', '.yml', '.toml', '.xml',
|
|
33
|
+
'.md', '.txt', '.rst', '.sh', '.bash', '.zsh',
|
|
34
|
+
'.sql', '.graphql', '.prisma', '.env',
|
|
35
|
+
'.dockerfile', '.tf', '.hcl',
|
|
36
|
+
]);
|
|
37
|
+
constructor(workspaceRoot) {
|
|
38
|
+
this.workspaceRoot = path.resolve(workspaceRoot);
|
|
39
|
+
}
|
|
40
|
+
getStatus() {
|
|
41
|
+
return {
|
|
42
|
+
indexedFileCount: this.indexedFileCount,
|
|
43
|
+
totalChunks: this.totalChunks,
|
|
44
|
+
isIndexing: this.isIndexing,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
static metaPath(workspaceRoot) {
|
|
48
|
+
return path.join(workspaceRoot, '.vigthoria', 'index', 'meta.json');
|
|
49
|
+
}
|
|
50
|
+
static loadMeta(workspaceRoot) {
|
|
51
|
+
try {
|
|
52
|
+
const metaPath = CodebaseIndexer.metaPath(workspaceRoot);
|
|
53
|
+
if (fs.existsSync(metaPath)) {
|
|
54
|
+
return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// fresh workspace
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
saveMeta(meta) {
|
|
63
|
+
const dir = path.join(this.workspaceRoot, '.vigthoria', 'index');
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
fs.writeFileSync(CodebaseIndexer.metaPath(this.workspaceRoot), `${JSON.stringify(meta, null, 2)}\n`);
|
|
66
|
+
}
|
|
67
|
+
hasLocalIndex() {
|
|
68
|
+
const meta = CodebaseIndexer.loadMeta(this.workspaceRoot);
|
|
69
|
+
return !!(meta && meta.indexedFileCount > 0) || this.totalChunks > 0;
|
|
70
|
+
}
|
|
71
|
+
countCandidateFiles() {
|
|
72
|
+
return this.findFiles(this.workspaceRoot).length;
|
|
73
|
+
}
|
|
74
|
+
countIndexableFiles() {
|
|
75
|
+
return this.findFiles(this.workspaceRoot).length;
|
|
76
|
+
}
|
|
77
|
+
async indexWorkspace() {
|
|
78
|
+
if (this.isIndexing) {
|
|
79
|
+
return {
|
|
80
|
+
indexedFileCount: this.indexedFileCount,
|
|
81
|
+
totalChunks: this.totalChunks,
|
|
82
|
+
indexHash: '',
|
|
83
|
+
topFiles: [],
|
|
84
|
+
indexedAt: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
this.isIndexing = true;
|
|
88
|
+
this.index.clear();
|
|
89
|
+
this.invertedIndex.clear();
|
|
90
|
+
this.chunkStore.clear();
|
|
91
|
+
this.fileHashes.clear();
|
|
92
|
+
this.indexedFileCount = 0;
|
|
93
|
+
this.totalChunks = 0;
|
|
94
|
+
try {
|
|
95
|
+
const files = this.findFiles(this.workspaceRoot);
|
|
96
|
+
for (const filePath of files) {
|
|
97
|
+
this.indexFile(filePath);
|
|
98
|
+
}
|
|
99
|
+
const indexHash = crypto.createHash('sha256')
|
|
100
|
+
.update(`${this.indexedFileCount}:${this.totalChunks}:${files.length}`)
|
|
101
|
+
.digest('hex')
|
|
102
|
+
.slice(0, 16);
|
|
103
|
+
const topFiles = files.slice(0, 12).map((filePath) => path.relative(this.workspaceRoot, filePath));
|
|
104
|
+
const meta = {
|
|
105
|
+
indexedFileCount: this.indexedFileCount,
|
|
106
|
+
totalChunks: this.totalChunks,
|
|
107
|
+
indexHash,
|
|
108
|
+
topFiles,
|
|
109
|
+
indexedAt: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
this.saveMeta(meta);
|
|
112
|
+
return meta;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.isIndexing = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
search(query, maxResults = this.maxResults) {
|
|
119
|
+
if (this.totalChunks === 0) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const queryTerms = this.tokenize(query.toLowerCase());
|
|
123
|
+
const scores = new Map();
|
|
124
|
+
for (const term of queryTerms) {
|
|
125
|
+
const matchingChunks = this.invertedIndex.get(term);
|
|
126
|
+
if (!matchingChunks)
|
|
127
|
+
continue;
|
|
128
|
+
const idf = Math.log(this.totalChunks / matchingChunks.size);
|
|
129
|
+
for (const chunkId of matchingChunks) {
|
|
130
|
+
const chunk = this.chunkStore.get(chunkId);
|
|
131
|
+
if (!chunk)
|
|
132
|
+
continue;
|
|
133
|
+
const tf = (chunk.terms.get(term) || 0) / chunk.totalTerms;
|
|
134
|
+
scores.set(chunkId, (scores.get(chunkId) || 0) + tf * idf);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const [chunkId, score] of scores) {
|
|
138
|
+
const chunk = this.chunkStore.get(chunkId);
|
|
139
|
+
if (!chunk)
|
|
140
|
+
continue;
|
|
141
|
+
const fileName = path.basename(chunk.filePath).toLowerCase();
|
|
142
|
+
for (const term of queryTerms) {
|
|
143
|
+
if (fileName.includes(term)) {
|
|
144
|
+
scores.set(chunkId, score * 1.5);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return Array.from(scores.entries())
|
|
149
|
+
.sort((a, b) => b[1] - a[1])
|
|
150
|
+
.slice(0, maxResults)
|
|
151
|
+
.map(([chunkId, score]) => {
|
|
152
|
+
const chunk = this.chunkStore.get(chunkId);
|
|
153
|
+
return {
|
|
154
|
+
filePath: chunk.filePath,
|
|
155
|
+
relativePath: chunk.relativePath,
|
|
156
|
+
startLine: chunk.startLine,
|
|
157
|
+
endLine: chunk.endLine,
|
|
158
|
+
content: chunk.content,
|
|
159
|
+
language: chunk.language,
|
|
160
|
+
score: Math.round(score * 1000) / 1000,
|
|
161
|
+
symbol: chunk.symbol,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
getContextForQuery(query, maxTokens = 4000) {
|
|
166
|
+
const results = this.search(query, 15);
|
|
167
|
+
if (results.length === 0) {
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
let context = '### Relevant code from workspace:\n\n';
|
|
171
|
+
let currentTokens = 0;
|
|
172
|
+
const avgCharsPerToken = 4;
|
|
173
|
+
for (const result of results) {
|
|
174
|
+
const entry = `**${result.relativePath}** (lines ${result.startLine}-${result.endLine}):\n\`\`\`${result.language}\n${result.content}\n\`\`\`\n\n`;
|
|
175
|
+
const entryTokens = Math.ceil(entry.length / avgCharsPerToken);
|
|
176
|
+
if (currentTokens + entryTokens > maxTokens)
|
|
177
|
+
break;
|
|
178
|
+
context += entry;
|
|
179
|
+
currentTokens += entryTokens;
|
|
180
|
+
}
|
|
181
|
+
return context;
|
|
182
|
+
}
|
|
183
|
+
formatSearchResults(query, maxResults = 30) {
|
|
184
|
+
const results = this.search(query, Math.min(maxResults, 20));
|
|
185
|
+
if (results.length === 0) {
|
|
186
|
+
return 'No indexed codebase matches found. Run /index or wait for workspace indexing to finish.';
|
|
187
|
+
}
|
|
188
|
+
return results.map((result) => (`[indexed:${result.score}] ${result.relativePath}:${result.startLine}-${result.endLine}`
|
|
189
|
+
+ (result.symbol ? ` (${result.symbol})` : '')
|
|
190
|
+
+ `\n${result.content.split('\n').slice(0, 8).join('\n')}`)).join('\n\n');
|
|
191
|
+
}
|
|
192
|
+
findFiles(rootPath) {
|
|
193
|
+
const files = [];
|
|
194
|
+
const walk = (currentDir, depth) => {
|
|
195
|
+
if (depth > 14 || files.length >= 10000)
|
|
196
|
+
return;
|
|
197
|
+
let entries;
|
|
198
|
+
try {
|
|
199
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (this.shouldIgnore(entry.name, path.join(currentDir, entry.name)))
|
|
206
|
+
continue;
|
|
207
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
walk(fullPath, depth + 1);
|
|
210
|
+
}
|
|
211
|
+
else if (entry.isFile()) {
|
|
212
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
213
|
+
if (this.supportedExtensions.has(ext)) {
|
|
214
|
+
files.push(fullPath);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
walk(rootPath, 0);
|
|
220
|
+
return files;
|
|
221
|
+
}
|
|
222
|
+
indexFile(filePath) {
|
|
223
|
+
try {
|
|
224
|
+
const stat = fs.statSync(filePath);
|
|
225
|
+
if (!stat.isFile() || stat.size > this.maxFileSize || stat.size === 0)
|
|
226
|
+
return;
|
|
227
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
228
|
+
const hash = crypto.createHash('md5').update(content).digest('hex');
|
|
229
|
+
if (this.fileHashes.get(filePath) === hash)
|
|
230
|
+
return;
|
|
231
|
+
this.fileHashes.set(filePath, hash);
|
|
232
|
+
const relativePath = path.relative(this.workspaceRoot, filePath);
|
|
233
|
+
const language = this.getLanguage(filePath);
|
|
234
|
+
const lines = content.split('\n');
|
|
235
|
+
const chunks = this.chunkFile(lines, filePath, relativePath, language);
|
|
236
|
+
const chunkIds = [];
|
|
237
|
+
for (const chunk of chunks) {
|
|
238
|
+
const chunkId = `${relativePath}:${chunk.startLine}-${chunk.endLine}`;
|
|
239
|
+
const terms = new Map();
|
|
240
|
+
const tokens = this.tokenize(chunk.content.toLowerCase());
|
|
241
|
+
let totalTerms = 0;
|
|
242
|
+
for (const token of tokens) {
|
|
243
|
+
terms.set(token, (terms.get(token) || 0) + 1);
|
|
244
|
+
totalTerms += 1;
|
|
245
|
+
if (!this.invertedIndex.has(token)) {
|
|
246
|
+
this.invertedIndex.set(token, new Set());
|
|
247
|
+
}
|
|
248
|
+
this.invertedIndex.get(token).add(chunkId);
|
|
249
|
+
}
|
|
250
|
+
this.chunkStore.set(chunkId, { ...chunk, terms, totalTerms });
|
|
251
|
+
chunkIds.push(chunkId);
|
|
252
|
+
this.totalChunks += 1;
|
|
253
|
+
}
|
|
254
|
+
this.index.set(filePath, chunkIds);
|
|
255
|
+
this.indexedFileCount += 1;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// skip unreadable files
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
chunkFile(lines, filePath, relativePath, language) {
|
|
262
|
+
const chunks = [];
|
|
263
|
+
for (let i = 0; i < lines.length; i += this.chunkSize - this.chunkOverlap) {
|
|
264
|
+
const endLine = Math.min(i + this.chunkSize, lines.length);
|
|
265
|
+
const chunkLines = lines.slice(i, endLine);
|
|
266
|
+
if (chunkLines.join('').trim().length === 0)
|
|
267
|
+
continue;
|
|
268
|
+
chunks.push({
|
|
269
|
+
filePath,
|
|
270
|
+
relativePath,
|
|
271
|
+
startLine: i + 1,
|
|
272
|
+
endLine,
|
|
273
|
+
content: chunkLines.join('\n'),
|
|
274
|
+
language,
|
|
275
|
+
symbol: null,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return chunks;
|
|
279
|
+
}
|
|
280
|
+
tokenize(text) {
|
|
281
|
+
return text
|
|
282
|
+
.replace(/[^a-zA-Z0-9_$]/g, ' ')
|
|
283
|
+
.split(/\s+/)
|
|
284
|
+
.filter((token) => token.length > 2)
|
|
285
|
+
.filter((token) => !this.isStopWord(token));
|
|
286
|
+
}
|
|
287
|
+
isStopWord(word) {
|
|
288
|
+
const stopWords = new Set([
|
|
289
|
+
'the', 'and', 'for', 'that', 'this', 'with', 'from', 'are',
|
|
290
|
+
'was', 'were', 'been', 'have', 'has', 'had', 'not', 'but',
|
|
291
|
+
'its', 'can', 'will', 'would', 'could', 'should', 'may',
|
|
292
|
+
'var', 'let', 'const', 'function', 'return', 'true', 'false',
|
|
293
|
+
'null', 'undefined', 'new', 'class', 'import', 'export',
|
|
294
|
+
'require', 'module', 'exports', 'default', 'async', 'await',
|
|
295
|
+
]);
|
|
296
|
+
return stopWords.has(word);
|
|
297
|
+
}
|
|
298
|
+
getLanguage(filePath) {
|
|
299
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
300
|
+
const map = {
|
|
301
|
+
'.js': 'javascript', '.jsx': 'javascript',
|
|
302
|
+
'.ts': 'typescript', '.tsx': 'typescript',
|
|
303
|
+
'.py': 'python', '.html': 'html', '.css': 'css', '.json': 'json',
|
|
304
|
+
'.md': 'markdown', '.sh': 'shell', '.go': 'go', '.rs': 'rust',
|
|
305
|
+
};
|
|
306
|
+
return map[ext] || 'text';
|
|
307
|
+
}
|
|
308
|
+
shouldIgnore(name, fullPath) {
|
|
309
|
+
if (name.startsWith('.') && name !== '.env')
|
|
310
|
+
return true;
|
|
311
|
+
const lowerPath = fullPath.toLowerCase();
|
|
312
|
+
return this.ignorePatterns.some((pattern) => lowerPath.includes(pattern));
|
|
313
|
+
}
|
|
314
|
+
}
|
package/dist/utils/tools.d.ts
CHANGED
|
@@ -95,12 +95,14 @@ export declare class AgenticTools {
|
|
|
95
95
|
private requireNonEmptyString;
|
|
96
96
|
private requireArgsObject;
|
|
97
97
|
private sessionApprovedTools;
|
|
98
|
+
private indexedCodebaseSearch;
|
|
98
99
|
private static permissionsFile;
|
|
99
100
|
constructor(logger: Logger, cwd: string, permissionCallback: (action: string, options?: {
|
|
100
101
|
batchApproval?: boolean;
|
|
101
102
|
}) => Promise<boolean | 'batch' | 'persist'>, autoApprove?: boolean);
|
|
102
103
|
/** Rebind tool execution to a different local workspace root (e.g. prompt path override). */
|
|
103
104
|
setWorkspaceRoot(cwd: string): void;
|
|
105
|
+
setIndexedCodebaseSearch(handler: ((query: string, maxResults: number) => string) | null): void;
|
|
104
106
|
getWorkspaceRoot(): string;
|
|
105
107
|
private getErrorMessage;
|
|
106
108
|
private assertToolCall;
|
package/dist/utils/tools.js
CHANGED
|
@@ -433,6 +433,7 @@ export class AgenticTools {
|
|
|
433
433
|
}
|
|
434
434
|
// Session-based tool approvals - remembers which tools user approved for this turn
|
|
435
435
|
sessionApprovedTools = new Set();
|
|
436
|
+
indexedCodebaseSearch = null;
|
|
436
437
|
// Persistent permissions - tool allowlists per project
|
|
437
438
|
static permissionsFile = path.join(process.env.HOME || process.env.USERPROFILE || '~', '.vigthoria', 'permissions.json');
|
|
438
439
|
constructor(logger, cwd, permissionCallback, autoApprove = false) {
|
|
@@ -460,6 +461,9 @@ export class AgenticTools {
|
|
|
460
461
|
}
|
|
461
462
|
this.cwd = path.resolve(cwd);
|
|
462
463
|
}
|
|
464
|
+
setIndexedCodebaseSearch(handler) {
|
|
465
|
+
this.indexedCodebaseSearch = handler;
|
|
466
|
+
}
|
|
463
467
|
getWorkspaceRoot() {
|
|
464
468
|
return this.cwd;
|
|
465
469
|
}
|
|
@@ -2815,6 +2819,16 @@ export class AgenticTools {
|
|
|
2815
2819
|
const scope = args.scope || 'all';
|
|
2816
2820
|
const includePattern = args.include || '';
|
|
2817
2821
|
const maxResults = Math.min(parseInt(args.max_results || '30', 10), 100);
|
|
2822
|
+
if (this.indexedCodebaseSearch && (scope === 'all' || scope === 'content')) {
|
|
2823
|
+
const indexedOutput = this.indexedCodebaseSearch(query, maxResults);
|
|
2824
|
+
if (indexedOutput && !indexedOutput.includes('No indexed codebase matches found')) {
|
|
2825
|
+
return {
|
|
2826
|
+
success: true,
|
|
2827
|
+
output: indexedOutput,
|
|
2828
|
+
metadata: { searchStatus: 'search_matches_found', source: 'tfidf-index' },
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2818
2832
|
const results = [];
|
|
2819
2833
|
const seen = new Set();
|
|
2820
2834
|
// Helper: collect files recursively respecting gitignore-like patterns
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Brain — local codebase index + Brain Hub sync for CLI.
|
|
3
|
+
*/
|
|
4
|
+
import { CodebaseIndexer, IndexMeta } from './codebase-indexer.js';
|
|
5
|
+
export type WorkspaceBrainOptions = {
|
|
6
|
+
workspacePath: string;
|
|
7
|
+
apiBase?: string;
|
|
8
|
+
getAuthToken: () => string | null | Promise<string | null>;
|
|
9
|
+
autoIndex?: boolean;
|
|
10
|
+
brainSyncEnabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type EnsureIndexedResult = {
|
|
13
|
+
indexed: boolean;
|
|
14
|
+
fileCount: number;
|
|
15
|
+
chunkCount: number;
|
|
16
|
+
prompted?: 'index_now' | 'later' | 'skipped';
|
|
17
|
+
};
|
|
18
|
+
export declare class WorkspaceBrainService {
|
|
19
|
+
private workspacePath;
|
|
20
|
+
private indexer;
|
|
21
|
+
private brainClient;
|
|
22
|
+
private autoIndex;
|
|
23
|
+
private brainSyncEnabled;
|
|
24
|
+
private accountContextCache;
|
|
25
|
+
constructor(options: WorkspaceBrainOptions);
|
|
26
|
+
getIndexer(): CodebaseIndexer;
|
|
27
|
+
getStatus(): {
|
|
28
|
+
workspacePath: string;
|
|
29
|
+
indexedFileCount: number;
|
|
30
|
+
totalChunks: number;
|
|
31
|
+
isIndexing: boolean;
|
|
32
|
+
meta: IndexMeta | null;
|
|
33
|
+
};
|
|
34
|
+
ensureIndexed(options?: {
|
|
35
|
+
promptIfMissing?: boolean;
|
|
36
|
+
askToIndex?: (fileCount: number, workspaceName: string) => Promise<'index_now' | 'later'>;
|
|
37
|
+
}): Promise<EnsureIndexedResult>;
|
|
38
|
+
reindexWorkspace(): Promise<IndexMeta>;
|
|
39
|
+
buildCodebaseContext(prompt: string): string;
|
|
40
|
+
searchCodebase(query: string, maxResults?: number): string;
|
|
41
|
+
fetchAccountBrainContext(force?: boolean): Promise<string>;
|
|
42
|
+
private syncIndexToHub;
|
|
43
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Brain — local codebase index + Brain Hub sync for CLI.
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { BrainHubClient } from './brain-hub-client.js';
|
|
6
|
+
import { CodebaseIndexer } from './codebase-indexer.js';
|
|
7
|
+
export class WorkspaceBrainService {
|
|
8
|
+
workspacePath;
|
|
9
|
+
indexer;
|
|
10
|
+
brainClient;
|
|
11
|
+
autoIndex;
|
|
12
|
+
brainSyncEnabled;
|
|
13
|
+
accountContextCache = null;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.workspacePath = path.resolve(options.workspacePath);
|
|
16
|
+
this.indexer = new CodebaseIndexer(this.workspacePath);
|
|
17
|
+
this.brainClient = new BrainHubClient({
|
|
18
|
+
apiBase: options.apiBase,
|
|
19
|
+
getAuthToken: options.getAuthToken,
|
|
20
|
+
});
|
|
21
|
+
this.autoIndex = options.autoIndex ?? !/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_AUTO_INDEX || ''));
|
|
22
|
+
this.brainSyncEnabled = options.brainSyncEnabled ?? !/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_BRAIN_SYNC || ''));
|
|
23
|
+
}
|
|
24
|
+
getIndexer() {
|
|
25
|
+
return this.indexer;
|
|
26
|
+
}
|
|
27
|
+
getStatus() {
|
|
28
|
+
const status = this.indexer.getStatus();
|
|
29
|
+
return {
|
|
30
|
+
workspacePath: this.workspacePath,
|
|
31
|
+
indexedFileCount: status.indexedFileCount,
|
|
32
|
+
totalChunks: status.totalChunks,
|
|
33
|
+
isIndexing: status.isIndexing,
|
|
34
|
+
meta: CodebaseIndexer.loadMeta(this.workspacePath),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async ensureIndexed(options = {}) {
|
|
38
|
+
if (this.indexer.hasLocalIndex() && this.indexer.getStatus().totalChunks === 0) {
|
|
39
|
+
const meta = await this.indexer.indexWorkspace();
|
|
40
|
+
await this.syncIndexToHub(meta);
|
|
41
|
+
return { indexed: true, fileCount: meta.indexedFileCount, chunkCount: meta.totalChunks };
|
|
42
|
+
}
|
|
43
|
+
if (this.indexer.hasLocalIndex()) {
|
|
44
|
+
const meta = CodebaseIndexer.loadMeta(this.workspacePath);
|
|
45
|
+
return {
|
|
46
|
+
indexed: true,
|
|
47
|
+
fileCount: meta?.indexedFileCount || this.indexer.getStatus().indexedFileCount,
|
|
48
|
+
chunkCount: meta?.totalChunks || this.indexer.getStatus().totalChunks,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (!this.autoIndex && !options.promptIfMissing) {
|
|
52
|
+
return { indexed: false, fileCount: 0, chunkCount: 0, prompted: 'skipped' };
|
|
53
|
+
}
|
|
54
|
+
if (options.promptIfMissing && options.askToIndex) {
|
|
55
|
+
const wsName = path.basename(this.workspacePath);
|
|
56
|
+
const fileCount = this.indexer.countIndexableFiles();
|
|
57
|
+
if (fileCount === 0) {
|
|
58
|
+
return { indexed: false, fileCount: 0, chunkCount: 0, prompted: 'skipped' };
|
|
59
|
+
}
|
|
60
|
+
const choice = await options.askToIndex(fileCount, wsName);
|
|
61
|
+
if (choice === 'later') {
|
|
62
|
+
return { indexed: false, fileCount: 0, chunkCount: 0, prompted: 'later' };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const meta = await this.indexer.indexWorkspace();
|
|
66
|
+
await this.syncIndexToHub(meta);
|
|
67
|
+
return {
|
|
68
|
+
indexed: true,
|
|
69
|
+
fileCount: meta.indexedFileCount,
|
|
70
|
+
chunkCount: meta.totalChunks,
|
|
71
|
+
prompted: options.promptIfMissing ? 'index_now' : undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async reindexWorkspace() {
|
|
75
|
+
const meta = await this.indexer.indexWorkspace();
|
|
76
|
+
await this.syncIndexToHub(meta);
|
|
77
|
+
return meta;
|
|
78
|
+
}
|
|
79
|
+
buildCodebaseContext(prompt) {
|
|
80
|
+
if (!prompt.trim() || !this.indexer.hasLocalIndex()) {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
const context = this.indexer.getContextForQuery(prompt, 3500);
|
|
84
|
+
if (!context) {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
return `Vigthoria codebase index context.\n${context}`;
|
|
88
|
+
}
|
|
89
|
+
searchCodebase(query, maxResults = 30) {
|
|
90
|
+
return this.indexer.formatSearchResults(query, maxResults);
|
|
91
|
+
}
|
|
92
|
+
async fetchAccountBrainContext(force = false) {
|
|
93
|
+
if (!this.brainSyncEnabled) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
const cacheTtlMs = 5 * 60 * 1000;
|
|
97
|
+
if (!force && this.accountContextCache && Date.now() - this.accountContextCache.fetchedAt < cacheTtlMs) {
|
|
98
|
+
return this.accountContextCache.text;
|
|
99
|
+
}
|
|
100
|
+
const response = await this.brainClient.fetchAccountContext(25);
|
|
101
|
+
const text = String(response.formattedText || '').trim();
|
|
102
|
+
this.accountContextCache = { text, fetchedAt: Date.now() };
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
105
|
+
async syncIndexToHub(meta) {
|
|
106
|
+
if (!this.brainSyncEnabled) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const wsName = path.basename(this.workspacePath);
|
|
110
|
+
const summary = `Workspace "${wsName}" indexed in Vigthoria CLI: ${meta.indexedFileCount} files, ${meta.totalChunks} chunks. Key paths: ${meta.topFiles.slice(0, 6).join(', ')}`;
|
|
111
|
+
await this.brainClient.syncWorkspaceIndex({
|
|
112
|
+
workspaceName: wsName,
|
|
113
|
+
workspacePath: this.workspacePath,
|
|
114
|
+
fileCount: meta.indexedFileCount,
|
|
115
|
+
chunkCount: meta.totalChunks,
|
|
116
|
+
topFiles: meta.topFiles,
|
|
117
|
+
summary,
|
|
118
|
+
indexHash: meta.indexHash,
|
|
119
|
+
}).catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -46,7 +46,7 @@ import('./dist/utils/context-ranker.js').then((m)=>{
|
|
|
46
46
|
EOF2
|
|
47
47
|
|
|
48
48
|
echo "[2.1/2.4] tsc validator emits passing result"
|
|
49
|
-
node - << 'EOF2'
|
|
49
|
+
node --input-type=module - << 'EOF2'
|
|
50
50
|
import fs from 'node:fs';
|
|
51
51
|
import os from 'node:os';
|
|
52
52
|
import path from 'node:path';
|