micro-models-agent 0.47.0 → 0.48.1

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 (218) hide show
  1. package/README.md +358 -312
  2. package/dist/cli/commands.js +323 -0
  3. package/dist/cli/completer.js +167 -0
  4. package/dist/cli/index.js +2 -0
  5. package/dist/cli/main.js +165 -0
  6. package/dist/cli/plugin-commands.js +36 -0
  7. package/dist/cli/repl-commands.js +661 -0
  8. package/dist/cli/repl.js +616 -0
  9. package/dist/cli/run-result.js +22 -0
  10. package/dist/cli/security-commands.js +164 -0
  11. package/dist/cli/setup.js +231 -0
  12. package/dist/config/config.js +249 -0
  13. package/dist/config/defaults.js +124 -0
  14. package/dist/config/experts.js +15 -0
  15. package/dist/config/index.js +3 -0
  16. package/dist/config/security.js +193 -0
  17. package/dist/config/types.js +1 -0
  18. package/dist/core/agent-moe.js +102 -0
  19. package/dist/core/agent.js +886 -0
  20. package/dist/core/bootstrap.js +404 -0
  21. package/dist/core/index.js +2 -0
  22. package/dist/core/prompt-builder.js +76 -0
  23. package/dist/core/session-logger.js +197 -0
  24. package/dist/core/types.js +1 -0
  25. package/dist/core/version.js +24 -0
  26. package/dist/core/workspace.js +76 -0
  27. package/dist/i18n/en.json +598 -0
  28. package/dist/i18n/index.js +46 -0
  29. package/dist/i18n/ru.json +598 -0
  30. package/dist/index.js +22 -0
  31. package/dist/llm/image-utils.js +143 -0
  32. package/dist/llm/index.js +4 -0
  33. package/dist/llm/model-loader.js +78 -0
  34. package/dist/llm/openai-compat.js +359 -0
  35. package/dist/llm/orchestrator.js +198 -0
  36. package/dist/llm/provider.js +10 -0
  37. package/dist/llm/response.js +39 -0
  38. package/dist/llm/token-counter.js +39 -0
  39. package/dist/llm/types.js +1 -0
  40. package/dist/logger/app-logger.js +143 -0
  41. package/dist/logger/file-log.js +151 -0
  42. package/dist/logger/index.js +1 -0
  43. package/dist/main.js +672 -357
  44. package/dist/migration/backup.js +45 -0
  45. package/dist/migration/detect.js +50 -0
  46. package/dist/migration/index.js +2 -0
  47. package/dist/modules/artifacts/store.js +61 -0
  48. package/dist/modules/browser/actions.js +76 -0
  49. package/dist/modules/browser/bridge-client.js +199 -0
  50. package/dist/modules/browser/bridge-path.js +10 -0
  51. package/dist/modules/browser/bridge-server.mjs +202 -202
  52. package/dist/modules/browser/cookie-store.js +24 -0
  53. package/dist/modules/browser/driver.js +136 -0
  54. package/dist/modules/browser/index.js +7 -0
  55. package/dist/modules/browser/module.js +29 -0
  56. package/dist/modules/browser/session.js +338 -0
  57. package/dist/modules/browser/snapshot.js +148 -0
  58. package/dist/modules/browser/types.js +12 -0
  59. package/dist/modules/certification/cli.js +174 -0
  60. package/dist/modules/certification/fact-checker.js +82 -0
  61. package/dist/modules/certification/loader.js +105 -0
  62. package/dist/modules/certification/manifest.js +50 -0
  63. package/dist/modules/certification/runner.js +159 -0
  64. package/dist/modules/certification/scenarios.js +124 -0
  65. package/dist/modules/certification/types.js +1 -0
  66. package/dist/modules/context/chunk-query.js +100 -0
  67. package/dist/modules/context/fact-extractor.js +162 -0
  68. package/dist/modules/context/history.js +15 -0
  69. package/dist/modules/context/index.js +1 -0
  70. package/dist/modules/context/manager.js +423 -0
  71. package/dist/modules/execution/audit-runners.js +152 -0
  72. package/dist/modules/execution/auditor.js +218 -0
  73. package/dist/modules/execution/execution-plugin.js +272 -0
  74. package/dist/modules/execution/index.js +8 -0
  75. package/dist/modules/execution/module.js +436 -0
  76. package/dist/modules/execution/moe-executor.js +291 -0
  77. package/dist/modules/execution/plan-coverage.js +68 -0
  78. package/dist/modules/execution/plan-persister.js +46 -0
  79. package/dist/modules/execution/plan-store.js +157 -0
  80. package/dist/modules/execution/plan-tool.js +508 -0
  81. package/dist/modules/execution/plan-validator.js +153 -0
  82. package/dist/modules/execution/planner.js +90 -0
  83. package/dist/modules/execution/stuck-detector.js +510 -0
  84. package/dist/modules/execution/tracker.js +67 -0
  85. package/dist/modules/execution/types.js +1 -0
  86. package/dist/modules/execution/verifier.js +222 -0
  87. package/dist/modules/execution/windows-commands.js +41 -0
  88. package/dist/modules/hallucination/confidence.js +66 -0
  89. package/dist/modules/hallucination/consistency.js +26 -0
  90. package/dist/modules/hallucination/detector.js +43 -0
  91. package/dist/modules/hallucination/factual.js +129 -0
  92. package/dist/modules/hallucination/index.js +5 -0
  93. package/dist/modules/hallucination/js-identifiers.js +262 -0
  94. package/dist/modules/hallucination/llm-judge.js +101 -0
  95. package/dist/modules/index.js +5 -0
  96. package/dist/modules/indexer/cache.js +40 -0
  97. package/dist/modules/indexer/index.js +3 -0
  98. package/dist/modules/indexer/module.js +245 -0
  99. package/dist/modules/indexer/project-profile.js +183 -0
  100. package/dist/modules/indexer/walker.js +101 -0
  101. package/dist/modules/lsp/check-tool.js +58 -0
  102. package/dist/modules/lsp/client.js +278 -0
  103. package/dist/modules/lsp/command.js +60 -0
  104. package/dist/modules/lsp/config.js +135 -0
  105. package/dist/modules/lsp/index.js +3 -0
  106. package/dist/modules/lsp/module.js +232 -0
  107. package/dist/modules/lsp/probe.js +76 -0
  108. package/dist/modules/lsp/project-root.js +32 -0
  109. package/dist/modules/lsp/startup-check.js +141 -0
  110. package/dist/modules/lsp/types.js +1 -0
  111. package/dist/modules/mcp/client.js +399 -0
  112. package/dist/modules/mcp/index.js +3 -0
  113. package/dist/modules/mcp/module.js +142 -0
  114. package/dist/modules/mcp/registry.js +15 -0
  115. package/dist/modules/memory/index.js +1 -0
  116. package/dist/modules/memory/module.js +96 -0
  117. package/dist/modules/memory/search.js +42 -0
  118. package/dist/modules/memory/store.js +69 -0
  119. package/dist/modules/pipelines/engine.js +60 -0
  120. package/dist/modules/pipelines/index.js +3 -0
  121. package/dist/modules/pipelines/parser.js +56 -0
  122. package/dist/modules/pipelines/template.js +14 -0
  123. package/dist/modules/plugins/builtin/lint-on-write.js +231 -0
  124. package/dist/modules/plugins/builtin/notify.js +9 -0
  125. package/dist/modules/plugins/index.js +1 -0
  126. package/dist/modules/plugins/loader.js +70 -0
  127. package/dist/modules/plugins/manager.js +217 -0
  128. package/dist/modules/plugins/types.js +1 -0
  129. package/dist/modules/processes/detect.js +34 -0
  130. package/dist/modules/processes/index.js +2 -0
  131. package/dist/modules/processes/registry.js +327 -0
  132. package/dist/modules/processes/runner.js +23 -0
  133. package/dist/modules/registry.js +47 -0
  134. package/dist/modules/security/audit-log.js +136 -0
  135. package/dist/modules/security/audit-notifier.js +292 -0
  136. package/dist/modules/security/command-validator.js +205 -0
  137. package/dist/modules/security/content-scanner.js +53 -0
  138. package/dist/modules/security/data-sanitizer.js +89 -0
  139. package/dist/modules/security/encryption.js +242 -0
  140. package/dist/modules/security/index.js +14 -0
  141. package/dist/modules/security/network-validator.js +71 -0
  142. package/dist/modules/security/path-validator.js +207 -0
  143. package/dist/modules/security/rate-limiter.js +119 -0
  144. package/dist/modules/security/security-policies.js +531 -0
  145. package/dist/modules/security/session-encryption.js +210 -0
  146. package/dist/modules/security/session-isolation.js +95 -0
  147. package/dist/modules/session/index.js +3 -0
  148. package/dist/modules/session/manager.js +172 -0
  149. package/dist/modules/session/module.js +24 -0
  150. package/dist/modules/session/store.js +222 -0
  151. package/dist/modules/session/types.js +1 -0
  152. package/dist/modules/skills/index.js +2 -0
  153. package/dist/modules/skills/loader.js +72 -0
  154. package/dist/modules/skills/matcher.js +27 -0
  155. package/dist/modules/skills/module.js +129 -0
  156. package/dist/modules/types.js +1 -0
  157. package/dist/modules/updater/checker.js +96 -0
  158. package/dist/modules/updater/index.js +2 -0
  159. package/dist/modules/updater/module.js +116 -0
  160. package/dist/modules/user-profile/compressor.js +16 -0
  161. package/dist/modules/user-profile/index.js +1 -0
  162. package/dist/modules/user-profile/profile.js +68 -0
  163. package/dist/skills/builtin/git.md +36 -36
  164. package/dist/skills/builtin/typescript.md +35 -35
  165. package/dist/tools/approve.js +32 -0
  166. package/dist/tools/attach-image.js +89 -0
  167. package/dist/tools/bash.js +496 -0
  168. package/dist/tools/browser.js +114 -0
  169. package/dist/tools/chunk-query.js +99 -0
  170. package/dist/tools/create-dir.js +55 -0
  171. package/dist/tools/delete-file.js +62 -0
  172. package/dist/tools/download-file.js +116 -0
  173. package/dist/tools/edit-file.js +79 -0
  174. package/dist/tools/enable-tools.js +58 -0
  175. package/dist/tools/executor.js +144 -0
  176. package/dist/tools/file-info.js +46 -0
  177. package/dist/tools/filter-tools.js +17 -0
  178. package/dist/tools/glob-tool.js +26 -0
  179. package/dist/tools/grep-tool.js +84 -0
  180. package/dist/tools/hidden-tools-block.js +37 -0
  181. package/dist/tools/index.js +78 -0
  182. package/dist/tools/list-dir.js +48 -0
  183. package/dist/tools/load-skill.js +42 -0
  184. package/dist/tools/mcp-call.js +68 -0
  185. package/dist/tools/move-file.js +85 -0
  186. package/dist/tools/path-utils.js +51 -0
  187. package/dist/tools/pipeline-run.js +144 -0
  188. package/dist/tools/preview.js +2 -0
  189. package/dist/tools/process-kill.js +29 -0
  190. package/dist/tools/process-list.js +36 -0
  191. package/dist/tools/process-log.js +45 -0
  192. package/dist/tools/question.js +140 -0
  193. package/dist/tools/read-file.js +91 -0
  194. package/dist/tools/recall.js +117 -0
  195. package/dist/tools/registry.js +47 -0
  196. package/dist/tools/remember.js +67 -0
  197. package/dist/tools/scope-check.js +30 -0
  198. package/dist/tools/search-history.js +84 -0
  199. package/dist/tools/subagent.js +196 -0
  200. package/dist/tools/types.js +1 -0
  201. package/dist/tools/user-input.js +123 -0
  202. package/dist/tools/web-browse.js +86 -0
  203. package/dist/tools/web-fetch.js +98 -0
  204. package/dist/tools/web-search.js +78 -0
  205. package/dist/tools/write-file.js +81 -0
  206. package/dist/ui/box.js +77 -0
  207. package/dist/ui/colors.js +4 -0
  208. package/dist/ui/diff.js +178 -0
  209. package/dist/ui/index.js +6 -0
  210. package/dist/ui/line-editor.js +703 -0
  211. package/dist/ui/line-math.js +69 -0
  212. package/dist/ui/md-formatter.js +212 -0
  213. package/dist/ui/output.js +13 -0
  214. package/dist/ui/plan-view.js +103 -0
  215. package/dist/ui/renderer.js +209 -0
  216. package/dist/ui/spinner.js +70 -0
  217. package/dist/ui/table.js +144 -0
  218. package/package.json +48 -48
@@ -0,0 +1,144 @@
1
+ import { t } from "../i18n/index";
2
+ import { PipelineEngine } from "../modules/pipelines/engine";
3
+ import { PipelineParser } from "../modules/pipelines/parser";
4
+ import { TemplateEngine } from "../modules/pipelines/template";
5
+ import { Agent } from "../core/agent";
6
+ import { ContextManager } from "../modules/context/manager";
7
+ import { PluginManager } from "../modules/plugins/manager";
8
+ import { HallucinationDetector } from "../modules/hallucination/detector";
9
+ import { logSecurityBlock } from "../modules/security/audit-log";
10
+ import { getSessionSecurityConfig } from "../modules/security/session-isolation";
11
+ const engine = new PipelineEngine();
12
+ const MAX_CONCURRENT = 3;
13
+ const MAX_ATTEMPTS = 3;
14
+ async function runStep(ctx, step, params, outputs) {
15
+ const prompt = TemplateEngine.render(step.prompt, params, outputs);
16
+ let lastError = "";
17
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
18
+ try {
19
+ const subContextManager = new ContextManager(ctx.config.contextWindow, ctx.config.contextBudget);
20
+ const subPluginManager = new PluginManager();
21
+ const systemPrompt = {
22
+ content: `You are a pipeline step agent ("${step.agent}"). Complete the given task using the available tools.`,
23
+ priority: "critical",
24
+ essential: true,
25
+ estimatedTokens: 80,
26
+ };
27
+ const subDeps = {
28
+ config: ctx.config,
29
+ llmProvider: ctx.llmProvider,
30
+ toolExecutor: ctx.toolExecutor,
31
+ pluginManager: subPluginManager,
32
+ contextManager: subContextManager,
33
+ hallucinationDetector: new HallucinationDetector(ctx.baseDir, ctx.llmProvider),
34
+ logger: ctx.logger,
35
+ baseDir: ctx.baseDir,
36
+ scope: ctx.scope,
37
+ recursionDepth: (ctx.recursionDepth ?? 0) + 1,
38
+ promptBlocks: [systemPrompt],
39
+ };
40
+ const subAgent = new Agent(subDeps);
41
+ const result = await subAgent.run(prompt);
42
+ if (result.success) {
43
+ return { ok: true, output: result.text };
44
+ }
45
+ lastError = result.error || "no output";
46
+ }
47
+ catch (e) {
48
+ lastError = e.message;
49
+ }
50
+ await new Promise((r) => setTimeout(r, 500 * attempt));
51
+ }
52
+ return { ok: false, output: "", error: lastError };
53
+ }
54
+ export const pipelineRunTool = {
55
+ name: "pipeline_run",
56
+ description: "Run a named pipeline with YAML definition. Creates a DAG of sub-agents that execute in dependency order.",
57
+ tags: ["shell", "code"],
58
+ parameters: {
59
+ type: "object",
60
+ properties: {
61
+ name: { type: "string", description: "Pipeline name" },
62
+ yaml: { type: "string", description: "Pipeline YAML definition with steps" },
63
+ params: { type: "object", description: "Template params for {key} placeholders" },
64
+ },
65
+ required: ["name", "yaml"],
66
+ },
67
+ handler: async (ctx, args) => {
68
+ const name = String(args.name || "");
69
+ const yaml = String(args.yaml || "");
70
+ const params = args.params || {};
71
+ if (!yaml) {
72
+ return { success: false, output: t("pipeline.invalid") };
73
+ }
74
+ if (!ctx.llmProvider || !ctx.toolExecutor) {
75
+ return {
76
+ success: false,
77
+ output: "Pipeline cannot run: missing llmProvider or toolExecutor in context",
78
+ };
79
+ }
80
+ const securityConfig = ctx.sessionContext
81
+ ? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
82
+ : ctx.config.security;
83
+ const maxDepth = securityConfig?.maxRecursionDepth ?? 3;
84
+ const currentDepth = ctx.recursionDepth ?? 0;
85
+ if (currentDepth >= maxDepth) {
86
+ logSecurityBlock(ctx.sessionId, "bash_command", `Maximum recursion depth (${maxDepth}) exceeded`, name);
87
+ return {
88
+ success: false,
89
+ output: `[SECURITY BLOCKED] Maximum pipeline recursion depth (${maxDepth}) exceeded`,
90
+ };
91
+ }
92
+ try {
93
+ const pipeline = PipelineParser.parse(yaml);
94
+ engine.reset();
95
+ const order = engine.resolveDependencies(pipeline.steps);
96
+ const completed = new Set();
97
+ const outputs = {};
98
+ const logs = [];
99
+ const failed = new Set();
100
+ while (completed.size < pipeline.steps.length) {
101
+ const ready = engine
102
+ .getReadySteps(pipeline.steps, completed)
103
+ .filter((s) => !failed.has(s.id));
104
+ if (ready.length === 0) {
105
+ if (failed.size > 0)
106
+ break;
107
+ throw new Error(t("pipeline.circular", { stepId: order.join(", ") }));
108
+ }
109
+ const batch = ready.slice(0, MAX_CONCURRENT);
110
+ const results = await Promise.all(batch.map((step) => runStep(ctx, step, params, outputs)));
111
+ for (let i = 0; i < batch.length; i++) {
112
+ const step = batch[i];
113
+ const result = results[i];
114
+ if (result.ok) {
115
+ completed.add(step.id);
116
+ outputs[step.id] = { output: result.output };
117
+ engine.setStepStatus(step.id, "done");
118
+ logs.push(` ✓ ${step.id}: ${result.output.split("\n")[0]}`);
119
+ }
120
+ else {
121
+ failed.add(step.id);
122
+ engine.setStepStatus(step.id, "failed");
123
+ logs.push(` ✗ ${step.id}: ${result.error}`);
124
+ }
125
+ }
126
+ if (failed.size > 0)
127
+ break;
128
+ }
129
+ if (failed.size > 0) {
130
+ return {
131
+ success: false,
132
+ output: `Pipeline "${pipeline.name}" failed. Executed ${completed.size}/${pipeline.steps.length} steps.\n${logs.join("\n")}`,
133
+ };
134
+ }
135
+ return {
136
+ success: true,
137
+ output: `Pipeline "${pipeline.name}" completed successfully (${completed.size} steps).\n${logs.join("\n")}`,
138
+ };
139
+ }
140
+ catch (e) {
141
+ return { success: false, output: `Pipeline error: ${e.message}` };
142
+ }
143
+ },
144
+ };
@@ -0,0 +1,2 @@
1
+ /** Maximum number of lines shown in tool output previews. */
2
+ export const MAX_PREVIEW_LINES = 15;
@@ -0,0 +1,29 @@
1
+ import { processRegistry } from "../modules/processes";
2
+ import { t } from "../i18n/index";
3
+ export const processKillTool = {
4
+ name: "process_kill",
5
+ description: "Stop a background process started via bash (dev server, watcher). Kills the whole process tree (children included). Use the id returned by bash or process_list.",
6
+ tags: ["shell"],
7
+ parameters: {
8
+ type: "object",
9
+ properties: {
10
+ id: { type: "string", description: "Process id from bash output or process_list" },
11
+ },
12
+ required: ["id"],
13
+ },
14
+ handler: async (ctx, args) => {
15
+ const id = String(args.id);
16
+ const entry = processRegistry.get(id);
17
+ if (!entry) {
18
+ return { success: false, output: t("proc.not_found", { id }) };
19
+ }
20
+ const killed = processRegistry.kill(id);
21
+ if (!killed) {
22
+ return { success: false, output: t("proc.kill_failed", { id }) };
23
+ }
24
+ return {
25
+ success: true,
26
+ output: t("proc.killed", { id, pid: entry.pid }),
27
+ };
28
+ },
29
+ };
@@ -0,0 +1,36 @@
1
+ import { processRegistry } from "../modules/processes";
2
+ import { t } from "../i18n/index";
3
+ export const processListTool = {
4
+ name: "process_list",
5
+ description: "List background processes started via the bash tool (dev servers, watchers, long-running commands). Shows id, pid, command, status, and recent output. Use with process_log and process_kill to inspect or stop them.",
6
+ tags: ["shell"],
7
+ parameters: {
8
+ type: "object",
9
+ properties: {},
10
+ },
11
+ handler: async (ctx) => {
12
+ const list = processRegistry.list(ctx.sessionId);
13
+ if (list.length === 0) {
14
+ return { success: true, output: t("proc.none") };
15
+ }
16
+ const statusLabel = (status) => {
17
+ if (status === "running")
18
+ return t("proc.status_running");
19
+ if (status === "exited")
20
+ return t("proc.status_exited");
21
+ return t("proc.status_killed");
22
+ };
23
+ const lines = [`${t("proc.list_header")} (${list.length}):`];
24
+ for (const entry of list) {
25
+ const tail = entry.log.length > 0 ? entry.log[entry.log.length - 1] : "";
26
+ const detail = tail ? ` — ${tail.slice(0, 80)}${tail.length > 80 ? "…" : ""}` : "";
27
+ lines.push(` ${entry.id} PID ${entry.pid} ${statusLabel(entry.status)} ${entry.command}${detail}`);
28
+ }
29
+ lines.push(`\n${t("proc.hint", {
30
+ list: "process_list",
31
+ log: "process_log",
32
+ kill: "process_kill",
33
+ })}`);
34
+ return { success: true, output: lines.join("\n") };
35
+ },
36
+ };
@@ -0,0 +1,45 @@
1
+ import { processRegistry } from "../modules/processes";
2
+ import { t } from "../i18n/index";
3
+ import { MAX_PREVIEW_LINES } from "./preview";
4
+ const DEFAULT_TAIL = MAX_PREVIEW_LINES;
5
+ export const processLogTool = {
6
+ name: "process_log",
7
+ description: `Show the buffered output of a background process started via bash. Shows the last ${DEFAULT_TAIL} lines by default. Use after starting a dev server to verify it came up without errors, and while it runs to check its state.`,
8
+ tags: ["shell"],
9
+ boundedOutput: true,
10
+ parameters: {
11
+ type: "object",
12
+ properties: {
13
+ id: { type: "string", description: "Process id from bash output or process_list" },
14
+ tail: {
15
+ type: "number",
16
+ description: `Number of trailing lines to show (default: ${DEFAULT_TAIL}, max 300)`,
17
+ },
18
+ },
19
+ required: ["id"],
20
+ },
21
+ handler: async (ctx, args) => {
22
+ const id = String(args.id);
23
+ const entry = processRegistry.get(id);
24
+ if (!entry) {
25
+ return { success: false, output: t("proc.not_found", { id }) };
26
+ }
27
+ const status = entry.status === "running"
28
+ ? t("proc.status_running")
29
+ : entry.status === "exited"
30
+ ? t("proc.status_exited")
31
+ : t("proc.status_killed");
32
+ const tail = typeof args.tail === "number" ? args.tail : DEFAULT_TAIL;
33
+ const log = processRegistry.getLog(id, tail);
34
+ if (!log) {
35
+ return {
36
+ success: true,
37
+ output: `${t("proc.log_header", { id, status })} ${t("proc.log_empty")}`,
38
+ };
39
+ }
40
+ return {
41
+ success: true,
42
+ output: `${t("proc.log_header", { id, status })}\n${log}`,
43
+ };
44
+ },
45
+ };
@@ -0,0 +1,140 @@
1
+ import { t } from "../i18n/index";
2
+ import { askUser } from "./user-input";
3
+ const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
4
+ 1. Gather user preferences or requirements
5
+ 2. Clarify ambiguous instructions
6
+ 3. Get decisions on implementation choices as you work
7
+ 4. Offer choices to the user about what direction to take.
8
+
9
+ Usage notes:
10
+ - When "custom" is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options
11
+ - Answers are returned as arrays of labels; set "multiple": true to allow selecting more than one
12
+ - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
13
+ - Execution blocks until the user answers every question
14
+ - Omit "options" for a free-text question`;
15
+ function isOption(val) {
16
+ if (!val || typeof val !== "object")
17
+ return false;
18
+ const opt = val;
19
+ return typeof opt.label === "string" && typeof opt.description === "string";
20
+ }
21
+ function normalizeQuestions(args) {
22
+ const raw = args.questions;
23
+ if (Array.isArray(raw)) {
24
+ const specs = [];
25
+ for (const item of raw) {
26
+ if (!item || typeof item !== "object")
27
+ continue;
28
+ const q = item;
29
+ if (typeof q.question !== "string" || !q.question.trim())
30
+ continue;
31
+ const options = Array.isArray(q.options) ? q.options.filter(isOption) : undefined;
32
+ // An explicitly provided options array with no valid entries is a
33
+ // malformed call — skip the question instead of blocking on stdin.
34
+ if (Array.isArray(q.options) && options.length === 0)
35
+ continue;
36
+ specs.push({
37
+ question: q.question,
38
+ header: typeof q.header === "string" ? q.header : undefined,
39
+ options,
40
+ multiple: q.multiple === true,
41
+ custom: q.custom === false ? false : undefined,
42
+ });
43
+ }
44
+ return specs;
45
+ }
46
+ // Legacy format: { question: "..." } — plain free-text question.
47
+ if (typeof args.question === "string" && args.question.trim()) {
48
+ return [{ question: args.question }];
49
+ }
50
+ return [];
51
+ }
52
+ export const questionTool = {
53
+ name: "question",
54
+ description: DESCRIPTION,
55
+ tags: ["core"],
56
+ interactive: true,
57
+ parameters: {
58
+ type: "object",
59
+ properties: {
60
+ questions: {
61
+ type: "array",
62
+ description: "Questions to ask",
63
+ items: {
64
+ type: "object",
65
+ properties: {
66
+ question: { type: "string", description: "Complete question" },
67
+ header: {
68
+ type: "string",
69
+ description: "Very short label (max 30 chars)",
70
+ },
71
+ options: {
72
+ type: "array",
73
+ description: "Available choices; omit for a free-text question",
74
+ items: {
75
+ type: "object",
76
+ properties: {
77
+ label: {
78
+ type: "string",
79
+ description: "Display text (1-5 words, concise)",
80
+ },
81
+ description: {
82
+ type: "string",
83
+ description: "Explanation of choice",
84
+ },
85
+ },
86
+ required: ["label", "description"],
87
+ },
88
+ },
89
+ multiple: {
90
+ type: "boolean",
91
+ description: "Allow selecting multiple choices",
92
+ },
93
+ custom: {
94
+ type: "boolean",
95
+ description: "Allow typing a custom answer (default: true)",
96
+ },
97
+ },
98
+ required: ["question"],
99
+ },
100
+ },
101
+ question: {
102
+ type: "string",
103
+ description: "Legacy single free-text question",
104
+ },
105
+ },
106
+ },
107
+ handler: async (ctx, args) => {
108
+ if (ctx.exitOnComplete) {
109
+ return { success: false, output: t("tool.interactive_disabled") };
110
+ }
111
+ const questions = normalizeQuestions(args);
112
+ if (questions.length === 0) {
113
+ return { success: false, output: t("tool.question.no_questions") };
114
+ }
115
+ const answers = [];
116
+ for (let i = 0; i < questions.length; i++) {
117
+ const q = questions[i];
118
+ const progress = questions.length > 1
119
+ ? t("tool.question.progress", {
120
+ current: i + 1,
121
+ total: questions.length,
122
+ })
123
+ : undefined;
124
+ answers.push(await askUser(q.question, {
125
+ header: q.header,
126
+ options: q.options,
127
+ multiple: q.multiple,
128
+ custom: q.custom,
129
+ progress,
130
+ }));
131
+ }
132
+ const formatted = questions
133
+ .map((q, i) => `"${q.question}"="${answers[i].length ? answers[i].join(", ") : t("tool.question.unanswered")}"`)
134
+ .join(", ");
135
+ return {
136
+ success: true,
137
+ output: t("tool.question.answered", { formatted }),
138
+ };
139
+ },
140
+ };
@@ -0,0 +1,91 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { extname } from "path";
3
+ import { t } from "../i18n/index";
4
+ import { isPathInScope } from "../modules/security/path-validator";
5
+ import { DEFAULT_SECURITY_CONFIG } from "../config/security";
6
+ import { logSecurityBlock } from "../modules/security/audit-log";
7
+ import { safeResolvePath } from "./path-utils";
8
+ /**
9
+ * Default lines per read_file call. Deliberately much larger than
10
+ * MAX_PREVIEW_LINES (15): a 15-line default forces the model to issue 10-17
11
+ * calls per file, which floods the context and drives compaction every 15
12
+ * iterations (observed: 96 read_file calls, App.tsx read 23x, then 11
13
+ * compactions in ~20 min that deleted the user's task mid-turn). 300 lines
14
+ * covers typical component files in one call while still capping output.
15
+ */
16
+ const DEFAULT_LIMIT = 300;
17
+ export const readFileTool = {
18
+ name: "read_file",
19
+ description: `Read a file from the filesystem. Reads up to ${DEFAULT_LIMIT} lines at a time by default; use offset to page through large files.`,
20
+ tags: ["file", "code"],
21
+ boundedOutput: true,
22
+ parameters: {
23
+ type: "object",
24
+ properties: {
25
+ path: { type: "string", description: "File path to read" },
26
+ offset: {
27
+ type: "number",
28
+ description: `Starting line (1-indexed). Use offset=<next> from a truncated result to continue reading.`,
29
+ },
30
+ limit: {
31
+ type: "number",
32
+ description: `Number of lines to read (default ${DEFAULT_LIMIT})`,
33
+ },
34
+ },
35
+ required: ["path"],
36
+ },
37
+ handler: async (ctx, args) => {
38
+ const path = String(args.path);
39
+ const resolved = safeResolvePath(ctx.baseDir, path);
40
+ const securityPaths = ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths;
41
+ const scopeCheck = isPathInScope(ctx.baseDir, resolved, ctx.scope, securityPaths);
42
+ if (!scopeCheck.allowed) {
43
+ const pathStr = path;
44
+ logSecurityBlock(ctx.sessionId || undefined, "file_read", scopeCheck.reason || "Path not allowed", pathStr);
45
+ return {
46
+ success: false,
47
+ output: t("file.path_not_allowed", {
48
+ path: `${path} — ${scopeCheck.reason}`,
49
+ }),
50
+ };
51
+ }
52
+ if (!existsSync(resolved)) {
53
+ // For relative paths, tell the model WHERE the path resolved — the
54
+ // "not found" alone is confusing (it cannot tell baseDir from cwd).
55
+ const output = resolved !== path
56
+ ? t("file.notfound_resolved", {
57
+ path,
58
+ resolved,
59
+ })
60
+ : t("file.notfound", { path });
61
+ return { success: false, output };
62
+ }
63
+ const content = readFileSync(resolved, "utf-8");
64
+ const lines = content.split("\n");
65
+ const total = lines.length;
66
+ const offset = args.offset || 1;
67
+ const limit = args.limit || DEFAULT_LIMIT;
68
+ const end = Math.min(offset - 1 + limit, total);
69
+ const selected = lines.slice(offset - 1, end).join("\n");
70
+ const ext = extname(resolved) || "(no extension)";
71
+ const header = t("file.read_header", {
72
+ path,
73
+ ext,
74
+ total: String(total),
75
+ from: String(offset),
76
+ to: String(end),
77
+ });
78
+ const truncated = end < total
79
+ ? t("file.read_truncated", {
80
+ remaining: String(total - end),
81
+ next: String(end + 1),
82
+ })
83
+ : "";
84
+ const display = truncated ? `${header}${truncated}` : header;
85
+ return {
86
+ success: true,
87
+ output: `${header}\n${selected}${truncated}`,
88
+ display,
89
+ };
90
+ },
91
+ };
@@ -0,0 +1,117 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { t } from "../i18n/index";
4
+ import { MemoryStore } from "../modules/memory/store";
5
+ const CATEGORIES = ["preferences", "conventions", "decisions", "errors", "facts"];
6
+ export const recallTool = {
7
+ name: "recall",
8
+ description: "Recall stored information from memory. Search across preferences, facts, conventions, decisions, errors.",
9
+ tags: ["memory"],
10
+ parameters: {
11
+ type: "object",
12
+ properties: {
13
+ query: {
14
+ type: "string",
15
+ description: "Search query (full-text search across all memory)",
16
+ },
17
+ category: {
18
+ type: "string",
19
+ description: "Limit to specific category: preferences, conventions, decisions, errors, facts",
20
+ enum: CATEGORIES,
21
+ },
22
+ },
23
+ },
24
+ handler: async (_ctx, args) => {
25
+ const query = args.query ? String(args.query) : "";
26
+ const category = args.category ? String(args.category) : "";
27
+ const memoryDir = join(homedir(), ".mma", "memory");
28
+ const store = new MemoryStore(memoryDir);
29
+ try {
30
+ // No query, no category → show everything
31
+ if (!query && !category) {
32
+ return { success: true, output: formatAll(store) };
33
+ }
34
+ // Category only → show that category
35
+ if (!query && category) {
36
+ return { success: true, output: formatCategory(store, category) };
37
+ }
38
+ // Query with optional category → search
39
+ if (category) {
40
+ const results = searchCategory(store, category, query);
41
+ if (results.length === 0) {
42
+ return { success: true, output: t("tool.recall.empty", { query }) };
43
+ }
44
+ return {
45
+ success: true,
46
+ output: t("tool.recall.search_results", { category, results: results.join("\n") }),
47
+ };
48
+ }
49
+ // Query across all
50
+ const results = store.search(query);
51
+ if (results.length === 0) {
52
+ return { success: true, output: t("tool.recall.empty", { query }) };
53
+ }
54
+ const formatted = results.map((r) => `[${r.file}] ${r.match}`).join("\n");
55
+ return {
56
+ success: true,
57
+ output: t("tool.recall.search_results", { category: "all", results: formatted }),
58
+ };
59
+ }
60
+ catch (err) {
61
+ return { success: true, output: t("tool.memory_error", { error: String(err) }) };
62
+ }
63
+ },
64
+ };
65
+ function formatAll(store) {
66
+ const parts = [];
67
+ const prefs = store.getPreferences();
68
+ if (Object.keys(prefs).length > 0) {
69
+ parts.push("Preferences:");
70
+ for (const [k, v] of Object.entries(prefs)) {
71
+ parts.push(` ${k} = ${v}`);
72
+ }
73
+ }
74
+ for (const cat of ["facts", "conventions", "decisions", "errors"]) {
75
+ const content = store.read(cat);
76
+ const entries = content.split("\n").filter((l) => l.startsWith("- "));
77
+ if (entries.length > 0) {
78
+ parts.push(`\n${cat.charAt(0).toUpperCase() + cat.slice(1)} (last 5):`);
79
+ for (const e of entries.slice(-5)) {
80
+ parts.push(` ${e}`);
81
+ }
82
+ }
83
+ }
84
+ return parts.length > 0 ? parts.join("\n") : t("tool.recall.no_memory");
85
+ }
86
+ function formatCategory(store, category) {
87
+ if (category === "preferences") {
88
+ const prefs = store.getPreferences();
89
+ if (Object.keys(prefs).length === 0)
90
+ return t("tool.recall.no_memory");
91
+ const lines = ["Preferences:"];
92
+ for (const [k, v] of Object.entries(prefs)) {
93
+ lines.push(` ${k} = ${v}`);
94
+ }
95
+ return lines.join("\n");
96
+ }
97
+ const content = store.read(category);
98
+ const entries = content.split("\n").filter((l) => l.startsWith("- "));
99
+ if (entries.length === 0)
100
+ return t("tool.recall.no_memory");
101
+ return `${category.charAt(0).toUpperCase() + category.slice(1)} (${entries.length} entries):\n${entries.join("\n")}`;
102
+ }
103
+ function searchCategory(store, category, query) {
104
+ if (category === "preferences") {
105
+ const prefs = store.getPreferences();
106
+ const lower = query.toLowerCase();
107
+ return Object.entries(prefs)
108
+ .filter(([k, v]) => k.toLowerCase().includes(lower) || v.toLowerCase().includes(lower))
109
+ .map(([k, v]) => ` ${k} = ${v}`);
110
+ }
111
+ const content = store.read(category);
112
+ const lower = query.toLowerCase();
113
+ return content
114
+ .split("\n")
115
+ .filter((l) => l.startsWith("- ") && l.toLowerCase().includes(lower))
116
+ .map((l) => ` ${l}`);
117
+ }
@@ -0,0 +1,47 @@
1
+ export class ToolRegistry {
2
+ tools = new Map();
3
+ register(tool) {
4
+ if (this.tools.has(tool.name)) {
5
+ throw new Error(`Tool already registered: ${tool.name}`);
6
+ }
7
+ this.tools.set(tool.name, tool);
8
+ }
9
+ get(name) {
10
+ return this.tools.get(name);
11
+ }
12
+ has(name) {
13
+ return this.tools.has(name);
14
+ }
15
+ list() {
16
+ return Array.from(this.tools.keys()).sort();
17
+ }
18
+ getAll() {
19
+ return Array.from(this.tools.values());
20
+ }
21
+ getByTags(tags) {
22
+ return this.getAll().filter((t) => {
23
+ if (!t.tags || t.tags.length === 0)
24
+ return false;
25
+ return tags.some((tag) => t.tags.includes(tag));
26
+ });
27
+ }
28
+ /**
29
+ * Tools to expose to the LLM. Always includes `alwaysOn` tools, then tools
30
+ * matching the given tags. When no tags are given, returns every registered
31
+ * tool (backward-compatible). Duplicates are impossible because the
32
+ * alwaysOn + tagged sets are unioned by name.
33
+ */
34
+ getAllForLLM(tags) {
35
+ const all = this.getAll();
36
+ const tagSet = new Set(tags ?? []);
37
+ const selected = tagSet.size === 0
38
+ ? all
39
+ : all.filter((t) => t.alwaysOn || (t.tags && t.tags.some((tag) => tagSet.has(tag))));
40
+ return selected.map((t) => ({
41
+ name: t.name,
42
+ description: t.description,
43
+ parameters: t.parameters,
44
+ boundedOutput: t.boundedOutput,
45
+ }));
46
+ }
47
+ }