micro-models-agent 0.7.10 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/cli/commands.js +173 -0
  2. package/dist/cli/completer.js +168 -0
  3. package/dist/cli/index.js +2 -0
  4. package/dist/cli/main.js +95 -0
  5. package/dist/cli/repl.js +762 -0
  6. package/dist/cli/security-commands.js +166 -0
  7. package/dist/cli/setup.js +214 -0
  8. package/dist/config/config.js +186 -0
  9. package/dist/config/defaults.js +91 -0
  10. package/dist/config/experts.js +15 -0
  11. package/dist/config/index.js +3 -0
  12. package/dist/config/security.js +187 -0
  13. package/dist/config/types.js +1 -0
  14. package/dist/core/agent.js +626 -0
  15. package/dist/core/bootstrap.js +317 -0
  16. package/dist/core/index.js +2 -0
  17. package/dist/core/prompt-builder.js +55 -0
  18. package/dist/core/types.js +1 -0
  19. package/dist/i18n/en.json +405 -0
  20. package/dist/i18n/index.js +43 -0
  21. package/dist/i18n/ru.json +405 -0
  22. package/dist/index.js +22 -0
  23. package/dist/llm/index.js +4 -0
  24. package/dist/llm/model-loader.js +78 -0
  25. package/dist/llm/openai-compat.js +277 -0
  26. package/dist/llm/orchestrator.js +194 -0
  27. package/dist/llm/provider.js +2 -0
  28. package/dist/llm/response.js +39 -0
  29. package/dist/llm/token-counter.js +37 -0
  30. package/dist/llm/types.js +1 -0
  31. package/dist/logger/app-logger.js +76 -0
  32. package/dist/logger/index.js +1 -0
  33. package/dist/main.js +15904 -0
  34. package/dist/migration/backup.js +45 -0
  35. package/dist/migration/detect.js +50 -0
  36. package/dist/migration/index.js +2 -0
  37. package/dist/modules/browser/actions.js +46 -0
  38. package/dist/modules/browser/cookie-store.js +24 -0
  39. package/dist/modules/browser/index.js +5 -0
  40. package/dist/modules/browser/module.js +28 -0
  41. package/dist/modules/browser/session.js +287 -0
  42. package/dist/modules/browser/snapshot.js +114 -0
  43. package/dist/modules/browser/types.js +9 -0
  44. package/dist/modules/context/history.js +15 -0
  45. package/dist/modules/context/index.js +1 -0
  46. package/dist/modules/context/manager.js +179 -0
  47. package/dist/modules/execution/auditor.js +72 -0
  48. package/dist/modules/execution/index.js +6 -0
  49. package/dist/modules/execution/module.js +334 -0
  50. package/dist/modules/execution/moe-executor.js +196 -0
  51. package/dist/modules/execution/plan-validator.js +153 -0
  52. package/dist/modules/execution/planner.js +35 -0
  53. package/dist/modules/execution/stuck-detector.js +113 -0
  54. package/dist/modules/execution/tracker.js +53 -0
  55. package/dist/modules/execution/types.js +1 -0
  56. package/dist/modules/execution/verifier.js +149 -0
  57. package/dist/modules/hallucination/confidence.js +47 -0
  58. package/dist/modules/hallucination/consistency.js +32 -0
  59. package/dist/modules/hallucination/detector.js +41 -0
  60. package/dist/modules/hallucination/factual.js +128 -0
  61. package/dist/modules/hallucination/index.js +4 -0
  62. package/dist/modules/index.js +5 -0
  63. package/dist/modules/indexer/cache.js +38 -0
  64. package/dist/modules/indexer/index.js +3 -0
  65. package/dist/modules/indexer/module.js +192 -0
  66. package/dist/modules/indexer/walker.js +101 -0
  67. package/dist/modules/mcp/client.js +393 -0
  68. package/dist/modules/mcp/index.js +3 -0
  69. package/dist/modules/mcp/module.js +146 -0
  70. package/dist/modules/mcp/registry.js +15 -0
  71. package/dist/modules/memory/index.js +1 -0
  72. package/dist/modules/memory/search.js +26 -0
  73. package/dist/modules/memory/store.js +38 -0
  74. package/dist/modules/pipelines/engine.js +60 -0
  75. package/dist/modules/pipelines/index.js +3 -0
  76. package/dist/modules/pipelines/parser.js +53 -0
  77. package/dist/modules/pipelines/template.js +14 -0
  78. package/dist/modules/plugins/builtin/lint-on-write.js +121 -0
  79. package/dist/modules/plugins/builtin/notify.js +8 -0
  80. package/dist/modules/plugins/index.js +1 -0
  81. package/dist/modules/plugins/loader.js +28 -0
  82. package/dist/modules/plugins/manager.js +161 -0
  83. package/dist/modules/plugins/types.js +1 -0
  84. package/dist/modules/registry.js +45 -0
  85. package/dist/modules/security/audit-log.js +116 -0
  86. package/dist/modules/security/audit-notifier.js +292 -0
  87. package/dist/modules/security/command-validator.js +104 -0
  88. package/dist/modules/security/content-scanner.js +52 -0
  89. package/dist/modules/security/data-sanitizer.js +97 -0
  90. package/dist/modules/security/encryption.js +238 -0
  91. package/dist/modules/security/index.js +14 -0
  92. package/dist/modules/security/network-validator.js +79 -0
  93. package/dist/modules/security/path-validator.js +155 -0
  94. package/dist/modules/security/rate-limiter.js +119 -0
  95. package/dist/modules/security/security-policies.js +393 -0
  96. package/dist/modules/security/session-encryption.js +193 -0
  97. package/dist/modules/security/session-isolation.js +95 -0
  98. package/dist/modules/session/index.js +3 -0
  99. package/dist/modules/session/manager.js +167 -0
  100. package/dist/modules/session/module.js +28 -0
  101. package/dist/modules/session/store.js +174 -0
  102. package/dist/modules/session/types.js +1 -0
  103. package/dist/modules/skills/index.js +3 -0
  104. package/dist/modules/skills/loader.js +72 -0
  105. package/dist/modules/skills/matcher.js +27 -0
  106. package/dist/modules/skills/module.js +180 -0
  107. package/dist/modules/types.js +1 -0
  108. package/dist/modules/updater/checker.js +32 -0
  109. package/dist/modules/updater/index.js +1 -0
  110. package/dist/modules/user-profile/compressor.js +16 -0
  111. package/dist/modules/user-profile/index.js +1 -0
  112. package/dist/modules/user-profile/profile.js +68 -0
  113. package/dist/tools/approve.js +32 -0
  114. package/dist/tools/bash.js +80 -0
  115. package/dist/tools/browser.js +97 -0
  116. package/dist/tools/create-dir.js +57 -0
  117. package/dist/tools/delete-file.js +64 -0
  118. package/dist/tools/edit-file.js +78 -0
  119. package/dist/tools/executor.js +83 -0
  120. package/dist/tools/file-info.js +46 -0
  121. package/dist/tools/filter-tools.js +10 -0
  122. package/dist/tools/glob-tool.js +19 -0
  123. package/dist/tools/grep-tool.js +57 -0
  124. package/dist/tools/index.js +44 -0
  125. package/dist/tools/list-dir.js +40 -0
  126. package/dist/tools/load-skill.js +48 -0
  127. package/dist/tools/mcp-call.js +68 -0
  128. package/dist/tools/move-file.js +84 -0
  129. package/dist/tools/pipeline-run.js +144 -0
  130. package/dist/tools/question.js +142 -0
  131. package/dist/tools/read-file.js +70 -0
  132. package/dist/tools/registry.js +36 -0
  133. package/dist/tools/scope-check.js +30 -0
  134. package/dist/tools/search-history.js +64 -0
  135. package/dist/tools/subagent.js +142 -0
  136. package/dist/tools/types.js +1 -0
  137. package/dist/tools/user-input.js +123 -0
  138. package/dist/tools/web-browse.js +51 -0
  139. package/dist/tools/web-fetch.js +62 -0
  140. package/dist/tools/web-search.js +59 -0
  141. package/dist/tools/write-file.js +80 -0
  142. package/dist/ui/box.js +81 -0
  143. package/dist/ui/colors.js +4 -0
  144. package/dist/ui/diff.js +185 -0
  145. package/dist/ui/index.js +6 -0
  146. package/dist/ui/md-formatter.js +212 -0
  147. package/dist/ui/output.js +13 -0
  148. package/dist/ui/renderer.js +141 -0
  149. package/dist/ui/spinner.js +70 -0
  150. package/dist/ui/table.js +144 -0
  151. package/package.json +1 -1
@@ -0,0 +1,149 @@
1
+ import { existsSync } from 'fs';
2
+ import { resolve, extname } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { t } from '../../i18n/index';
5
+ import { validateExpertConfig } from '../../config/experts';
6
+ export class StepVerifier {
7
+ baseDir;
8
+ constructor(baseDir) {
9
+ this.baseDir = baseDir;
10
+ }
11
+ async checkFileExists(path) {
12
+ const resolved = resolve(this.baseDir, path);
13
+ const exists = existsSync(resolved);
14
+ return {
15
+ passed: exists,
16
+ message: exists ? t('verify.file_exists', { path }) : t('verify.file_not_found', { path }),
17
+ };
18
+ }
19
+ async runScript(scriptName) {
20
+ try {
21
+ execSync(`bun run ${scriptName}`, {
22
+ cwd: this.baseDir,
23
+ encoding: 'utf-8',
24
+ timeout: 60_000,
25
+ stdio: 'pipe',
26
+ });
27
+ return { passed: true, message: t('verify.script_passed', { script: scriptName }) };
28
+ }
29
+ catch (e) {
30
+ return { passed: false, message: t('verify.script_failed', { script: scriptName, message: e.message }) };
31
+ }
32
+ }
33
+ async runTypeCheck() {
34
+ const tsconfigPath = resolve(this.baseDir, 'tsconfig.json');
35
+ if (!existsSync(tsconfigPath)) {
36
+ return { passed: true, message: 'No tsconfig.json found — skipping type check' };
37
+ }
38
+ try {
39
+ execSync('npx tsc --noEmit', {
40
+ cwd: this.baseDir,
41
+ encoding: 'utf-8',
42
+ timeout: 60_000,
43
+ stdio: 'pipe',
44
+ });
45
+ return { passed: true, message: 'TypeScript type check passed' };
46
+ }
47
+ catch (e) {
48
+ const stderr = e.stderr?.toString() || e.stdout?.toString() || e.message;
49
+ return { passed: false, message: `TypeScript type check failed: ${stderr.slice(0, 500)}` };
50
+ }
51
+ }
52
+ async runTests() {
53
+ const pkgPath = resolve(this.baseDir, 'package.json');
54
+ if (!existsSync(pkgPath)) {
55
+ return { passed: true, message: 'No package.json found — skipping tests' };
56
+ }
57
+ try {
58
+ const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf-8'));
59
+ if (!pkg.scripts?.test) {
60
+ return { passed: true, message: 'No test script defined — skipping tests' };
61
+ }
62
+ }
63
+ catch { /* fall through — attempt to run */ }
64
+ return this.runScript('test');
65
+ }
66
+ async verifyArtifactFiles(files) {
67
+ return Promise.all(files.map(f => this.checkFileExists(f)));
68
+ }
69
+ async verifyMoEManifest(plan, config, allToolTags) {
70
+ const errors = [];
71
+ const warnings = [];
72
+ const details = [];
73
+ const expertErrors = validateExpertConfig(config, allToolTags);
74
+ for (const err of expertErrors) {
75
+ errors.push(err);
76
+ details.push({ passed: false, message: err });
77
+ }
78
+ const tscResult = await this.runTypeCheck();
79
+ details.push(tscResult);
80
+ if (!tscResult.passed) {
81
+ errors.push(tscResult.message);
82
+ }
83
+ for (const sub of plan.subtasks) {
84
+ const fileChecks = await this.verifyArtifactFiles(sub.allowed_files || []);
85
+ for (const check of fileChecks) {
86
+ details.push(check);
87
+ if (!check.passed) {
88
+ errors.push(check.message);
89
+ }
90
+ }
91
+ }
92
+ return {
93
+ success: errors.length === 0,
94
+ errors,
95
+ warnings,
96
+ details,
97
+ };
98
+ }
99
+ async verifyStep(stepDescription) {
100
+ const fileMatches = stepDescription.match(/\b[\w./-]+\.[a-z]+/gi) || [];
101
+ const results = [];
102
+ let syntaxValid = true;
103
+ for (const filePath of fileMatches) {
104
+ const result = await this.checkFileExists(filePath);
105
+ results.push(result);
106
+ if (result.passed) {
107
+ const fullPath = resolve(this.baseDir, filePath);
108
+ if (!this.validateSyntax(fullPath)) {
109
+ syntaxValid = false;
110
+ results.push({ passed: false, message: t('verify.syntax_error', { path: filePath }) });
111
+ }
112
+ }
113
+ }
114
+ return {
115
+ passed: results.every(r => r.passed),
116
+ syntaxValid,
117
+ failed: results.filter(r => !r.passed),
118
+ };
119
+ }
120
+ validateSyntax(filePath) {
121
+ const ext = extname(filePath);
122
+ if (ext === '.ts' || ext === '.tsx') {
123
+ try {
124
+ execSync(`npx tsc --noEmit --skipLibCheck ${filePath}`, { stdio: 'pipe', timeout: 10000 });
125
+ return true;
126
+ }
127
+ catch (err) {
128
+ if (err.status === 127 || err.message.includes('not found') || err.message.includes('ENOENT')) {
129
+ return true;
130
+ }
131
+ const stderr = err.stderr?.toString() || '';
132
+ if (stderr.includes('error TS') && !stderr.includes('Cannot find module')) {
133
+ return false;
134
+ }
135
+ return true;
136
+ }
137
+ }
138
+ if (ext === '.js' || ext === '.jsx') {
139
+ try {
140
+ execSync(`node --check ${filePath}`, { stdio: 'pipe', timeout: 5000 });
141
+ return true;
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
147
+ return true;
148
+ }
149
+ }
@@ -0,0 +1,47 @@
1
+ import { t } from "../../i18n/index";
2
+ const UNCERTAINTY_MARKERS = [
3
+ "i think",
4
+ "maybe",
5
+ "probably",
6
+ "i believe",
7
+ "not sure",
8
+ "might be",
9
+ ];
10
+ const MIN_CHARS = 1;
11
+ export class ConfidenceCheck {
12
+ previousResponse = "";
13
+ setPreviousResponse(response) {
14
+ this.previousResponse = response;
15
+ }
16
+ validate(response) {
17
+ if (!response || response.length < MIN_CHARS) {
18
+ return { status: "retry", reason: t("hall.short_response") };
19
+ }
20
+ if (this.previousResponse) {
21
+ const overlap = this.calculateOverlap(response, this.previousResponse);
22
+ if (overlap > 0.5) {
23
+ return {
24
+ status: "retry",
25
+ reason: t("hall.repetitive", { pct: Math.round(overlap * 100) }),
26
+ };
27
+ }
28
+ }
29
+ const lower = response.toLowerCase();
30
+ const foundMarkers = UNCERTAINTY_MARKERS.filter((m) => lower.includes(m));
31
+ if (foundMarkers.length > 0) {
32
+ return {
33
+ status: "warn",
34
+ reason: t("hall.uncertainty", { markers: foundMarkers.join(", ") }),
35
+ };
36
+ }
37
+ return { status: "pass" };
38
+ }
39
+ calculateOverlap(a, b) {
40
+ const wordsA = new Set(a.toLowerCase().split(/\s+/));
41
+ const wordsB = b.toLowerCase().split(/\s+/);
42
+ if (wordsB.length === 0)
43
+ return 0;
44
+ const matches = wordsB.filter((w) => wordsA.has(w));
45
+ return matches.length / wordsB.length;
46
+ }
47
+ }
@@ -0,0 +1,32 @@
1
+ import { t } from '../../i18n/index';
2
+ export class ConsistencyCheck {
3
+ decisions = [];
4
+ createdFiles = new Set();
5
+ deletedFiles = new Set();
6
+ trackDecision(decision, location) {
7
+ this.decisions.push({ decision, location });
8
+ }
9
+ trackCreatedFile(path) {
10
+ this.createdFiles.add(path);
11
+ }
12
+ trackDeletedFile(path) {
13
+ this.deletedFiles.add(path);
14
+ }
15
+ getCreatedFiles() {
16
+ return Array.from(this.createdFiles);
17
+ }
18
+ validate(response) {
19
+ const lower = response.toLowerCase();
20
+ for (const d of this.decisions) {
21
+ const decisionWords = d.decision.toLowerCase().split(/\s+/).filter(w => w.length > 3);
22
+ const contradicts = decisionWords.some(word => lower.includes(`instead of ${word}`) || lower.includes(`not ${word}`));
23
+ if (contradicts) {
24
+ return {
25
+ status: 'warn',
26
+ reason: t('hall.contradiction', { decision: d.decision, location: d.location }),
27
+ };
28
+ }
29
+ }
30
+ return { status: 'pass' };
31
+ }
32
+ }
@@ -0,0 +1,41 @@
1
+ import { FactualCheck } from './factual';
2
+ import { ConsistencyCheck } from './consistency';
3
+ import { ConfidenceCheck } from './confidence';
4
+ export class HallucinationDetector {
5
+ factual;
6
+ consistency;
7
+ confidence;
8
+ constructor() {
9
+ this.factual = new FactualCheck();
10
+ this.consistency = new ConsistencyCheck();
11
+ this.confidence = new ConfidenceCheck();
12
+ }
13
+ getFactualCheck() {
14
+ return this.factual;
15
+ }
16
+ getConsistencyCheck() {
17
+ return this.consistency;
18
+ }
19
+ getConfidenceCheck() {
20
+ return this.confidence;
21
+ }
22
+ validate(response) {
23
+ const confidenceResult = this.confidence.validate(response);
24
+ if (confidenceResult.status === 'retry' || confidenceResult.status === 'block') {
25
+ return confidenceResult;
26
+ }
27
+ const factualResult = this.factual.validate(response);
28
+ const consistencyResult = this.consistency.validate(response);
29
+ const warnings = [];
30
+ if (factualResult.status === 'warn')
31
+ warnings.push(factualResult.reason || '');
32
+ if (consistencyResult.status === 'warn')
33
+ warnings.push(consistencyResult.reason || '');
34
+ if (confidenceResult.status === 'warn')
35
+ warnings.push(confidenceResult.reason || '');
36
+ if (warnings.length > 0) {
37
+ return { status: 'warn', reason: warnings.join('; ') };
38
+ }
39
+ return { status: 'pass' };
40
+ }
41
+ }
@@ -0,0 +1,128 @@
1
+ import { t } from "../../i18n/index";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ const FILE_EXTENSIONS = new Set([
5
+ "ts",
6
+ "tsx",
7
+ "js",
8
+ "jsx",
9
+ "json",
10
+ "md",
11
+ "yaml",
12
+ "yml",
13
+ "py",
14
+ "rs",
15
+ "go",
16
+ "java",
17
+ "c",
18
+ "cpp",
19
+ "h",
20
+ "hpp",
21
+ "html",
22
+ "css",
23
+ "scss",
24
+ "less",
25
+ "vue",
26
+ "svelte",
27
+ "sh",
28
+ "bash",
29
+ "zsh",
30
+ "ps1",
31
+ "bat",
32
+ "cmd",
33
+ "txt",
34
+ "env",
35
+ "gitignore",
36
+ "dockerignore",
37
+ "dockerfile",
38
+ "makefile",
39
+ "cmake",
40
+ "toml",
41
+ "xml",
42
+ "sql",
43
+ "graphql",
44
+ "proto",
45
+ "wasm",
46
+ ]);
47
+ const VERSION_PATTERN = /^\d+(\.\d+)*$/;
48
+ const COMMON_WORDS = new Set(["node.js", "Node.js"]);
49
+ export class FactualCheck {
50
+ knownPaths = new Set();
51
+ createdPaths = new Set();
52
+ baseDir = process.cwd();
53
+ setBaseDir(dir) {
54
+ this.baseDir = dir;
55
+ }
56
+ trackReadPath(path) {
57
+ this.knownPaths.add(path);
58
+ const base = path.split(/[/\\]/).pop();
59
+ if (base && base !== path)
60
+ this.knownPaths.add(base);
61
+ }
62
+ trackCreatedPath(path) {
63
+ this.knownPaths.add(path);
64
+ const base = path.split(/[/\\]/).pop();
65
+ if (base && base !== path)
66
+ this.knownPaths.add(base);
67
+ this.createdPaths.add(path);
68
+ }
69
+ trackDeletedPath(path) {
70
+ this.knownPaths.add(path);
71
+ const base = path.split(/[/\\]/).pop();
72
+ if (base && base !== path)
73
+ this.knownPaths.add(base);
74
+ this.createdPaths.delete(path);
75
+ }
76
+ pathExistsOnDisk(path) {
77
+ try {
78
+ if (path.startsWith("/") ||
79
+ path.startsWith("~") ||
80
+ /^[A-Za-z]:/.test(path)) {
81
+ return existsSync(path);
82
+ }
83
+ return existsSync(join(this.baseDir, path));
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ validate(response) {
90
+ const pathRegex = /[\w\-./]+\.\w+/g;
91
+ const mentionedPaths = response.match(pathRegex) || [];
92
+ const unknownPaths = mentionedPaths.filter((p) => {
93
+ if (this.knownPaths.has(p))
94
+ return false;
95
+ if (VERSION_PATTERN.test(p))
96
+ return false;
97
+ if (p.startsWith("/"))
98
+ return false;
99
+ if (COMMON_WORDS.has(p))
100
+ return false;
101
+ if (this.pathExistsOnDisk(p))
102
+ return false;
103
+ const ext = p.split(".").pop()?.toLowerCase() || "";
104
+ if (!FILE_EXTENSIONS.has(ext))
105
+ return false;
106
+ const basename = p
107
+ .split("/")
108
+ .pop()
109
+ ?.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
110
+ if (basename) {
111
+ const urlPattern = new RegExp(`(https?|file)://[^\\s]*${basename}`);
112
+ if (urlPattern.test(response))
113
+ return false;
114
+ }
115
+ return true;
116
+ });
117
+ if (unknownPaths.length > 0) {
118
+ const uniquePaths = [...new Set(unknownPaths)];
119
+ return {
120
+ status: "warn",
121
+ reason: t("hall.unknown_paths", {
122
+ paths: uniquePaths.slice(0, 3).join(", "),
123
+ }),
124
+ };
125
+ }
126
+ return { status: "pass" };
127
+ }
128
+ }
@@ -0,0 +1,4 @@
1
+ export { HallucinationDetector } from './detector';
2
+ export { FactualCheck } from './factual';
3
+ export { ConsistencyCheck } from './consistency';
4
+ export { ConfidenceCheck } from './confidence';
@@ -0,0 +1,5 @@
1
+ export { ModuleRegistry } from './registry';
2
+ export { SkillsLoader, SkillsMatcher, SkillsModule } from './skills';
3
+ export { PluginManager } from './plugins';
4
+ export { PluginLoader } from './plugins/loader';
5
+ export { MCPClient, MCPRegistry } from './mcp';
@@ -0,0 +1,38 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
2
+ import { join } from 'path';
3
+ export class IndexCache {
4
+ cachePath;
5
+ cache = null;
6
+ constructor(cacheDir) {
7
+ this.cachePath = join(cacheDir, 'index-cache.json');
8
+ }
9
+ load() {
10
+ if (this.cache)
11
+ return this.cache;
12
+ if (!existsSync(this.cachePath))
13
+ return null;
14
+ try {
15
+ this.cache = JSON.parse(readFileSync(this.cachePath, 'utf-8'));
16
+ return this.cache;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ save(result) {
23
+ this.cache = result;
24
+ const dir = join(this.cachePath, '..');
25
+ if (!existsSync(dir))
26
+ mkdirSync(dir, { recursive: true });
27
+ writeFileSync(this.cachePath, JSON.stringify(result), 'utf-8');
28
+ }
29
+ invalidate() {
30
+ this.cache = null;
31
+ if (existsSync(this.cachePath)) {
32
+ try {
33
+ rmSync(this.cachePath);
34
+ }
35
+ catch { /* ignore */ }
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,3 @@
1
+ export { Indexer } from './walker';
2
+ export { IndexCache } from './cache';
3
+ export { IndexerModule } from './module';
@@ -0,0 +1,192 @@
1
+ import { dirname } from 'path';
2
+ import { Indexer } from './walker';
3
+ import { IndexCache } from './cache';
4
+ import { t } from '../../i18n/index';
5
+ export class IndexerModule {
6
+ name = 'indexer';
7
+ indexer;
8
+ cache;
9
+ baseDir;
10
+ index = null;
11
+ rebuildTimeout = null;
12
+ constructor(opts) {
13
+ this.baseDir = opts.baseDir;
14
+ this.indexer = new Indexer(opts.baseDir);
15
+ this.cache = new IndexCache(opts.cacheDir);
16
+ }
17
+ async buildIndex() {
18
+ if (this.index)
19
+ return this.index;
20
+ const cached = this.cache.load();
21
+ if (cached) {
22
+ this.index = cached;
23
+ return cached;
24
+ }
25
+ try {
26
+ const result = await this.indexer.walk();
27
+ this.cache.save(result);
28
+ this.index = result;
29
+ return result;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ async refresh() {
36
+ this.cache.invalidate();
37
+ this.index = null;
38
+ return this.buildIndex();
39
+ }
40
+ find(query) {
41
+ if (!this.index)
42
+ return [];
43
+ const q = query.toLowerCase();
44
+ return this.index.files.filter((f) => {
45
+ const path = f.path.toLowerCase();
46
+ return path.includes(q) || f.exports.some((e) => e.toLowerCase().includes(q));
47
+ });
48
+ }
49
+ watch() {
50
+ this.unwatch();
51
+ try {
52
+ this.indexer.watch((_event, filename) => {
53
+ if (!filename)
54
+ return;
55
+ if (this.rebuildTimeout)
56
+ clearTimeout(this.rebuildTimeout);
57
+ this.rebuildTimeout = setTimeout(() => {
58
+ this.refresh().catch(() => { });
59
+ }, 2000);
60
+ });
61
+ }
62
+ catch {
63
+ // Watching is best-effort; manual refresh still works.
64
+ }
65
+ }
66
+ unwatch() {
67
+ if (this.rebuildTimeout) {
68
+ clearTimeout(this.rebuildTimeout);
69
+ this.rebuildTimeout = null;
70
+ }
71
+ this.indexer.unwatch();
72
+ }
73
+ getSystemPromptBlock() {
74
+ if (!this.index)
75
+ return null;
76
+ const content = this.formatMap(this.index);
77
+ return {
78
+ content,
79
+ priority: 'normal',
80
+ essential: false,
81
+ estimatedTokens: this.estimateTokens(content),
82
+ };
83
+ }
84
+ getToolDefinitions() {
85
+ return [this.createProjectMapTool()];
86
+ }
87
+ getPlugin() {
88
+ return {
89
+ name: 'indexer',
90
+ onSessionStart: () => {
91
+ this.buildIndex().catch(() => { });
92
+ this.watch();
93
+ },
94
+ onSessionEnd: () => {
95
+ this.unwatch();
96
+ },
97
+ };
98
+ }
99
+ formatMap(result) {
100
+ const summary = this.indexer.summarize(result);
101
+ const dirCounts = this.getDirectoryCounts(result);
102
+ const topDirs = Object.entries(dirCounts)
103
+ .sort((a, b) => b[1] - a[1])
104
+ .slice(0, 10)
105
+ .map(([dir, count]) => `${dir} (${count})`)
106
+ .join(', ') || '-';
107
+ const fileLines = result.files.slice(0, 100).map((f) => {
108
+ const path = f.path.replace(/\\/g, '/');
109
+ const exports = f.exports.length > 0 ? `: ${f.exports.join(', ')}` : '';
110
+ return `- ${path}${exports}`;
111
+ });
112
+ const more = result.files.length > 100
113
+ ? `\n${t('indexer.and_more', { count: result.files.length - 100 })}`
114
+ : '';
115
+ return [
116
+ `${t('indexer.map_header')} (${this.baseDir})`,
117
+ summary,
118
+ `${t('indexer.top_directories')}: ${topDirs}`,
119
+ `${t('indexer.files')}:` + (fileLines.length > 0 ? '' : ' ' + t('indexer.empty')),
120
+ ...fileLines,
121
+ more,
122
+ ].join('\n');
123
+ }
124
+ getDirectoryCounts(result) {
125
+ const counts = {};
126
+ for (const f of result.files) {
127
+ const normalized = f.path.replace(/\\/g, '/');
128
+ const dir = dirname(normalized);
129
+ const key = dir === '.' ? '(root)' : dir;
130
+ counts[key] = (counts[key] || 0) + 1;
131
+ }
132
+ return counts;
133
+ }
134
+ estimateTokens(content) {
135
+ return Math.ceil(content.length / 4);
136
+ }
137
+ createProjectMapTool() {
138
+ return {
139
+ name: 'project_map',
140
+ description: t('indexer.project_map_desc'),
141
+ parameters: {
142
+ type: 'object',
143
+ properties: {
144
+ action: {
145
+ type: 'string',
146
+ enum: ['summary', 'refresh', 'find'],
147
+ description: t('indexer.project_map_action_desc'),
148
+ },
149
+ query: {
150
+ type: 'string',
151
+ description: t('indexer.project_map_query_desc'),
152
+ },
153
+ },
154
+ required: ['action'],
155
+ },
156
+ handler: async (_ctx, args) => {
157
+ const action = String(args.action || 'summary');
158
+ if (action === 'refresh') {
159
+ const result = await this.refresh();
160
+ const summary = result ? this.indexer.summarize(result) : t('indexer.empty');
161
+ return this.makeResult(t('indexer.refreshed', { summary }));
162
+ }
163
+ if (action === 'find') {
164
+ const query = String(args.query || '').toLowerCase();
165
+ if (!query)
166
+ return { success: false, output: t('tool.invalid_params') };
167
+ const matches = this.find(query);
168
+ if (matches.length === 0) {
169
+ return this.makeResult(t('indexer.no_matches', { query }));
170
+ }
171
+ const lines = matches.map((f) => {
172
+ const path = f.path.replace(/\\/g, '/');
173
+ const exports = f.exports.length > 0 ? `: ${f.exports.join(', ')}` : '';
174
+ return `- ${path}${exports}`;
175
+ }).join('\n');
176
+ return this.makeResult(t('indexer.find_results', { count: matches.length, results: lines }));
177
+ }
178
+ if (!this.index) {
179
+ await this.buildIndex();
180
+ }
181
+ if (!this.index) {
182
+ return this.makeResult(t('indexer.not_indexed'));
183
+ }
184
+ const summary = this.indexer.summarize(this.index);
185
+ return this.makeResult(t('indexer.summary', { summary }));
186
+ },
187
+ };
188
+ }
189
+ makeResult(output) {
190
+ return { success: true, output, display: output };
191
+ }
192
+ }