jinzd-ai-cli 0.4.75 → 0.4.76

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 CHANGED
@@ -29,6 +29,7 @@
29
29
  - **Anthropic Batches API** *(v0.4.73+)* — `aicli batch submit/list/status/results/cancel` for 50%-off, 24-hour async processing — ideal for offline analysis and bulk evals
30
30
  - **Web UI Session Replay** *(v0.4.71+)* — 🎬 button on every saved session opens a timeline replay: every message, tool call, reasoning, and cache-aware token usage at a glance
31
31
  - **Conversation Branching** *(v0.4.74+)* — `/branch list/new/switch/delete/rename` inside the REPL, plus a 🌿 "fork here" button on every replay step — explore alternate directions without losing the original thread
32
+ - **Symbol Index** *(v0.4.76+)* — persistent tree-sitter index for TS/JS/TSX/Python powers three new AI tools: `find_symbol`, `get_outline`, `find_references`. Orders of magnitude faster than grep for definition lookups; background refresh on REPL startup, `/index status|rebuild|clear` to manage
32
33
  - **Streaming Tool Use** — Real-time streaming of AI reasoning and tool calls as they happen
33
34
  - **Sub-Agents** — Delegate complex subtasks to isolated child agents with independent tool loops
34
35
  - **Extended Thinking** — Claude deep reasoning mode with `/think` toggle
@@ -145,7 +146,7 @@ aicli
145
146
 
146
147
  ## Built-in Tools (Agentic)
147
148
 
148
- AI autonomously invokes these 24 tools during conversations:
149
+ AI autonomously invokes these 27 tools during conversations:
149
150
 
150
151
  | Tool | Safety | Description |
151
152
  |------|--------|-------------|
@@ -173,6 +174,9 @@ AI autonomously invokes these 24 tools during conversations:
173
174
  | `git_log` | safe | Show commit history (oneline/full, filter by file/author) |
174
175
  | `git_commit` | write | Create a git commit (stage files, message) |
175
176
  | `notebook_edit` | write | Edit Jupyter notebook cells (add/edit/delete/move) |
177
+ | `find_symbol` | safe | Locate symbol definitions via persistent tree-sitter index (TS/JS/TSX/Python) |
178
+ | `get_outline` | safe | Enumerate all top-level declarations in one source file |
179
+ | `find_references` | safe | Search indexed files for references to a symbol name |
176
180
 
177
181
  **Safety levels**: `safe` = auto-execute, `write` = diff preview + confirmation, `destructive` = prominent warning + confirmation.
178
182
 
@@ -195,6 +199,7 @@ AI autonomously invokes these 24 tools during conversations:
195
199
  | `/checkpoint` | Save/restore conversation checkpoints |
196
200
  | `/fork` | Fork the current session into a new session file |
197
201
  | `/branch` | Create/switch/delete branches *within* the current session (B2) |
202
+ | `/index` | Manage tree-sitter symbol index (status/rebuild/clear) — powers `find_symbol` / `get_outline` / `find_references` (C1) |
198
203
  | `/search <keyword>` | Full-text search across all sessions |
199
204
  | `/skill` | Manage agent skill packs |
200
205
  | `/mcp` | View MCP server status and tools |
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/core/constants.ts
4
- var VERSION = "0.4.75";
4
+ var VERSION = "0.4.76";
5
5
  var APP_NAME = "ai-cli";
6
6
  var CONFIG_DIR_NAME = ".aicli";
7
7
  var CONFIG_FILE_NAME = "config.json";
@@ -30,8 +30,14 @@ var PLAN_MODE_READONLY_TOOLS = /* @__PURE__ */ new Set([
30
30
  "google_search",
31
31
  "ask_user",
32
32
  // 允许:可向用户澄清需求
33
- "write_todos"
33
+ "write_todos",
34
34
  // 允许:可输出任务列表作为实施计划
35
+ "find_symbol",
36
+ // C1 symbol index (read-only)
37
+ "get_outline",
38
+ // C1 symbol index (read-only)
39
+ "find_references"
40
+ // C1 symbol index (read-only)
35
41
  ]);
36
42
  var PLAN_MODE_SYSTEM_ADDON = `# \u{1F50D} Plan Mode \u2014 Read-Only Planning Mode
37
43
 
@@ -71,7 +77,10 @@ var SUBAGENT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
71
77
  "web_fetch",
72
78
  "google_search",
73
79
  "write_todos",
74
- "run_tests"
80
+ "run_tests",
81
+ "find_symbol",
82
+ "get_outline",
83
+ "find_references"
75
84
  ]);
76
85
  var CONTEXT_PRESSURE_THRESHOLD = 0.8;
77
86
  var TEST_TIMEOUT = 3e5;
@@ -6,7 +6,7 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.75";
9
+ var VERSION = "0.4.76";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -35,8 +35,14 @@ var PLAN_MODE_READONLY_TOOLS = /* @__PURE__ */ new Set([
35
35
  "google_search",
36
36
  "ask_user",
37
37
  // 允许:可向用户澄清需求
38
- "write_todos"
38
+ "write_todos",
39
39
  // 允许:可输出任务列表作为实施计划
40
+ "find_symbol",
41
+ // C1 symbol index (read-only)
42
+ "get_outline",
43
+ // C1 symbol index (read-only)
44
+ "find_references"
45
+ // C1 symbol index (read-only)
40
46
  ]);
41
47
  var PLAN_MODE_SYSTEM_ADDON = `# \u{1F50D} Plan Mode \u2014 Read-Only Planning Mode
42
48
 
@@ -76,7 +82,10 @@ var SUBAGENT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
76
82
  "web_fetch",
77
83
  "google_search",
78
84
  "write_todos",
79
- "run_tests"
85
+ "run_tests",
86
+ "find_symbol",
87
+ "get_outline",
88
+ "find_references"
80
89
  ]);
81
90
  var TEST_TIMEOUT = 3e5;
82
91
  var AGENTIC_BEHAVIOR_GUIDELINE = `# Important Behavioral Guidelines
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/symbols/store.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import os from "os";
7
+ import crypto from "crypto";
8
+
9
+ // src/symbols/types.ts
10
+ var SYMBOL_INDEX_VERSION = 1;
11
+ function detectLanguage(ext) {
12
+ switch (ext.toLowerCase()) {
13
+ case "ts":
14
+ case "mts":
15
+ case "cts":
16
+ return "typescript";
17
+ case "tsx":
18
+ return "tsx";
19
+ case "js":
20
+ case "mjs":
21
+ case "cjs":
22
+ case "jsx":
23
+ return "javascript";
24
+ case "py":
25
+ case "pyi":
26
+ return "python";
27
+ default:
28
+ return null;
29
+ }
30
+ }
31
+
32
+ // src/symbols/store.ts
33
+ var INDEX_DIR_NAME = "index";
34
+ function indexDir() {
35
+ return path.join(os.homedir(), ".aicli", INDEX_DIR_NAME);
36
+ }
37
+ function projectHash(root) {
38
+ const normalized = path.resolve(root).toLowerCase();
39
+ return crypto.createHash("sha1").update(normalized).digest("hex").slice(0, 16);
40
+ }
41
+ function indexPath(root) {
42
+ return path.join(indexDir(), `${projectHash(root)}.json`);
43
+ }
44
+ function loadIndex(root) {
45
+ const p = indexPath(root);
46
+ try {
47
+ if (!fs.existsSync(p)) return null;
48
+ const raw = fs.readFileSync(p, "utf-8");
49
+ const parsed = JSON.parse(raw);
50
+ if (parsed.version !== SYMBOL_INDEX_VERSION) return null;
51
+ if (parsed.root && path.resolve(parsed.root) !== path.resolve(root)) {
52
+ return null;
53
+ }
54
+ return parsed;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function saveIndex(index) {
60
+ const dir = indexDir();
61
+ fs.mkdirSync(dir, { recursive: true });
62
+ const target = indexPath(index.root);
63
+ const tmp = `${target}.tmp`;
64
+ fs.writeFileSync(tmp, JSON.stringify(index), "utf-8");
65
+ fs.renameSync(tmp, target);
66
+ }
67
+ function clearIndex(root) {
68
+ const p = indexPath(root);
69
+ try {
70
+ if (fs.existsSync(p)) fs.unlinkSync(p);
71
+ } catch {
72
+ }
73
+ }
74
+ function emptyIndex(root) {
75
+ return {
76
+ version: SYMBOL_INDEX_VERSION,
77
+ root: path.resolve(root),
78
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
79
+ fileCount: 0,
80
+ symbolCount: 0,
81
+ files: {},
82
+ symbols: []
83
+ };
84
+ }
85
+ function upsertFileSymbols(index, file, mtime, symbols) {
86
+ const absFile = path.resolve(file);
87
+ const had = absFile in index.files;
88
+ index.symbols = index.symbols.filter((s) => s.location.file !== absFile);
89
+ index.symbols.push(...symbols);
90
+ index.files[absFile] = mtime;
91
+ index.symbolCount = index.symbols.length;
92
+ if (!had) index.fileCount = Object.keys(index.files).length;
93
+ index.generated = (/* @__PURE__ */ new Date()).toISOString();
94
+ return index;
95
+ }
96
+ function removeFile(index, file) {
97
+ const absFile = path.resolve(file);
98
+ if (!(absFile in index.files)) return index;
99
+ index.symbols = index.symbols.filter((s) => s.location.file !== absFile);
100
+ delete index.files[absFile];
101
+ index.symbolCount = index.symbols.length;
102
+ index.fileCount = Object.keys(index.files).length;
103
+ index.generated = (/* @__PURE__ */ new Date()).toISOString();
104
+ return index;
105
+ }
106
+
107
+ export {
108
+ detectLanguage,
109
+ loadIndex,
110
+ saveIndex,
111
+ clearIndex,
112
+ emptyIndex,
113
+ upsertFileSymbols,
114
+ removeFile
115
+ };
@@ -0,0 +1,113 @@
1
+ // src/symbols/store.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ import crypto from "crypto";
6
+
7
+ // src/symbols/types.ts
8
+ var SYMBOL_INDEX_VERSION = 1;
9
+ function detectLanguage(ext) {
10
+ switch (ext.toLowerCase()) {
11
+ case "ts":
12
+ case "mts":
13
+ case "cts":
14
+ return "typescript";
15
+ case "tsx":
16
+ return "tsx";
17
+ case "js":
18
+ case "mjs":
19
+ case "cjs":
20
+ case "jsx":
21
+ return "javascript";
22
+ case "py":
23
+ case "pyi":
24
+ return "python";
25
+ default:
26
+ return null;
27
+ }
28
+ }
29
+
30
+ // src/symbols/store.ts
31
+ var INDEX_DIR_NAME = "index";
32
+ function indexDir() {
33
+ return path.join(os.homedir(), ".aicli", INDEX_DIR_NAME);
34
+ }
35
+ function projectHash(root) {
36
+ const normalized = path.resolve(root).toLowerCase();
37
+ return crypto.createHash("sha1").update(normalized).digest("hex").slice(0, 16);
38
+ }
39
+ function indexPath(root) {
40
+ return path.join(indexDir(), `${projectHash(root)}.json`);
41
+ }
42
+ function loadIndex(root) {
43
+ const p = indexPath(root);
44
+ try {
45
+ if (!fs.existsSync(p)) return null;
46
+ const raw = fs.readFileSync(p, "utf-8");
47
+ const parsed = JSON.parse(raw);
48
+ if (parsed.version !== SYMBOL_INDEX_VERSION) return null;
49
+ if (parsed.root && path.resolve(parsed.root) !== path.resolve(root)) {
50
+ return null;
51
+ }
52
+ return parsed;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ function saveIndex(index) {
58
+ const dir = indexDir();
59
+ fs.mkdirSync(dir, { recursive: true });
60
+ const target = indexPath(index.root);
61
+ const tmp = `${target}.tmp`;
62
+ fs.writeFileSync(tmp, JSON.stringify(index), "utf-8");
63
+ fs.renameSync(tmp, target);
64
+ }
65
+ function clearIndex(root) {
66
+ const p = indexPath(root);
67
+ try {
68
+ if (fs.existsSync(p)) fs.unlinkSync(p);
69
+ } catch {
70
+ }
71
+ }
72
+ function emptyIndex(root) {
73
+ return {
74
+ version: SYMBOL_INDEX_VERSION,
75
+ root: path.resolve(root),
76
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
77
+ fileCount: 0,
78
+ symbolCount: 0,
79
+ files: {},
80
+ symbols: []
81
+ };
82
+ }
83
+ function upsertFileSymbols(index, file, mtime, symbols) {
84
+ const absFile = path.resolve(file);
85
+ const had = absFile in index.files;
86
+ index.symbols = index.symbols.filter((s) => s.location.file !== absFile);
87
+ index.symbols.push(...symbols);
88
+ index.files[absFile] = mtime;
89
+ index.symbolCount = index.symbols.length;
90
+ if (!had) index.fileCount = Object.keys(index.files).length;
91
+ index.generated = (/* @__PURE__ */ new Date()).toISOString();
92
+ return index;
93
+ }
94
+ function removeFile(index, file) {
95
+ const absFile = path.resolve(file);
96
+ if (!(absFile in index.files)) return index;
97
+ index.symbols = index.symbols.filter((s) => s.location.file !== absFile);
98
+ delete index.files[absFile];
99
+ index.symbolCount = index.symbols.length;
100
+ index.fileCount = Object.keys(index.files).length;
101
+ index.generated = (/* @__PURE__ */ new Date()).toISOString();
102
+ return index;
103
+ }
104
+
105
+ export {
106
+ detectLanguage,
107
+ loadIndex,
108
+ saveIndex,
109
+ clearIndex,
110
+ emptyIndex,
111
+ upsertFileSymbols,
112
+ removeFile
113
+ };
@@ -8,7 +8,7 @@ import {
8
8
  CONFIG_FILE_NAME,
9
9
  HISTORY_DIR_NAME,
10
10
  PLUGINS_DIR_NAME
11
- } from "./chunk-5P4QTZBI.js";
11
+ } from "./chunk-2Q77FT3F.js";
12
12
 
13
13
  // src/config/config-manager.ts
14
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TEST_TIMEOUT
4
- } from "./chunk-5P4QTZBI.js";
4
+ } from "./chunk-2Q77FT3F.js";
5
5
 
6
6
  // src/tools/builtin/run-tests.ts
7
7
  import { execSync } from "child_process";
@@ -2,9 +2,15 @@
2
2
  import {
3
3
  fileCheckpoints
4
4
  } from "./chunk-4BKXL7SM.js";
5
+ import {
6
+ indexProject
7
+ } from "./chunk-NHNWUBXB.js";
8
+ import {
9
+ loadIndex
10
+ } from "./chunk-6VRJGH25.js";
5
11
  import {
6
12
  runTestsTool
7
- } from "./chunk-3BHGEPIT.js";
13
+ } from "./chunk-K3JJX2Z5.js";
8
14
  import {
9
15
  EnvLoader,
10
16
  NetworkError,
@@ -17,7 +23,7 @@ import {
17
23
  SUBAGENT_ALLOWED_TOOLS,
18
24
  SUBAGENT_DEFAULT_MAX_ROUNDS,
19
25
  SUBAGENT_MAX_ROUNDS_LIMIT
20
- } from "./chunk-5P4QTZBI.js";
26
+ } from "./chunk-2Q77FT3F.js";
21
27
 
22
28
  // src/tools/builtin/bash.ts
23
29
  import { execSync } from "child_process";
@@ -1028,8 +1034,8 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
1028
1034
  if (rule.when) {
1029
1035
  if (rule.when.dangerLevel && rule.when.dangerLevel !== dangerLevel) continue;
1030
1036
  if (rule.when.pathPattern) {
1031
- const path = String(args["path"] ?? args["command"] ?? "");
1032
- if (!path.includes(rule.when.pathPattern)) continue;
1037
+ const path3 = String(args["path"] ?? args["command"] ?? "");
1038
+ if (!path3.includes(rule.when.pathPattern)) continue;
1033
1039
  }
1034
1040
  }
1035
1041
  return rule.action;
@@ -1729,6 +1735,13 @@ Important: For long content (over 500 lines or 3000 chars), you MUST split into
1729
1735
  }
1730
1736
  const lines = content.split("\n").length;
1731
1737
  const mode = appendMode ? "appended" : "written";
1738
+ void (async () => {
1739
+ try {
1740
+ const { updateFile } = await import("./indexer-C7QYYHSZ.js");
1741
+ await updateFile(process.cwd(), filePath);
1742
+ } catch {
1743
+ }
1744
+ })();
1732
1745
  return `File ${mode}: ${filePath} (${lines} lines, ${content.length} bytes)`;
1733
1746
  }
1734
1747
  };
@@ -4286,6 +4299,220 @@ var notebookEditTool = {
4286
4299
  }
4287
4300
  };
4288
4301
 
4302
+ // src/tools/builtin/symbol-tools.ts
4303
+ import path2 from "path";
4304
+
4305
+ // src/symbols/queries.ts
4306
+ import path from "path";
4307
+ import fs from "fs";
4308
+ function findSymbol(index, opts) {
4309
+ const name = opts.name.toLowerCase();
4310
+ const exact = opts.exact !== false;
4311
+ const kinds = opts.kind ? new Set(Array.isArray(opts.kind) ? opts.kind : [opts.kind]) : null;
4312
+ const limit = opts.limit ?? 50;
4313
+ const out = [];
4314
+ for (const s of index.symbols) {
4315
+ if (kinds && !kinds.has(s.kind)) continue;
4316
+ const lower = s.name.toLowerCase();
4317
+ if (exact ? lower === name : lower.includes(name)) {
4318
+ out.push(s);
4319
+ if (out.length >= limit) break;
4320
+ }
4321
+ }
4322
+ return out;
4323
+ }
4324
+ function getOutline(index, file) {
4325
+ const abs = path.resolve(file);
4326
+ return index.symbols.filter((s) => s.location.file === abs).sort((a, b) => a.location.line - b.location.line).map((s) => ({
4327
+ name: s.name,
4328
+ kind: s.kind,
4329
+ line: s.location.line,
4330
+ signature: s.signature,
4331
+ container: s.container,
4332
+ exported: s.exported
4333
+ }));
4334
+ }
4335
+ function findReferences(index, name, opts = {}) {
4336
+ const maxFiles = opts.maxFiles ?? 2e3;
4337
+ const maxHits = opts.maxHits ?? 500;
4338
+ const re = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\b`);
4339
+ const hits = [];
4340
+ const files = Object.keys(index.files).slice(0, maxFiles);
4341
+ for (const file of files) {
4342
+ let content;
4343
+ try {
4344
+ content = fs.readFileSync(file, "utf-8");
4345
+ } catch {
4346
+ continue;
4347
+ }
4348
+ if (!re.test(content)) continue;
4349
+ const lines = content.split(/\r?\n/);
4350
+ for (let i = 0; i < lines.length; i++) {
4351
+ const m = re.exec(lines[i]);
4352
+ if (m) {
4353
+ hits.push({
4354
+ file,
4355
+ line: i + 1,
4356
+ column: m.index,
4357
+ text: lines[i].trim().slice(0, 200)
4358
+ });
4359
+ if (hits.length >= maxHits) return hits;
4360
+ }
4361
+ }
4362
+ }
4363
+ return hits;
4364
+ }
4365
+
4366
+ // src/tools/builtin/symbol-tools.ts
4367
+ var NO_INDEX_HINT = "No symbol index exists for this project yet. Ask the user to run `/index rebuild`, or call indexProject() first. Alternatively, use grep_files for a slower fallback.";
4368
+ async function ensureIndex(root, autoBuild) {
4369
+ let index = loadIndex(root);
4370
+ if (!index && autoBuild) {
4371
+ const result = await indexProject(root);
4372
+ index = result.index;
4373
+ }
4374
+ return index;
4375
+ }
4376
+ var findSymbolTool = {
4377
+ definition: {
4378
+ name: "find_symbol",
4379
+ description: "Locate symbol definitions (functions, classes, methods, interfaces, types, variables) across the project using the pre-built symbol index. Orders of magnitude faster than grep_files for definition lookups. Returns file:line locations plus a one-line signature.",
4380
+ parameters: {
4381
+ name: {
4382
+ type: "string",
4383
+ description: 'Symbol name to find (e.g. "handleChat", "ProviderRegistry")',
4384
+ required: true
4385
+ },
4386
+ exact: {
4387
+ type: "boolean",
4388
+ description: "Exact match (default true). When false, substring match (case-insensitive).",
4389
+ required: false
4390
+ },
4391
+ kind: {
4392
+ type: "string",
4393
+ description: "Restrict to one kind: function, method, class, interface, type, enum, variable, property. Omit to search all.",
4394
+ required: false
4395
+ },
4396
+ path: {
4397
+ type: "string",
4398
+ description: "Project root (defaults to current working directory).",
4399
+ required: false
4400
+ },
4401
+ limit: {
4402
+ type: "number",
4403
+ description: "Max results (default 50).",
4404
+ required: false
4405
+ }
4406
+ }
4407
+ },
4408
+ async execute(args) {
4409
+ const name = String(args.name ?? "").trim();
4410
+ if (!name) return "Error: `name` is required.";
4411
+ const root = path2.resolve(String(args.path ?? process.cwd()));
4412
+ const index = await ensureIndex(root, true);
4413
+ if (!index) return NO_INDEX_HINT;
4414
+ const kindArg = args.kind ? String(args.kind) : void 0;
4415
+ const hits = findSymbol(index, {
4416
+ name,
4417
+ exact: args.exact !== false,
4418
+ kind: kindArg,
4419
+ limit: typeof args.limit === "number" ? args.limit : 50
4420
+ });
4421
+ if (hits.length === 0) {
4422
+ return `No symbol matching "${name}" found (index has ${index.symbolCount} symbols across ${index.fileCount} files).`;
4423
+ }
4424
+ const lines = hits.map((s) => {
4425
+ const rel = path2.relative(index.root, s.location.file) || s.location.file;
4426
+ const container = s.container ? ` (in ${s.container})` : "";
4427
+ const exp = s.exported ? " [exported]" : "";
4428
+ const sig = s.signature ? `
4429
+ ${s.signature}` : "";
4430
+ return `${s.kind} ${s.name}${container}${exp} \u2014 ${rel}:${s.location.line}${sig}`;
4431
+ });
4432
+ return `Found ${hits.length} symbol(s):
4433
+ ${lines.join("\n")}`;
4434
+ }
4435
+ };
4436
+ var getOutlineTool = {
4437
+ definition: {
4438
+ name: "get_outline",
4439
+ description: "Return the complete list of top-level declarations in a single source file (classes, functions, methods, types), sorted by line number. Use this to understand file structure without reading its full contents.",
4440
+ parameters: {
4441
+ file: {
4442
+ type: "string",
4443
+ description: "Path to the source file (absolute, or relative to project root).",
4444
+ required: true
4445
+ },
4446
+ path: {
4447
+ type: "string",
4448
+ description: "Project root (defaults to current working directory).",
4449
+ required: false
4450
+ }
4451
+ }
4452
+ },
4453
+ async execute(args) {
4454
+ const file = String(args.file ?? "").trim();
4455
+ if (!file) return "Error: `file` is required.";
4456
+ const root = path2.resolve(String(args.path ?? process.cwd()));
4457
+ const absFile = path2.isAbsolute(file) ? file : path2.resolve(root, file);
4458
+ const index = await ensureIndex(root, true);
4459
+ if (!index) return NO_INDEX_HINT;
4460
+ const outline = getOutline(index, absFile);
4461
+ if (outline.length === 0) {
4462
+ return `No indexed symbols found for ${path2.relative(root, absFile) || absFile}. The file may be of an unsupported language (supported: TS/JS/TSX/JSX/Python), unreadable, or not yet re-indexed.`;
4463
+ }
4464
+ const rel = path2.relative(root, absFile) || absFile;
4465
+ const lines = outline.map((o) => {
4466
+ const container = o.container ? ` (in ${o.container})` : "";
4467
+ const exp = o.exported ? " [exported]" : "";
4468
+ return ` ${String(o.line).padStart(5)}: ${o.kind} ${o.name}${container}${exp}`;
4469
+ });
4470
+ return `Outline of ${rel} (${outline.length} symbols):
4471
+ ${lines.join("\n")}`;
4472
+ }
4473
+ };
4474
+ var findReferencesTool = {
4475
+ definition: {
4476
+ name: "find_references",
4477
+ description: "Find all textual references to a symbol name across indexed files (whole-word match). MVP: regex-based \u2014 may include unrelated identifiers with the same name. Pair with find_symbol to locate the definition.",
4478
+ parameters: {
4479
+ name: {
4480
+ type: "string",
4481
+ description: "Symbol name to find references for.",
4482
+ required: true
4483
+ },
4484
+ path: {
4485
+ type: "string",
4486
+ description: "Project root (defaults to current working directory).",
4487
+ required: false
4488
+ },
4489
+ max_hits: {
4490
+ type: "number",
4491
+ description: "Max reference hits to return (default 200).",
4492
+ required: false
4493
+ }
4494
+ }
4495
+ },
4496
+ async execute(args) {
4497
+ const name = String(args.name ?? "").trim();
4498
+ if (!name) return "Error: `name` is required.";
4499
+ const root = path2.resolve(String(args.path ?? process.cwd()));
4500
+ const index = await ensureIndex(root, true);
4501
+ if (!index) return NO_INDEX_HINT;
4502
+ const maxHits = typeof args.max_hits === "number" ? args.max_hits : 200;
4503
+ const hits = findReferences(index, name, { maxHits });
4504
+ if (hits.length === 0) {
4505
+ return `No references to "${name}" found in ${index.fileCount} indexed files.`;
4506
+ }
4507
+ const lines = hits.map((h) => {
4508
+ const rel = path2.relative(index.root, h.file) || h.file;
4509
+ return `${rel}:${h.line}:${h.column} ${h.text}`;
4510
+ });
4511
+ return `Found ${hits.length} reference(s) to "${name}":
4512
+ ${lines.join("\n")}`;
4513
+ }
4514
+ };
4515
+
4289
4516
  // src/core/token-estimator.ts
4290
4517
  var CJK_REGEX = /[\u2E80-\u9FFF\uA000-\uA4FF\uAC00-\uD7FF\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF]/g;
4291
4518
  function estimateTokens(text) {
@@ -4341,6 +4568,9 @@ var ToolRegistry = class {
4341
4568
  this.register(gitLogTool);
4342
4569
  this.register(gitCommitTool);
4343
4570
  this.register(notebookEditTool);
4571
+ this.register(findSymbolTool);
4572
+ this.register(getOutlineTool);
4573
+ this.register(findReferencesTool);
4344
4574
  }
4345
4575
  register(tool) {
4346
4576
  this.tools.set(tool.definition.name, tool);