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.
@@ -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;
@@ -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
- this.messages.push({ role: 'user', content: this.buildScopedUserPrompt(prompt) });
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.isDiagnosticPrompt(prompt) || this.directPromptMode || isPolicyAck);
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
+ }
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.10.36",
3
+ "version": "1.10.37",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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';