micro-models-agent 0.57.0 → 0.57.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 (232) hide show
  1. package/CHANGELOG.md +481 -476
  2. package/README.md +358 -358
  3. package/dist/certification/certifications.json +493 -493
  4. package/dist/cli/commands.js +447 -0
  5. package/dist/cli/completer.js +167 -0
  6. package/dist/cli/index.js +2 -0
  7. package/dist/cli/main.js +153 -0
  8. package/dist/cli/plugin-commands.js +36 -0
  9. package/dist/cli/repl-commands.js +761 -0
  10. package/dist/cli/repl.js +702 -0
  11. package/dist/cli/run-result.js +33 -0
  12. package/dist/cli/security-commands.js +164 -0
  13. package/dist/cli/setup.js +237 -0
  14. package/dist/config/config.js +276 -0
  15. package/dist/config/defaults.js +141 -0
  16. package/dist/config/domains.js +179 -0
  17. package/dist/config/experts.js +15 -0
  18. package/dist/config/index.js +4 -0
  19. package/dist/config/security.js +213 -0
  20. package/dist/config/types.js +1 -0
  21. package/dist/core/agent-moe.js +102 -0
  22. package/dist/core/agent.js +1018 -0
  23. package/dist/core/bootstrap.js +481 -0
  24. package/dist/core/crash-handler.js +51 -0
  25. package/dist/core/environment.js +199 -0
  26. package/dist/core/index.js +2 -0
  27. package/dist/core/prompt-builder.js +76 -0
  28. package/dist/core/session-logger.js +251 -0
  29. package/dist/core/types.js +1 -0
  30. package/dist/core/version.js +26 -0
  31. package/dist/core/workspace.js +76 -0
  32. package/dist/i18n/en.json +679 -0
  33. package/dist/i18n/index.js +46 -0
  34. package/dist/i18n/ru.json +679 -0
  35. package/dist/index.js +22 -0
  36. package/dist/llm/image-utils.js +143 -0
  37. package/dist/llm/index.js +4 -0
  38. package/dist/llm/model-loader.js +78 -0
  39. package/dist/llm/openai-compat.js +497 -0
  40. package/dist/llm/orchestrator.js +200 -0
  41. package/dist/llm/provider.js +10 -0
  42. package/dist/llm/response.js +39 -0
  43. package/dist/llm/token-counter.js +39 -0
  44. package/dist/llm/types.js +1 -0
  45. package/dist/logger/app-logger.js +189 -0
  46. package/dist/logger/file-log.js +151 -0
  47. package/dist/logger/index.js +1 -0
  48. package/dist/migration/backup.js +45 -0
  49. package/dist/migration/detect.js +50 -0
  50. package/dist/migration/index.js +2 -0
  51. package/dist/modules/artifacts/store.js +61 -0
  52. package/dist/modules/browser/actions.js +76 -0
  53. package/dist/modules/browser/bridge-client.js +199 -0
  54. package/dist/modules/browser/bridge-path.js +10 -0
  55. package/dist/modules/browser/bridge-server.mjs +219 -219
  56. package/dist/modules/browser/cookie-store.js +24 -0
  57. package/dist/modules/browser/driver.js +136 -0
  58. package/dist/modules/browser/index.js +7 -0
  59. package/dist/modules/browser/module.js +29 -0
  60. package/dist/modules/browser/session.js +342 -0
  61. package/dist/modules/browser/snapshot.js +148 -0
  62. package/dist/modules/browser/types.js +12 -0
  63. package/dist/modules/certification/cli.js +213 -0
  64. package/dist/modules/certification/fact-checker.js +82 -0
  65. package/dist/modules/certification/loader.js +106 -0
  66. package/dist/modules/certification/manifest.js +58 -0
  67. package/dist/modules/certification/runner.js +245 -0
  68. package/dist/modules/certification/scenarios.js +407 -0
  69. package/dist/modules/certification/types.js +1 -0
  70. package/dist/modules/context/chunk-query.js +100 -0
  71. package/dist/modules/context/fact-extractor.js +168 -0
  72. package/dist/modules/context/history.js +15 -0
  73. package/dist/modules/context/index.js +1 -0
  74. package/dist/modules/context/manager.js +440 -0
  75. package/dist/modules/execution/audit-runners.js +206 -0
  76. package/dist/modules/execution/auditor.js +218 -0
  77. package/dist/modules/execution/execution-plugin.js +431 -0
  78. package/dist/modules/execution/index.js +8 -0
  79. package/dist/modules/execution/module.js +625 -0
  80. package/dist/modules/execution/moe-executor.js +304 -0
  81. package/dist/modules/execution/plan-coverage.js +68 -0
  82. package/dist/modules/execution/plan-persister.js +46 -0
  83. package/dist/modules/execution/plan-store.js +196 -0
  84. package/dist/modules/execution/plan-tool.js +677 -0
  85. package/dist/modules/execution/plan-validator.js +153 -0
  86. package/dist/modules/execution/planner.js +94 -0
  87. package/dist/modules/execution/stuck-detector.js +746 -0
  88. package/dist/modules/execution/tracker.js +69 -0
  89. package/dist/modules/execution/types.js +1 -0
  90. package/dist/modules/execution/verifier.js +235 -0
  91. package/dist/modules/execution/windows-commands.js +41 -0
  92. package/dist/modules/hallucination/confidence.js +66 -0
  93. package/dist/modules/hallucination/consistency.js +26 -0
  94. package/dist/modules/hallucination/detector.js +47 -0
  95. package/dist/modules/hallucination/factual.js +169 -0
  96. package/dist/modules/hallucination/index.js +5 -0
  97. package/dist/modules/hallucination/js-identifiers.js +262 -0
  98. package/dist/modules/hallucination/llm-judge.js +101 -0
  99. package/dist/modules/index.js +5 -0
  100. package/dist/modules/indexer/cache.js +40 -0
  101. package/dist/modules/indexer/index.js +3 -0
  102. package/dist/modules/indexer/module.js +246 -0
  103. package/dist/modules/indexer/project-profile.js +183 -0
  104. package/dist/modules/indexer/walker.js +101 -0
  105. package/dist/modules/lsp/check-tool.js +58 -0
  106. package/dist/modules/lsp/client.js +389 -0
  107. package/dist/modules/lsp/command.js +60 -0
  108. package/dist/modules/lsp/config.js +135 -0
  109. package/dist/modules/lsp/index.js +3 -0
  110. package/dist/modules/lsp/module.js +260 -0
  111. package/dist/modules/lsp/probe.js +86 -0
  112. package/dist/modules/lsp/project-root.js +32 -0
  113. package/dist/modules/lsp/startup-check.js +144 -0
  114. package/dist/modules/lsp/types.js +1 -0
  115. package/dist/modules/mcp/client.js +399 -0
  116. package/dist/modules/mcp/index.js +3 -0
  117. package/dist/modules/mcp/module.js +142 -0
  118. package/dist/modules/mcp/registry.js +15 -0
  119. package/dist/modules/memory/index.js +1 -0
  120. package/dist/modules/memory/module.js +96 -0
  121. package/dist/modules/memory/search.js +42 -0
  122. package/dist/modules/memory/store.js +69 -0
  123. package/dist/modules/pipelines/engine.js +60 -0
  124. package/dist/modules/pipelines/index.js +3 -0
  125. package/dist/modules/pipelines/parser.js +56 -0
  126. package/dist/modules/pipelines/template.js +14 -0
  127. package/dist/modules/plugins/builtin/lint-on-write.js +334 -0
  128. package/dist/modules/plugins/builtin/notify.js +9 -0
  129. package/dist/modules/plugins/index.js +1 -0
  130. package/dist/modules/plugins/loader.js +70 -0
  131. package/dist/modules/plugins/manager.js +261 -0
  132. package/dist/modules/plugins/types.js +1 -0
  133. package/dist/modules/pricing/index.js +61 -0
  134. package/dist/modules/pricing/prices.js +129 -0
  135. package/dist/modules/processes/detect.js +34 -0
  136. package/dist/modules/processes/index.js +2 -0
  137. package/dist/modules/processes/registry.js +327 -0
  138. package/dist/modules/processes/runner.js +23 -0
  139. package/dist/modules/providers/create.js +22 -0
  140. package/dist/modules/providers/fallback.js +79 -0
  141. package/dist/modules/providers/health.js +46 -0
  142. package/dist/modules/providers/index.js +5 -0
  143. package/dist/modules/providers/manager.js +161 -0
  144. package/dist/modules/providers/presets.js +128 -0
  145. package/dist/modules/providers/registry.js +22 -0
  146. package/dist/modules/providers/types.js +1 -0
  147. package/dist/modules/registry.js +48 -0
  148. package/dist/modules/security/audit-log.js +136 -0
  149. package/dist/modules/security/audit-notifier.js +292 -0
  150. package/dist/modules/security/command-validator.js +219 -0
  151. package/dist/modules/security/content-scanner.js +53 -0
  152. package/dist/modules/security/data-sanitizer.js +89 -0
  153. package/dist/modules/security/encryption.js +242 -0
  154. package/dist/modules/security/index.js +14 -0
  155. package/dist/modules/security/network-validator.js +88 -0
  156. package/dist/modules/security/path-validator.js +203 -0
  157. package/dist/modules/security/rate-limiter.js +119 -0
  158. package/dist/modules/security/security-policies.js +531 -0
  159. package/dist/modules/security/session-encryption.js +210 -0
  160. package/dist/modules/security/session-isolation.js +95 -0
  161. package/dist/modules/session/index.js +3 -0
  162. package/dist/modules/session/manager.js +172 -0
  163. package/dist/modules/session/module.js +24 -0
  164. package/dist/modules/session/store.js +222 -0
  165. package/dist/modules/session/types.js +1 -0
  166. package/dist/modules/skills/index.js +2 -0
  167. package/dist/modules/skills/loader.js +72 -0
  168. package/dist/modules/skills/matcher.js +27 -0
  169. package/dist/modules/skills/module.js +129 -0
  170. package/dist/modules/types.js +1 -0
  171. package/dist/modules/updater/checker.js +96 -0
  172. package/dist/modules/updater/index.js +2 -0
  173. package/dist/modules/updater/module.js +116 -0
  174. package/dist/modules/user-profile/compressor.js +16 -0
  175. package/dist/modules/user-profile/index.js +1 -0
  176. package/dist/modules/user-profile/profile.js +68 -0
  177. package/dist/skills/builtin/git.md +36 -36
  178. package/dist/skills/builtin/typescript.md +35 -35
  179. package/dist/tools/approve.js +33 -0
  180. package/dist/tools/attach-image.js +101 -0
  181. package/dist/tools/bash.js +519 -0
  182. package/dist/tools/browser.js +115 -0
  183. package/dist/tools/chunk-query.js +100 -0
  184. package/dist/tools/create-dir.js +56 -0
  185. package/dist/tools/delete-file.js +63 -0
  186. package/dist/tools/download-file.js +117 -0
  187. package/dist/tools/edit-file.js +80 -0
  188. package/dist/tools/enable-tools.js +59 -0
  189. package/dist/tools/executor.js +154 -0
  190. package/dist/tools/file-info.js +47 -0
  191. package/dist/tools/filter-tools.js +17 -0
  192. package/dist/tools/glob-tool.js +27 -0
  193. package/dist/tools/grep-tool.js +125 -0
  194. package/dist/tools/hidden-tools-block.js +37 -0
  195. package/dist/tools/index.js +78 -0
  196. package/dist/tools/list-dir.js +49 -0
  197. package/dist/tools/load-skill.js +43 -0
  198. package/dist/tools/mcp-call.js +69 -0
  199. package/dist/tools/move-file.js +86 -0
  200. package/dist/tools/path-utils.js +101 -0
  201. package/dist/tools/pipeline-run.js +145 -0
  202. package/dist/tools/preview.js +2 -0
  203. package/dist/tools/process-kill.js +40 -0
  204. package/dist/tools/process-list.js +37 -0
  205. package/dist/tools/process-log.js +54 -0
  206. package/dist/tools/question.js +141 -0
  207. package/dist/tools/read-file.js +179 -0
  208. package/dist/tools/recall.js +118 -0
  209. package/dist/tools/registry.js +47 -0
  210. package/dist/tools/remember.js +68 -0
  211. package/dist/tools/scope-check.js +32 -0
  212. package/dist/tools/search-history.js +85 -0
  213. package/dist/tools/subagent.js +196 -0
  214. package/dist/tools/types.js +1 -0
  215. package/dist/tools/user-input.js +123 -0
  216. package/dist/tools/web-browse.js +87 -0
  217. package/dist/tools/web-fetch.js +119 -0
  218. package/dist/tools/web-search.js +105 -0
  219. package/dist/tools/write-file.js +82 -0
  220. package/dist/ui/box.js +77 -0
  221. package/dist/ui/colors.js +4 -0
  222. package/dist/ui/diff.js +178 -0
  223. package/dist/ui/index.js +6 -0
  224. package/dist/ui/line-editor.js +822 -0
  225. package/dist/ui/line-math.js +73 -0
  226. package/dist/ui/md-formatter.js +212 -0
  227. package/dist/ui/output.js +13 -0
  228. package/dist/ui/plan-view.js +103 -0
  229. package/dist/ui/renderer.js +259 -0
  230. package/dist/ui/spinner.js +70 -0
  231. package/dist/ui/table.js +144 -0
  232. package/package.json +51 -51
@@ -0,0 +1,179 @@
1
+ import { readFileSync, existsSync, statSync, openSync, readSync, closeSync } 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
+ /**
18
+ * Files larger than this are read through a bounded streaming path instead of
19
+ * readFileSync (which loads the whole file into memory just to slice a few
20
+ * hundred lines).
21
+ */
22
+ const LARGE_FILE_BYTES = 1024 * 1024;
23
+ const STREAM_CHUNK = 256 * 1024;
24
+ /**
25
+ * Bounded line-slice read for large files: streams the file in chunks,
26
+ * collects only the requested [offset, offset+limit) window, then counts the
27
+ * remaining newlines raw (no decode) to produce the total. Memory stays
28
+ * bounded regardless of file size.
29
+ */
30
+ function readLineSlice(path, offset, limit) {
31
+ const fd = openSync(path, "r");
32
+ try {
33
+ const buf = Buffer.alloc(STREAM_CHUNK);
34
+ let pos = 0;
35
+ let carry = "";
36
+ let lineNumber = 1;
37
+ let total = 0;
38
+ const collected = [];
39
+ const endLine = offset + limit; // exclusive
40
+ while (true) {
41
+ const bytes = readSync(fd, buf, 0, STREAM_CHUNK, pos);
42
+ if (bytes === 0)
43
+ break;
44
+ pos += bytes;
45
+ const chunk = carry + buf.toString("utf-8", 0, bytes);
46
+ const parts = chunk.split("\n");
47
+ carry = parts.pop() ?? "";
48
+ for (const line of parts) {
49
+ total++;
50
+ if (lineNumber >= offset && lineNumber < endLine)
51
+ collected.push(line);
52
+ lineNumber++;
53
+ }
54
+ if (lineNumber >= endLine && collected.length >= limit) {
55
+ // Window complete — count remaining newlines without decoding.
56
+ const rest = [];
57
+ while (true) {
58
+ const n = readSync(fd, buf, 0, STREAM_CHUNK, pos);
59
+ if (n === 0)
60
+ break;
61
+ pos += n;
62
+ rest.push(Buffer.from(buf.subarray(0, n)));
63
+ }
64
+ const tail = Buffer.concat([...rest, Buffer.from(carry, "utf-8")]);
65
+ for (let i = 0; i < tail.length; i++)
66
+ if (tail[i] === 0x0a)
67
+ total++;
68
+ total++; // last line has no trailing newline
69
+ return { selected: collected.join("\n"), total };
70
+ }
71
+ }
72
+ if (carry.length > 0 || total === 0) {
73
+ total++;
74
+ if (lineNumber >= offset && lineNumber < endLine)
75
+ collected.push(carry);
76
+ }
77
+ return { selected: collected.join("\n"), total };
78
+ }
79
+ finally {
80
+ closeSync(fd);
81
+ }
82
+ }
83
+ export const readFileTool = {
84
+ name: "read_file",
85
+ icon: "👀",
86
+ 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.`,
87
+ tags: ["file", "code"],
88
+ boundedOutput: true,
89
+ parameters: {
90
+ type: "object",
91
+ properties: {
92
+ path: { type: "string", description: "File path to read" },
93
+ offset: {
94
+ type: "number",
95
+ description: `Starting line (1-indexed). Use offset=<next> from a truncated result to continue reading.`,
96
+ },
97
+ limit: {
98
+ type: "number",
99
+ description: `Number of lines to read (default ${DEFAULT_LIMIT})`,
100
+ },
101
+ },
102
+ required: ["path"],
103
+ },
104
+ handler: async (ctx, args) => {
105
+ const path = String(args.path);
106
+ const resolved = safeResolvePath(ctx.baseDir, path);
107
+ const securityPaths = ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths;
108
+ const scopeCheck = isPathInScope(ctx.baseDir, resolved, ctx.scope, securityPaths);
109
+ if (!scopeCheck.allowed) {
110
+ const pathStr = path;
111
+ logSecurityBlock(ctx.sessionId || undefined, "file_read", scopeCheck.reason || "Path not allowed", pathStr);
112
+ return {
113
+ success: false,
114
+ output: t("file.path_not_allowed", {
115
+ path: `${path} — ${scopeCheck.reason}`,
116
+ }),
117
+ };
118
+ }
119
+ if (!existsSync(resolved)) {
120
+ // For relative paths, tell the model WHERE the path resolved — the
121
+ // "not found" alone is confusing (it cannot tell baseDir from cwd).
122
+ const output = resolved !== path
123
+ ? t("file.notfound_resolved", {
124
+ path,
125
+ resolved,
126
+ })
127
+ : t("file.notfound", { path });
128
+ return { success: false, output };
129
+ }
130
+ const offset = args.offset || 1;
131
+ const limit = args.limit || DEFAULT_LIMIT;
132
+ let lines;
133
+ let total;
134
+ let selected;
135
+ if (statSync(resolved).size > LARGE_FILE_BYTES) {
136
+ const slice = readLineSlice(resolved, offset, limit);
137
+ total = slice.total;
138
+ selected = slice.selected;
139
+ }
140
+ else {
141
+ const content = readFileSync(resolved, "utf-8");
142
+ lines = content.split("\n");
143
+ total = lines.length;
144
+ const end = Math.min(offset - 1 + limit, total);
145
+ selected = lines.slice(offset - 1, end).join("\n");
146
+ }
147
+ const end = Math.min(offset - 1 + limit, total);
148
+ const ext = extname(resolved) || "(no extension)";
149
+ const header = t("file.read_header", {
150
+ path,
151
+ ext,
152
+ total: String(total),
153
+ from: String(offset),
154
+ to: String(end),
155
+ });
156
+ const truncated = end < total
157
+ ? t("file.read_truncated", {
158
+ remaining: String(total - end),
159
+ next: String(end + 1),
160
+ })
161
+ : "";
162
+ // Compact user-facing display: the tool header already shows the path,
163
+ // so only a partial read needs extra info (the range within the file).
164
+ // A full read adds nothing — keeps the card to two lines.
165
+ const fullRead = offset === 1 && end >= total;
166
+ const display = fullRead
167
+ ? ""
168
+ : t("file.read_range", {
169
+ from: String(offset),
170
+ to: String(end),
171
+ total: String(total),
172
+ });
173
+ return {
174
+ success: true,
175
+ output: `${header}\n${selected}${truncated}`,
176
+ display,
177
+ };
178
+ },
179
+ };
@@ -0,0 +1,118 @@
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
+ icon: "💾",
9
+ description: "Recall stored information from memory. Search across preferences, facts, conventions, decisions, errors.",
10
+ tags: ["memory"],
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ query: {
15
+ type: "string",
16
+ description: "Search query (full-text search across all memory)",
17
+ },
18
+ category: {
19
+ type: "string",
20
+ description: "Limit to specific category: preferences, conventions, decisions, errors, facts",
21
+ enum: CATEGORIES,
22
+ },
23
+ },
24
+ },
25
+ handler: async (_ctx, args) => {
26
+ const query = args.query ? String(args.query) : "";
27
+ const category = args.category ? String(args.category) : "";
28
+ const memoryDir = join(homedir(), ".mma", "memory");
29
+ const store = new MemoryStore(memoryDir);
30
+ try {
31
+ // No query, no category → show everything
32
+ if (!query && !category) {
33
+ return { success: true, output: formatAll(store) };
34
+ }
35
+ // Category only → show that category
36
+ if (!query && category) {
37
+ return { success: true, output: formatCategory(store, category) };
38
+ }
39
+ // Query with optional category → search
40
+ if (category) {
41
+ const results = searchCategory(store, category, query);
42
+ if (results.length === 0) {
43
+ return { success: true, output: t("tool.recall.empty", { query }) };
44
+ }
45
+ return {
46
+ success: true,
47
+ output: t("tool.recall.search_results", { category, results: results.join("\n") }),
48
+ };
49
+ }
50
+ // Query across all
51
+ const results = store.search(query);
52
+ if (results.length === 0) {
53
+ return { success: true, output: t("tool.recall.empty", { query }) };
54
+ }
55
+ const formatted = results.map((r) => `[${r.file}] ${r.match}`).join("\n");
56
+ return {
57
+ success: true,
58
+ output: t("tool.recall.search_results", { category: "all", results: formatted }),
59
+ };
60
+ }
61
+ catch (err) {
62
+ return { success: true, output: t("tool.memory_error", { error: String(err) }) };
63
+ }
64
+ },
65
+ };
66
+ function formatAll(store) {
67
+ const parts = [];
68
+ const prefs = store.getPreferences();
69
+ if (Object.keys(prefs).length > 0) {
70
+ parts.push("Preferences:");
71
+ for (const [k, v] of Object.entries(prefs)) {
72
+ parts.push(` ${k} = ${v}`);
73
+ }
74
+ }
75
+ for (const cat of ["facts", "conventions", "decisions", "errors"]) {
76
+ const content = store.read(cat);
77
+ const entries = content.split("\n").filter((l) => l.startsWith("- "));
78
+ if (entries.length > 0) {
79
+ parts.push(`\n${cat.charAt(0).toUpperCase() + cat.slice(1)} (last 5):`);
80
+ for (const e of entries.slice(-5)) {
81
+ parts.push(` ${e}`);
82
+ }
83
+ }
84
+ }
85
+ return parts.length > 0 ? parts.join("\n") : t("tool.recall.no_memory");
86
+ }
87
+ function formatCategory(store, category) {
88
+ if (category === "preferences") {
89
+ const prefs = store.getPreferences();
90
+ if (Object.keys(prefs).length === 0)
91
+ return t("tool.recall.no_memory");
92
+ const lines = ["Preferences:"];
93
+ for (const [k, v] of Object.entries(prefs)) {
94
+ lines.push(` ${k} = ${v}`);
95
+ }
96
+ return lines.join("\n");
97
+ }
98
+ const content = store.read(category);
99
+ const entries = content.split("\n").filter((l) => l.startsWith("- "));
100
+ if (entries.length === 0)
101
+ return t("tool.recall.no_memory");
102
+ return `${category.charAt(0).toUpperCase() + category.slice(1)} (${entries.length} entries):\n${entries.join("\n")}`;
103
+ }
104
+ function searchCategory(store, category, query) {
105
+ if (category === "preferences") {
106
+ const prefs = store.getPreferences();
107
+ const lower = query.toLowerCase();
108
+ return Object.entries(prefs)
109
+ .filter(([k, v]) => k.toLowerCase().includes(lower) || v.toLowerCase().includes(lower))
110
+ .map(([k, v]) => ` ${k} = ${v}`);
111
+ }
112
+ const content = store.read(category);
113
+ const lower = query.toLowerCase();
114
+ return content
115
+ .split("\n")
116
+ .filter((l) => l.startsWith("- ") && l.toLowerCase().includes(lower))
117
+ .map((l) => ` ${l}`);
118
+ }
@@ -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
+ }
@@ -0,0 +1,68 @@
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 rememberTool = {
7
+ name: "remember",
8
+ icon: "🧠",
9
+ description: "Remember information across sessions. Use for user preferences, facts, conventions, decisions, or errors.",
10
+ tags: ["memory"],
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ category: {
15
+ type: "string",
16
+ description: "Memory category: preferences, conventions, decisions, errors, facts",
17
+ enum: CATEGORIES,
18
+ },
19
+ key: {
20
+ type: "string",
21
+ description: 'Key name (required for preferences, e.g. "color", "language")',
22
+ },
23
+ value: {
24
+ type: "string",
25
+ description: "Value to store (required for preferences)",
26
+ },
27
+ entry: {
28
+ type: "string",
29
+ description: "Text entry to append (for conventions, decisions, errors, facts)",
30
+ },
31
+ },
32
+ required: ["category"],
33
+ },
34
+ handler: async (_ctx, args) => {
35
+ const category = String(args.category || "");
36
+ if (!CATEGORIES.includes(category)) {
37
+ return { success: false, output: t("tool.invalid_params") };
38
+ }
39
+ const memoryDir = join(homedir(), ".mma", "memory");
40
+ const store = new MemoryStore(memoryDir);
41
+ try {
42
+ if (category === "preferences") {
43
+ const key = String(args.key || "");
44
+ const value = String(args.value || "");
45
+ if (!key) {
46
+ return { success: false, output: t("tool.remember.key_required") };
47
+ }
48
+ store.setPreference(key, value);
49
+ return {
50
+ success: true,
51
+ output: t("tool.remember.preference", { key, value }),
52
+ };
53
+ }
54
+ const entry = String(args.entry || "");
55
+ if (!entry) {
56
+ return { success: false, output: t("tool.remember.entry_required") };
57
+ }
58
+ store.append(category, entry);
59
+ return {
60
+ success: true,
61
+ output: t("tool.remember.entry", { category, entry }),
62
+ };
63
+ }
64
+ catch (err) {
65
+ return { success: true, output: t("tool.memory_error", { error: String(err) }) };
66
+ }
67
+ },
68
+ };
@@ -0,0 +1,32 @@
1
+ import { resolve, normalize } from "path";
2
+ import { isInsideDir, matchesScopeEntry } from "./path-utils";
3
+ export function isPathInScope(baseDir, targetPath, scope) {
4
+ const baseResolved = resolve(baseDir);
5
+ const targetResolved = resolve(baseDir, normalize(targetPath));
6
+ // Separator- and case-safe containment (sibling prefixes cannot pass).
7
+ if (!isInsideDir(targetResolved, baseResolved)) {
8
+ return { allowed: false, reason: "Path is outside the base working directory" };
9
+ }
10
+ if (!scope)
11
+ return { allowed: true };
12
+ const isRead = scope.read_only_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
13
+ const isWrite = scope.allowed_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
14
+ if (isWrite)
15
+ return { allowed: true };
16
+ if (isRead)
17
+ return { allowed: true, reason: "Read-only file" };
18
+ return { allowed: false, reason: "Path is not within the allowed scope for this sub-agent" };
19
+ }
20
+ export function isPathWritable(baseDir, targetPath, scope) {
21
+ const baseResolved = resolve(baseDir);
22
+ const targetResolved = resolve(baseDir, normalize(targetPath));
23
+ if (!isInsideDir(targetResolved, baseResolved)) {
24
+ return { allowed: false, reason: "Path is outside the base working directory" };
25
+ }
26
+ if (!scope)
27
+ return { allowed: true };
28
+ const isWrite = scope.allowed_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
29
+ if (isWrite)
30
+ return { allowed: true };
31
+ return { allowed: false, reason: "Path is not within the writable scope for this sub-agent" };
32
+ }
@@ -0,0 +1,85 @@
1
+ import * as fs from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { t } from "../i18n/index";
5
+ function searchFile(filePath, query, maxResults, results) {
6
+ if (!fs.existsSync(filePath))
7
+ return;
8
+ const content = fs.readFileSync(filePath, "utf-8");
9
+ for (const line of content.split("\n").filter(Boolean)) {
10
+ if (line.toLowerCase().includes(query)) {
11
+ try {
12
+ const entry = JSON.parse(line);
13
+ results.push(`[${filePath}] ${JSON.stringify(entry).slice(0, 200)}`);
14
+ }
15
+ catch {
16
+ /* skip */
17
+ }
18
+ }
19
+ if (results.length >= maxResults)
20
+ break;
21
+ }
22
+ }
23
+ export const searchHistoryTool = {
24
+ name: "search_history",
25
+ icon: "🕵️",
26
+ description: "Search through session history for past interactions across all sessions.",
27
+ tags: ["research"],
28
+ parameters: {
29
+ type: "object",
30
+ properties: {
31
+ query: { type: "string", description: "Search query" },
32
+ maxResults: {
33
+ type: "number",
34
+ description: "Maximum results (default 5)",
35
+ },
36
+ sessionId: {
37
+ type: "string",
38
+ description: "Optional: limit search to a specific session",
39
+ },
40
+ },
41
+ required: ["query"],
42
+ },
43
+ handler: async (_ctx, args) => {
44
+ const query = String(args.query || "").toLowerCase();
45
+ const maxResults = Number(args.maxResults) || 5;
46
+ const sessionId = args.sessionId ? String(args.sessionId) : null;
47
+ const sessionDir = join(homedir(), ".mma", "sessions");
48
+ const results = [];
49
+ try {
50
+ if (!fs.existsSync(sessionDir)) {
51
+ return {
52
+ success: true,
53
+ output: t("tool.no_sessions_dir", { dir: sessionDir }),
54
+ };
55
+ }
56
+ const entries = fs.readdirSync(sessionDir, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ if (!entry.isDirectory())
59
+ continue;
60
+ if (sessionId && entry.name !== sessionId)
61
+ continue;
62
+ const historyFile = join(sessionDir, entry.name, "history.jsonl");
63
+ searchFile(historyFile, query, maxResults, results);
64
+ if (results.length >= maxResults)
65
+ break;
66
+ }
67
+ if (results.length === 0) {
68
+ return { success: true, output: t("tool.no_history", { query }) };
69
+ }
70
+ return {
71
+ success: true,
72
+ output: t("tool.history_results", {
73
+ query,
74
+ results: results.join("\n"),
75
+ }),
76
+ };
77
+ }
78
+ catch (err) {
79
+ return {
80
+ success: false,
81
+ output: t("tool.history_error", { error: String(err) }),
82
+ };
83
+ }
84
+ },
85
+ };