micro-models-agent 0.40.1 → 0.41.2

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 (171) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +9 -19
  3. package/dist/cli/completer.js +36 -37
  4. package/dist/cli/index.js +2 -2
  5. package/dist/cli/main.js +48 -23
  6. package/dist/cli/plugin-commands.js +36 -0
  7. package/dist/cli/repl-commands.js +40 -12
  8. package/dist/cli/repl.js +217 -87
  9. package/dist/cli/run-result.js +22 -0
  10. package/dist/cli/security-commands.js +5 -7
  11. package/dist/cli/setup.js +8 -26
  12. package/dist/config/config.js +52 -5
  13. package/dist/config/defaults.js +29 -5
  14. package/dist/config/experts.js +1 -1
  15. package/dist/config/index.js +3 -3
  16. package/dist/config/security.js +3 -10
  17. package/dist/core/agent-moe.js +2 -10
  18. package/dist/core/agent.js +273 -82
  19. package/dist/core/bootstrap.js +80 -13
  20. package/dist/core/index.js +2 -2
  21. package/dist/core/prompt-builder.js +23 -2
  22. package/dist/core/session-logger.js +46 -4
  23. package/dist/core/version.js +24 -0
  24. package/dist/i18n/en.json +75 -2
  25. package/dist/i18n/ru.json +74 -1
  26. package/dist/index.js +1 -1
  27. package/dist/llm/image-utils.js +4 -5
  28. package/dist/llm/index.js +4 -4
  29. package/dist/llm/model-loader.js +6 -6
  30. package/dist/llm/openai-compat.js +40 -34
  31. package/dist/llm/orchestrator.js +33 -29
  32. package/dist/llm/response.js +9 -9
  33. package/dist/logger/app-logger.js +1 -1
  34. package/dist/logger/index.js +1 -1
  35. package/dist/main.js +2489 -2186
  36. package/dist/migration/backup.js +13 -13
  37. package/dist/migration/detect.js +11 -11
  38. package/dist/migration/index.js +2 -2
  39. package/dist/modules/artifacts/store.js +61 -0
  40. package/dist/modules/browser/actions.js +34 -4
  41. package/dist/modules/browser/bridge-client.js +199 -0
  42. package/dist/modules/browser/bridge-path.js +10 -0
  43. package/dist/modules/browser/bridge-server.mjs +202 -202
  44. package/dist/modules/browser/cookie-store.js +6 -6
  45. package/dist/modules/browser/driver.js +136 -0
  46. package/dist/modules/browser/index.js +7 -5
  47. package/dist/modules/browser/module.js +8 -7
  48. package/dist/modules/browser/session.js +87 -84
  49. package/dist/modules/browser/snapshot.js +92 -58
  50. package/dist/modules/browser/types.js +4 -1
  51. package/dist/modules/certification/cli.js +2 -4
  52. package/dist/modules/certification/fact-checker.js +1 -3
  53. package/dist/modules/certification/loader.js +3 -9
  54. package/dist/modules/certification/runner.js +1 -4
  55. package/dist/modules/context/chunk-query.js +100 -0
  56. package/dist/modules/context/fact-extractor.js +162 -0
  57. package/dist/modules/context/history.js +15 -0
  58. package/dist/modules/context/index.js +1 -1
  59. package/dist/modules/context/manager.js +160 -86
  60. package/dist/modules/execution/audit-runners.js +152 -0
  61. package/dist/modules/execution/auditor.js +177 -25
  62. package/dist/modules/execution/execution-plugin.js +272 -0
  63. package/dist/modules/execution/module.js +201 -544
  64. package/dist/modules/execution/moe-executor.js +25 -0
  65. package/dist/modules/execution/plan-store.js +1 -3
  66. package/dist/modules/execution/plan-tool.js +508 -0
  67. package/dist/modules/execution/plan-validator.js +10 -10
  68. package/dist/modules/execution/planner.js +6 -1
  69. package/dist/modules/execution/stuck-detector.js +173 -10
  70. package/dist/modules/execution/verifier.js +86 -42
  71. package/dist/modules/execution/windows-commands.js +41 -0
  72. package/dist/modules/hallucination/confidence.js +8 -1
  73. package/dist/modules/hallucination/detector.js +2 -5
  74. package/dist/modules/hallucination/factual.js +3 -64
  75. package/dist/modules/hallucination/index.js +1 -1
  76. package/dist/modules/hallucination/js-identifiers.js +190 -0
  77. package/dist/modules/hallucination/llm-judge.js +1 -3
  78. package/dist/modules/indexer/cache.js +9 -7
  79. package/dist/modules/indexer/index.js +3 -3
  80. package/dist/modules/indexer/module.js +95 -42
  81. package/dist/modules/indexer/project-profile.js +183 -0
  82. package/dist/modules/indexer/walker.js +17 -17
  83. package/dist/modules/lsp/check-tool.js +58 -0
  84. package/dist/modules/lsp/client.js +74 -31
  85. package/dist/modules/lsp/command.js +60 -0
  86. package/dist/modules/lsp/config.js +87 -33
  87. package/dist/modules/lsp/index.js +3 -3
  88. package/dist/modules/lsp/module.js +185 -21
  89. package/dist/modules/lsp/probe.js +76 -0
  90. package/dist/modules/lsp/project-root.js +32 -0
  91. package/dist/modules/lsp/startup-check.js +141 -0
  92. package/dist/modules/mcp/module.js +2 -6
  93. package/dist/modules/memory/index.js +1 -1
  94. package/dist/modules/memory/module.js +71 -23
  95. package/dist/modules/memory/search.js +11 -9
  96. package/dist/modules/memory/store.js +13 -13
  97. package/dist/modules/pipelines/engine.js +10 -10
  98. package/dist/modules/pipelines/index.js +3 -3
  99. package/dist/modules/pipelines/parser.js +17 -14
  100. package/dist/modules/pipelines/template.js +1 -1
  101. package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
  102. package/dist/modules/plugins/builtin/notify.js +3 -2
  103. package/dist/modules/plugins/index.js +1 -1
  104. package/dist/modules/plugins/loader.js +59 -17
  105. package/dist/modules/plugins/manager.js +73 -17
  106. package/dist/modules/processes/detect.js +34 -0
  107. package/dist/modules/processes/index.js +1 -1
  108. package/dist/modules/processes/registry.js +135 -46
  109. package/dist/modules/registry.js +4 -2
  110. package/dist/modules/security/audit-notifier.js +39 -39
  111. package/dist/modules/security/command-validator.js +2 -8
  112. package/dist/modules/security/data-sanitizer.js +1 -9
  113. package/dist/modules/security/encryption.js +58 -56
  114. package/dist/modules/security/network-validator.js +1 -9
  115. package/dist/modules/security/path-validator.js +1 -3
  116. package/dist/modules/security/security-policies.js +3 -19
  117. package/dist/modules/security/session-encryption.js +1 -1
  118. package/dist/modules/security/session-isolation.js +8 -8
  119. package/dist/modules/session/index.js +3 -3
  120. package/dist/modules/session/module.js +5 -5
  121. package/dist/modules/session/store.js +3 -9
  122. package/dist/modules/skills/matcher.js +27 -0
  123. package/dist/modules/skills/module.js +1 -2
  124. package/dist/modules/updater/checker.js +70 -6
  125. package/dist/modules/updater/index.js +2 -1
  126. package/dist/modules/updater/module.js +116 -0
  127. package/dist/modules/user-profile/compressor.js +2 -2
  128. package/dist/modules/user-profile/index.js +1 -1
  129. package/dist/modules/user-profile/profile.js +9 -9
  130. package/dist/tools/attach-image.js +1 -1
  131. package/dist/tools/bash.js +178 -19
  132. package/dist/tools/browser.js +46 -29
  133. package/dist/tools/chunk-query.js +99 -0
  134. package/dist/tools/download-file.js +116 -0
  135. package/dist/tools/enable-tools.js +58 -0
  136. package/dist/tools/executor.js +4 -5
  137. package/dist/tools/file-info.js +13 -12
  138. package/dist/tools/filter-tools.js +9 -2
  139. package/dist/tools/glob-tool.js +11 -11
  140. package/dist/tools/grep-tool.js +1 -3
  141. package/dist/tools/hidden-tools-block.js +37 -0
  142. package/dist/tools/index.js +13 -2
  143. package/dist/tools/list-dir.js +18 -17
  144. package/dist/tools/load-skill.js +1 -3
  145. package/dist/tools/path-utils.js +4 -4
  146. package/dist/tools/pipeline-run.js +25 -25
  147. package/dist/tools/process-kill.js +11 -11
  148. package/dist/tools/process-list.js +20 -22
  149. package/dist/tools/process-log.js +22 -18
  150. package/dist/tools/question.js +1 -3
  151. package/dist/tools/read-file.js +10 -2
  152. package/dist/tools/recall.js +44 -37
  153. package/dist/tools/registry.js +15 -4
  154. package/dist/tools/remember.js +29 -29
  155. package/dist/tools/scope-check.js +9 -9
  156. package/dist/tools/subagent.js +54 -9
  157. package/dist/tools/user-input.js +1 -1
  158. package/dist/tools/web-browse.js +3 -3
  159. package/dist/tools/web-fetch.js +3 -3
  160. package/dist/tools/web-search.js +3 -3
  161. package/dist/tools/write-file.js +1 -3
  162. package/dist/ui/box.js +1 -5
  163. package/dist/ui/index.js +6 -6
  164. package/dist/ui/line-editor.js +703 -0
  165. package/dist/ui/line-math.js +69 -0
  166. package/dist/ui/md-formatter.js +33 -33
  167. package/dist/ui/output.js +5 -5
  168. package/dist/ui/plan-view.js +103 -0
  169. package/dist/ui/renderer.js +15 -10
  170. package/dist/ui/table.js +1 -1
  171. package/package.json +48 -48
@@ -1,13 +1,13 @@
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';
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
11
  const engine = new PipelineEngine();
12
12
  const MAX_CONCURRENT = 3;
13
13
  const MAX_ATTEMPTS = 3;
@@ -52,29 +52,29 @@ async function runStep(ctx, step, params, outputs) {
52
52
  return { ok: false, output: "", error: lastError };
53
53
  }
54
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'],
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
58
  parameters: {
59
- type: 'object',
59
+ type: "object",
60
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' },
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
64
  },
65
- required: ['name', 'yaml'],
65
+ required: ["name", "yaml"],
66
66
  },
67
67
  handler: async (ctx, args) => {
68
- const name = String(args.name || '');
69
- const yaml = String(args.yaml || '');
68
+ const name = String(args.name || "");
69
+ const yaml = String(args.yaml || "");
70
70
  const params = args.params || {};
71
71
  if (!yaml) {
72
- return { success: false, output: t('pipeline.invalid') };
72
+ return { success: false, output: t("pipeline.invalid") };
73
73
  }
74
74
  if (!ctx.llmProvider || !ctx.toolExecutor) {
75
75
  return {
76
76
  success: false,
77
- output: 'Pipeline cannot run: missing llmProvider or toolExecutor in context',
77
+ output: "Pipeline cannot run: missing llmProvider or toolExecutor in context",
78
78
  };
79
79
  }
80
80
  const securityConfig = ctx.sessionContext
@@ -104,7 +104,7 @@ export const pipelineRunTool = {
104
104
  if (ready.length === 0) {
105
105
  if (failed.size > 0)
106
106
  break;
107
- throw new Error(t('pipeline.circular', { stepId: order.join(', ') }));
107
+ throw new Error(t("pipeline.circular", { stepId: order.join(", ") }));
108
108
  }
109
109
  const batch = ready.slice(0, MAX_CONCURRENT);
110
110
  const results = await Promise.all(batch.map((step) => runStep(ctx, step, params, outputs)));
@@ -114,12 +114,12 @@ export const pipelineRunTool = {
114
114
  if (result.ok) {
115
115
  completed.add(step.id);
116
116
  outputs[step.id] = { output: result.output };
117
- engine.setStepStatus(step.id, 'done');
117
+ engine.setStepStatus(step.id, "done");
118
118
  logs.push(` ✓ ${step.id}: ${result.output.split("\n")[0]}`);
119
119
  }
120
120
  else {
121
121
  failed.add(step.id);
122
- engine.setStepStatus(step.id, 'failed');
122
+ engine.setStepStatus(step.id, "failed");
123
123
  logs.push(` ✗ ${step.id}: ${result.error}`);
124
124
  }
125
125
  }
@@ -1,29 +1,29 @@
1
- import { processRegistry } from '../modules/processes';
2
- import { t } from '../i18n/index';
1
+ import { processRegistry } from "../modules/processes";
2
+ import { t } from "../i18n/index";
3
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'],
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
7
  parameters: {
8
- type: 'object',
8
+ type: "object",
9
9
  properties: {
10
- id: { type: 'string', description: 'Process id from bash output or process_list' },
10
+ id: { type: "string", description: "Process id from bash output or process_list" },
11
11
  },
12
- required: ['id'],
12
+ required: ["id"],
13
13
  },
14
14
  handler: async (ctx, args) => {
15
15
  const id = String(args.id);
16
16
  const entry = processRegistry.get(id);
17
17
  if (!entry) {
18
- return { success: false, output: t('proc.not_found', { id }) };
18
+ return { success: false, output: t("proc.not_found", { id }) };
19
19
  }
20
20
  const killed = processRegistry.kill(id);
21
21
  if (!killed) {
22
- return { success: false, output: t('proc.kill_failed', { id }) };
22
+ return { success: false, output: t("proc.kill_failed", { id }) };
23
23
  }
24
24
  return {
25
25
  success: true,
26
- output: t('proc.killed', { id, pid: entry.pid }),
26
+ output: t("proc.killed", { id, pid: entry.pid }),
27
27
  };
28
28
  },
29
29
  };
@@ -1,38 +1,36 @@
1
- import { processRegistry } from '../modules/processes';
2
- import { t } from '../i18n/index';
1
+ import { processRegistry } from "../modules/processes";
2
+ import { t } from "../i18n/index";
3
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'],
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
7
  parameters: {
8
- type: 'object',
8
+ type: "object",
9
9
  properties: {},
10
10
  },
11
11
  handler: async (ctx) => {
12
12
  const list = processRegistry.list(ctx.sessionId);
13
13
  if (list.length === 0) {
14
- return { success: true, output: t('proc.none') };
14
+ return { success: true, output: t("proc.none") };
15
15
  }
16
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');
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
22
  };
23
- const lines = [`${t('proc.list_header')} (${list.length}):`];
23
+ const lines = [`${t("proc.list_header")} (${list.length}):`];
24
24
  for (const entry of list) {
25
- const tail = entry.log.length > 0 ? entry.log[entry.log.length - 1] : '';
26
- const detail = tail
27
- ? ` — ${tail.slice(0, 80)}${tail.length > 80 ? '…' : ''}`
28
- : '';
25
+ const tail = entry.log.length > 0 ? entry.log[entry.log.length - 1] : "";
26
+ const detail = tail ? ` — ${tail.slice(0, 80)}${tail.length > 80 ? "…" : ""}` : "";
29
27
  lines.push(` ${entry.id} PID ${entry.pid} ${statusLabel(entry.status)} ${entry.command}${detail}`);
30
28
  }
31
- lines.push(`\n${t('proc.hint', {
32
- list: 'process_list',
33
- log: 'process_log',
34
- kill: 'process_kill',
29
+ lines.push(`\n${t("proc.hint", {
30
+ list: "process_list",
31
+ log: "process_log",
32
+ kill: "process_kill",
35
33
  })}`);
36
- return { success: true, output: lines.join('\n') };
34
+ return { success: true, output: lines.join("\n") };
37
35
  },
38
36
  };
@@ -1,41 +1,45 @@
1
- import { processRegistry } from '../modules/processes';
2
- import { t } from '../i18n/index';
3
- import { MAX_PREVIEW_LINES } from './preview';
1
+ import { processRegistry } from "../modules/processes";
2
+ import { t } from "../i18n/index";
3
+ import { MAX_PREVIEW_LINES } from "./preview";
4
4
  const DEFAULT_TAIL = MAX_PREVIEW_LINES;
5
5
  export const processLogTool = {
6
- name: 'process_log',
6
+ name: "process_log",
7
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'],
8
+ tags: ["shell"],
9
+ boundedOutput: true,
9
10
  parameters: {
10
- type: 'object',
11
+ type: "object",
11
12
  properties: {
12
- id: { type: 'string', description: 'Process id from bash output or process_list' },
13
- tail: { type: 'number', description: `Number of trailing lines to show (default: ${DEFAULT_TAIL}, max 300)` },
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
+ },
14
18
  },
15
- required: ['id'],
19
+ required: ["id"],
16
20
  },
17
21
  handler: async (ctx, args) => {
18
22
  const id = String(args.id);
19
23
  const entry = processRegistry.get(id);
20
24
  if (!entry) {
21
- return { success: false, output: t('proc.not_found', { id }) };
25
+ return { success: false, output: t("proc.not_found", { id }) };
22
26
  }
23
- const status = entry.status === 'running'
24
- ? t('proc.status_running')
25
- : entry.status === 'exited'
26
- ? t('proc.status_exited')
27
- : t('proc.status_killed');
28
- const tail = typeof args.tail === 'number' ? args.tail : DEFAULT_TAIL;
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;
29
33
  const log = processRegistry.getLog(id, tail);
30
34
  if (!log) {
31
35
  return {
32
36
  success: true,
33
- output: `${t('proc.log_header', { id, status })} ${t('proc.log_empty')}`,
37
+ output: `${t("proc.log_header", { id, status })} ${t("proc.log_empty")}`,
34
38
  };
35
39
  }
36
40
  return {
37
41
  success: true,
38
- output: `${t('proc.log_header', { id, status })}\n${log}`,
42
+ output: `${t("proc.log_header", { id, status })}\n${log}`,
39
43
  };
40
44
  },
41
45
  };
@@ -28,9 +28,7 @@ function normalizeQuestions(args) {
28
28
  const q = item;
29
29
  if (typeof q.question !== "string" || !q.question.trim())
30
30
  continue;
31
- const options = Array.isArray(q.options)
32
- ? q.options.filter(isOption)
33
- : undefined;
31
+ const options = Array.isArray(q.options) ? q.options.filter(isOption) : undefined;
34
32
  // An explicitly provided options array with no valid entries is a
35
33
  // malformed call — skip the question instead of blocking on stdin.
36
34
  if (Array.isArray(q.options) && options.length === 0)
@@ -5,12 +5,20 @@ import { isPathInScope } from "../modules/security/path-validator";
5
5
  import { DEFAULT_SECURITY_CONFIG } from "../config/security";
6
6
  import { logSecurityBlock } from "../modules/security/audit-log";
7
7
  import { safeResolvePath } from "./path-utils";
8
- import { MAX_PREVIEW_LINES } from "./preview";
9
- const DEFAULT_LIMIT = MAX_PREVIEW_LINES;
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;
10
17
  export const readFileTool = {
11
18
  name: "read_file",
12
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.`,
13
20
  tags: ["file", "code"],
21
+ boundedOutput: true,
14
22
  parameters: {
15
23
  type: "object",
16
24
  properties: {
@@ -1,30 +1,30 @@
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'];
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
6
  export const recallTool = {
7
- name: 'recall',
8
- description: 'Recall stored information from memory. Search across preferences, facts, conventions, decisions, errors.',
9
- tags: ['memory'],
7
+ name: "recall",
8
+ description: "Recall stored information from memory. Search across preferences, facts, conventions, decisions, errors.",
9
+ tags: ["memory"],
10
10
  parameters: {
11
- type: 'object',
11
+ type: "object",
12
12
  properties: {
13
13
  query: {
14
- type: 'string',
15
- description: 'Search query (full-text search across all memory)',
14
+ type: "string",
15
+ description: "Search query (full-text search across all memory)",
16
16
  },
17
17
  category: {
18
- type: 'string',
19
- description: 'Limit to specific category: preferences, conventions, decisions, errors, facts',
18
+ type: "string",
19
+ description: "Limit to specific category: preferences, conventions, decisions, errors, facts",
20
20
  enum: CATEGORIES,
21
21
  },
22
22
  },
23
23
  },
24
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');
25
+ const query = args.query ? String(args.query) : "";
26
+ const category = args.category ? String(args.category) : "";
27
+ const memoryDir = join(homedir(), ".mma", "memory");
28
28
  const store = new MemoryStore(memoryDir);
29
29
  try {
30
30
  // No query, no category → show everything
@@ -39,20 +39,26 @@ export const recallTool = {
39
39
  if (category) {
40
40
  const results = searchCategory(store, category, query);
41
41
  if (results.length === 0) {
42
- return { success: true, output: t('tool.recall.empty', { query }) };
42
+ return { success: true, output: t("tool.recall.empty", { query }) };
43
43
  }
44
- return { success: true, output: t('tool.recall.search_results', { category, results: results.join('\n') }) };
44
+ return {
45
+ success: true,
46
+ output: t("tool.recall.search_results", { category, results: results.join("\n") }),
47
+ };
45
48
  }
46
49
  // Query across all
47
50
  const results = store.search(query);
48
51
  if (results.length === 0) {
49
- return { success: true, output: t('tool.recall.empty', { query }) };
52
+ return { success: true, output: t("tool.recall.empty", { query }) };
50
53
  }
51
- const formatted = results.map(r => `[${r.file}] ${r.match}`).join('\n');
52
- return { success: true, output: t('tool.recall.search_results', { category: 'all', results: formatted }) };
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
+ };
53
59
  }
54
60
  catch (err) {
55
- return { success: true, output: t('tool.memory_error', { error: String(err) }) };
61
+ return { success: true, output: t("tool.memory_error", { error: String(err) }) };
56
62
  }
57
63
  },
58
64
  };
@@ -60,14 +66,14 @@ function formatAll(store) {
60
66
  const parts = [];
61
67
  const prefs = store.getPreferences();
62
68
  if (Object.keys(prefs).length > 0) {
63
- parts.push('Preferences:');
69
+ parts.push("Preferences:");
64
70
  for (const [k, v] of Object.entries(prefs)) {
65
71
  parts.push(` ${k} = ${v}`);
66
72
  }
67
73
  }
68
- for (const cat of ['facts', 'conventions', 'decisions', 'errors']) {
74
+ for (const cat of ["facts", "conventions", "decisions", "errors"]) {
69
75
  const content = store.read(cat);
70
- const entries = content.split('\n').filter(l => l.startsWith('- '));
76
+ const entries = content.split("\n").filter((l) => l.startsWith("- "));
71
77
  if (entries.length > 0) {
72
78
  parts.push(`\n${cat.charAt(0).toUpperCase() + cat.slice(1)} (last 5):`);
73
79
  for (const e of entries.slice(-5)) {
@@ -75,27 +81,27 @@ function formatAll(store) {
75
81
  }
76
82
  }
77
83
  }
78
- return parts.length > 0 ? parts.join('\n') : t('tool.recall.no_memory');
84
+ return parts.length > 0 ? parts.join("\n") : t("tool.recall.no_memory");
79
85
  }
80
86
  function formatCategory(store, category) {
81
- if (category === 'preferences') {
87
+ if (category === "preferences") {
82
88
  const prefs = store.getPreferences();
83
89
  if (Object.keys(prefs).length === 0)
84
- return t('tool.recall.no_memory');
85
- const lines = ['Preferences:'];
90
+ return t("tool.recall.no_memory");
91
+ const lines = ["Preferences:"];
86
92
  for (const [k, v] of Object.entries(prefs)) {
87
93
  lines.push(` ${k} = ${v}`);
88
94
  }
89
- return lines.join('\n');
95
+ return lines.join("\n");
90
96
  }
91
97
  const content = store.read(category);
92
- const entries = content.split('\n').filter(l => l.startsWith('- '));
98
+ const entries = content.split("\n").filter((l) => l.startsWith("- "));
93
99
  if (entries.length === 0)
94
- return t('tool.recall.no_memory');
95
- return `${category.charAt(0).toUpperCase() + category.slice(1)} (${entries.length} entries):\n${entries.join('\n')}`;
100
+ return t("tool.recall.no_memory");
101
+ return `${category.charAt(0).toUpperCase() + category.slice(1)} (${entries.length} entries):\n${entries.join("\n")}`;
96
102
  }
97
103
  function searchCategory(store, category, query) {
98
- if (category === 'preferences') {
104
+ if (category === "preferences") {
99
105
  const prefs = store.getPreferences();
100
106
  const lower = query.toLowerCase();
101
107
  return Object.entries(prefs)
@@ -104,7 +110,8 @@ function searchCategory(store, category, query) {
104
110
  }
105
111
  const content = store.read(category);
106
112
  const lower = query.toLowerCase();
107
- return content.split('\n')
108
- .filter(l => l.startsWith('- ') && l.toLowerCase().includes(lower))
109
- .map(l => ` ${l}`);
113
+ return content
114
+ .split("\n")
115
+ .filter((l) => l.startsWith("- ") && l.toLowerCase().includes(lower))
116
+ .map((l) => ` ${l}`);
110
117
  }
@@ -19,18 +19,29 @@ export class ToolRegistry {
19
19
  return Array.from(this.tools.values());
20
20
  }
21
21
  getByTags(tags) {
22
- return this.getAll().filter(t => {
22
+ return this.getAll().filter((t) => {
23
23
  if (!t.tags || t.tags.length === 0)
24
24
  return false;
25
- return tags.some(tag => t.tags.includes(tag));
25
+ return tags.some((tag) => t.tags.includes(tag));
26
26
  });
27
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
+ */
28
34
  getAllForLLM(tags) {
29
- const tools = tags ? this.getByTags(tags) : this.getAll();
30
- return tools.map(t => ({
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) => ({
31
41
  name: t.name,
32
42
  description: t.description,
33
43
  parameters: t.parameters,
44
+ boundedOutput: t.boundedOutput,
34
45
  }));
35
46
  }
36
47
  }
@@ -1,67 +1,67 @@
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'];
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
6
  export const rememberTool = {
7
- name: 'remember',
8
- description: 'Remember information across sessions. Use for user preferences, facts, conventions, decisions, or errors.',
9
- tags: ['memory'],
7
+ name: "remember",
8
+ description: "Remember information across sessions. Use for user preferences, facts, conventions, decisions, or errors.",
9
+ tags: ["memory"],
10
10
  parameters: {
11
- type: 'object',
11
+ type: "object",
12
12
  properties: {
13
13
  category: {
14
- type: 'string',
15
- description: 'Memory category: preferences, conventions, decisions, errors, facts',
14
+ type: "string",
15
+ description: "Memory category: preferences, conventions, decisions, errors, facts",
16
16
  enum: CATEGORIES,
17
17
  },
18
18
  key: {
19
- type: 'string',
19
+ type: "string",
20
20
  description: 'Key name (required for preferences, e.g. "color", "language")',
21
21
  },
22
22
  value: {
23
- type: 'string',
24
- description: 'Value to store (required for preferences)',
23
+ type: "string",
24
+ description: "Value to store (required for preferences)",
25
25
  },
26
26
  entry: {
27
- type: 'string',
28
- description: 'Text entry to append (for conventions, decisions, errors, facts)',
27
+ type: "string",
28
+ description: "Text entry to append (for conventions, decisions, errors, facts)",
29
29
  },
30
30
  },
31
- required: ['category'],
31
+ required: ["category"],
32
32
  },
33
33
  handler: async (_ctx, args) => {
34
- const category = String(args.category || '');
34
+ const category = String(args.category || "");
35
35
  if (!CATEGORIES.includes(category)) {
36
- return { success: false, output: t('tool.invalid_params') };
36
+ return { success: false, output: t("tool.invalid_params") };
37
37
  }
38
- const memoryDir = join(homedir(), '.mma', 'memory');
38
+ const memoryDir = join(homedir(), ".mma", "memory");
39
39
  const store = new MemoryStore(memoryDir);
40
40
  try {
41
- if (category === 'preferences') {
42
- const key = String(args.key || '');
43
- const value = String(args.value || '');
41
+ if (category === "preferences") {
42
+ const key = String(args.key || "");
43
+ const value = String(args.value || "");
44
44
  if (!key) {
45
- return { success: false, output: t('tool.remember.key_required') };
45
+ return { success: false, output: t("tool.remember.key_required") };
46
46
  }
47
47
  store.setPreference(key, value);
48
48
  return {
49
49
  success: true,
50
- output: t('tool.remember.preference', { key, value }),
50
+ output: t("tool.remember.preference", { key, value }),
51
51
  };
52
52
  }
53
- const entry = String(args.entry || '');
53
+ const entry = String(args.entry || "");
54
54
  if (!entry) {
55
- return { success: false, output: t('tool.remember.entry_required') };
55
+ return { success: false, output: t("tool.remember.entry_required") };
56
56
  }
57
57
  store.append(category, entry);
58
58
  return {
59
59
  success: true,
60
- output: t('tool.remember.entry', { category, entry }),
60
+ output: t("tool.remember.entry", { category, entry }),
61
61
  };
62
62
  }
63
63
  catch (err) {
64
- return { success: true, output: t('tool.memory_error', { error: String(err) }) };
64
+ return { success: true, output: t("tool.memory_error", { error: String(err) }) };
65
65
  }
66
66
  },
67
67
  };
@@ -1,30 +1,30 @@
1
- import { resolve, normalize } from 'path';
1
+ import { resolve, normalize } from "path";
2
2
  export function isPathInScope(baseDir, targetPath, scope) {
3
3
  const baseResolved = resolve(baseDir);
4
4
  const targetResolved = resolve(baseDir, normalize(targetPath));
5
5
  if (!targetResolved.startsWith(baseResolved)) {
6
- return { allowed: false, reason: 'Path is outside the base working directory' };
6
+ return { allowed: false, reason: "Path is outside the base working directory" };
7
7
  }
8
8
  if (!scope)
9
9
  return { allowed: true };
10
- const isRead = scope.read_only_files.some(f => targetResolved.startsWith(resolve(baseDir, f)));
11
- const isWrite = scope.allowed_files.some(f => targetResolved.startsWith(resolve(baseDir, f)));
10
+ const isRead = scope.read_only_files.some((f) => targetResolved.startsWith(resolve(baseDir, f)));
11
+ const isWrite = scope.allowed_files.some((f) => targetResolved.startsWith(resolve(baseDir, f)));
12
12
  if (isWrite)
13
13
  return { allowed: true };
14
14
  if (isRead)
15
- return { allowed: true, reason: 'Read-only file' };
16
- return { allowed: false, reason: 'Path is not within the allowed scope for this sub-agent' };
15
+ return { allowed: true, reason: "Read-only file" };
16
+ return { allowed: false, reason: "Path is not within the allowed scope for this sub-agent" };
17
17
  }
18
18
  export function isPathWritable(baseDir, targetPath, scope) {
19
19
  const baseResolved = resolve(baseDir);
20
20
  const targetResolved = resolve(baseDir, normalize(targetPath));
21
21
  if (!targetResolved.startsWith(baseResolved)) {
22
- return { allowed: false, reason: 'Path is outside the base working directory' };
22
+ return { allowed: false, reason: "Path is outside the base working directory" };
23
23
  }
24
24
  if (!scope)
25
25
  return { allowed: true };
26
- const isWrite = scope.allowed_files.some(f => targetResolved.startsWith(resolve(baseDir, f)));
26
+ const isWrite = scope.allowed_files.some((f) => targetResolved.startsWith(resolve(baseDir, f)));
27
27
  if (isWrite)
28
28
  return { allowed: true };
29
- return { allowed: false, reason: 'Path is not within the writable scope for this sub-agent' };
29
+ return { allowed: false, reason: "Path is not within the writable scope for this sub-agent" };
30
30
  }