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
@@ -7,9 +7,7 @@ function sanitizeToolName(server, tool) {
7
7
  }
8
8
  function convertInputSchema(schema) {
9
9
  if (schema && typeof schema === "object" && "properties" in schema) {
10
- const required = Array.isArray(schema.required)
11
- ? schema.required
12
- : [];
10
+ const required = Array.isArray(schema.required) ? schema.required : [];
13
11
  return {
14
12
  type: "object",
15
13
  properties: schema.properties,
@@ -118,9 +116,7 @@ export class MCPModule {
118
116
  const result = await client.callTool(d.toolName, args);
119
117
  return {
120
118
  success: true,
121
- output: typeof result === "string"
122
- ? result
123
- : JSON.stringify(result, null, 2),
119
+ output: typeof result === "string" ? result : JSON.stringify(result, null, 2),
124
120
  };
125
121
  }
126
122
  catch (e) {
@@ -1 +1 @@
1
- export { MemoryStore } from './store';
1
+ export { MemoryStore } from "./store";
@@ -1,44 +1,92 @@
1
- import { homedir } from 'os';
2
- import { join } from 'path';
3
- import { MemoryStore } from './store';
4
- import { rememberTool } from '../../tools/remember';
5
- import { recallTool } from '../../tools/recall';
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { MemoryStore } from "./store";
4
+ import { rememberTool } from "../../tools/remember";
5
+ import { recallTool } from "../../tools/recall";
6
+ const MEMORY_MD_FILES = ["errors", "conventions", "decisions", "facts"];
6
7
  export class MemoryModule {
7
- name = 'memory';
8
+ name = "memory";
8
9
  store;
9
- constructor(memoryDir) {
10
- const dir = memoryDir || join(homedir(), '.mma', 'memory');
11
- this.store = new MemoryStore(dir);
10
+ constructor(storeOrDir) {
11
+ if (storeOrDir instanceof MemoryStore) {
12
+ this.store = storeOrDir;
13
+ }
14
+ else {
15
+ const dir = storeOrDir || join(homedir(), ".mma", "memory");
16
+ this.store = new MemoryStore(dir);
17
+ }
12
18
  }
13
19
  getSystemPromptBlock() {
20
+ const parts = [];
14
21
  const prefs = this.store.getPreferences();
15
- if (Object.keys(prefs).length === 0)
22
+ if (Object.keys(prefs).length > 0) {
23
+ const prefStr = Object.entries(prefs)
24
+ .map(([k, v]) => `${k}=${v}`)
25
+ .join(", ");
26
+ parts.push(`User preferences: ${prefStr}`);
27
+ }
28
+ const memoryTail = this.getMemoryTail();
29
+ if (memoryTail)
30
+ parts.push(memoryTail);
31
+ if (parts.length === 0)
16
32
  return null;
17
- const prefStr = Object.entries(prefs)
18
- .map(([k, v]) => `${k}=${v}`)
19
- .join(', ');
33
+ const content = parts.join("\n\n");
20
34
  return {
21
- content: `User preferences: ${prefStr}`,
22
- priority: 'normal',
35
+ content,
36
+ priority: "normal",
23
37
  essential: false,
24
- estimatedTokens: Math.ceil(prefStr.length / 4) + 10,
38
+ estimatedTokens: Math.ceil(content.length / 4) + 10,
25
39
  };
26
40
  }
41
+ /**
42
+ * Bounded tail of the append-only .md memory files (errors, conventions,
43
+ * decisions, facts). This is what makes past-session lessons visible in the
44
+ * system prompt — without it the loop's error rules are write-only.
45
+ */
46
+ getMemoryTail() {
47
+ const sections = [];
48
+ const tailPerFile = 3;
49
+ for (const name of MEMORY_MD_FILES) {
50
+ const content = this.store.read(name).trim();
51
+ if (!content)
52
+ continue;
53
+ const entries = content
54
+ .split(/\r?\n/)
55
+ .map((l) => l.trim())
56
+ .filter((l) => l.startsWith("- "))
57
+ .slice(-tailPerFile);
58
+ if (entries.length === 0)
59
+ continue;
60
+ sections.push(`[Memory ${name}]:\n${entries.join("\n")}`);
61
+ }
62
+ if (sections.length === 0)
63
+ return null;
64
+ let joined = sections.join("\n\n");
65
+ // Hard cap so a huge errors.md can never blow the system prompt budget.
66
+ if (joined.length > 1200)
67
+ joined = joined.slice(0, 1200) + "\n…";
68
+ return joined;
69
+ }
27
70
  getToolDefinitions() {
28
71
  return [rememberTool, recallTool];
29
72
  }
30
73
  getPlugin() {
31
74
  return {
32
- name: 'memory',
75
+ name: "memory",
33
76
  isBuiltin: true,
34
77
  onBuildPrompt: () => {
78
+ const parts = [];
35
79
  const prefs = this.store.getPreferences();
36
- if (Object.keys(prefs).length === 0)
37
- return null;
38
- const prefStr = Object.entries(prefs)
39
- .map(([k, v]) => `${k}=${v}`)
40
- .join(', ');
41
- return `User preferences: ${prefStr}`;
80
+ if (Object.keys(prefs).length > 0) {
81
+ const prefStr = Object.entries(prefs)
82
+ .map(([k, v]) => `${k}=${v}`)
83
+ .join(", ");
84
+ parts.push(`User preferences: ${prefStr}`);
85
+ }
86
+ const memoryTail = this.getMemoryTail();
87
+ if (memoryTail)
88
+ parts.push(memoryTail);
89
+ return parts.length > 0 ? parts.join("\n\n") : null;
42
90
  },
43
91
  };
44
92
  }
@@ -1,6 +1,6 @@
1
- import { readFileSync, existsSync } from 'fs';
2
- import { join } from 'path';
3
- const MEMORY_FILES = ['conventions', 'decisions', 'errors', 'facts'];
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ const MEMORY_FILES = ["conventions", "decisions", "errors", "facts"];
4
4
  export class MemorySearch {
5
5
  memoryDir;
6
6
  constructor(memoryDir) {
@@ -13,8 +13,8 @@ export class MemorySearch {
13
13
  const path = join(this.memoryDir, `${name}.md`);
14
14
  if (!existsSync(path))
15
15
  continue;
16
- const content = readFileSync(path, 'utf-8');
17
- const lines = content.split('\n');
16
+ const content = readFileSync(path, "utf-8");
17
+ const lines = content.split("\n");
18
18
  for (const line of lines) {
19
19
  if (line.toLowerCase().includes(lowerQuery)) {
20
20
  results.push({ file: name, match: line.trim() });
@@ -22,18 +22,20 @@ export class MemorySearch {
22
22
  }
23
23
  }
24
24
  // Search preferences.json
25
- const prefsPath = join(this.memoryDir, 'preferences.json');
25
+ const prefsPath = join(this.memoryDir, "preferences.json");
26
26
  if (existsSync(prefsPath)) {
27
27
  try {
28
- const prefs = JSON.parse(readFileSync(prefsPath, 'utf-8'));
28
+ const prefs = JSON.parse(readFileSync(prefsPath, "utf-8"));
29
29
  for (const [key, value] of Object.entries(prefs)) {
30
30
  const searchStr = `${key}=${value}`;
31
31
  if (searchStr.toLowerCase().includes(lowerQuery)) {
32
- results.push({ file: 'preferences', match: `${key} = ${value}` });
32
+ results.push({ file: "preferences", match: `${key} = ${value}` });
33
33
  }
34
34
  }
35
35
  }
36
- catch { /* skip */ }
36
+ catch {
37
+ /* skip */
38
+ }
37
39
  }
38
40
  return results;
39
41
  }
@@ -1,7 +1,7 @@
1
- import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
2
- import { join } from 'path';
3
- import { MemorySearch } from './search';
4
- const MEMORY_FILES = ['conventions', 'decisions', 'errors', 'facts'];
1
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { MemorySearch } from "./search";
4
+ const MEMORY_FILES = ["conventions", "decisions", "errors", "facts"];
5
5
  export class MemoryStore {
6
6
  memoryDir;
7
7
  constructor(memoryDir) {
@@ -10,7 +10,7 @@ export class MemoryStore {
10
10
  for (const name of MEMORY_FILES) {
11
11
  const path = join(this.memoryDir, `${name}.md`);
12
12
  if (!existsSync(path)) {
13
- writeFileSync(path, `# ${name.charAt(0).toUpperCase() + name.slice(1)}\n\n`, 'utf-8');
13
+ writeFileSync(path, `# ${name.charAt(0).toUpperCase() + name.slice(1)}\n\n`, "utf-8");
14
14
  }
15
15
  }
16
16
  }
@@ -22,28 +22,28 @@ export class MemoryStore {
22
22
  read(name) {
23
23
  const path = join(this.memoryDir, `${name}.md`);
24
24
  if (!existsSync(path))
25
- return '';
26
- return readFileSync(path, 'utf-8');
25
+ return "";
26
+ return readFileSync(path, "utf-8");
27
27
  }
28
28
  append(name, entry) {
29
29
  const path = join(this.memoryDir, `${name}.md`);
30
- const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
30
+ const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
31
31
  const formatted = `- **${timestamp}** — ${entry}\n`;
32
- appendFileSync(path, formatted, 'utf-8');
32
+ appendFileSync(path, formatted, "utf-8");
33
33
  }
34
34
  search(query) {
35
35
  const searchModule = new MemorySearch(this.memoryDir);
36
36
  return searchModule.query(query);
37
37
  }
38
38
  prefsPath() {
39
- return join(this.memoryDir, 'preferences.json');
39
+ return join(this.memoryDir, "preferences.json");
40
40
  }
41
41
  getPreferences() {
42
42
  const path = this.prefsPath();
43
43
  if (!existsSync(path))
44
44
  return {};
45
45
  try {
46
- return JSON.parse(readFileSync(path, 'utf-8'));
46
+ return JSON.parse(readFileSync(path, "utf-8"));
47
47
  }
48
48
  catch {
49
49
  return {};
@@ -52,14 +52,14 @@ export class MemoryStore {
52
52
  setPreference(key, value) {
53
53
  const prefs = this.getPreferences();
54
54
  prefs[key] = value;
55
- writeFileSync(this.prefsPath(), JSON.stringify(prefs, null, 2), 'utf-8');
55
+ writeFileSync(this.prefsPath(), JSON.stringify(prefs, null, 2), "utf-8");
56
56
  }
57
57
  deletePreference(key) {
58
58
  const prefs = this.getPreferences();
59
59
  if (!(key in prefs))
60
60
  return false;
61
61
  delete prefs[key];
62
- writeFileSync(this.prefsPath(), JSON.stringify(prefs, null, 2), 'utf-8');
62
+ writeFileSync(this.prefsPath(), JSON.stringify(prefs, null, 2), "utf-8");
63
63
  return true;
64
64
  }
65
65
  appendRule(category, pattern, cause, solution) {
@@ -1,4 +1,4 @@
1
- import { t } from '../../i18n/index';
1
+ import { t } from "../../i18n/index";
2
2
  export class PipelineEngine {
3
3
  stepStatus = new Map();
4
4
  resolveDependencies(steps) {
@@ -7,7 +7,7 @@ export class PipelineEngine {
7
7
  const order = [];
8
8
  function visit(stepId, stepMap) {
9
9
  if (inStack.has(stepId))
10
- throw new Error(t('pipeline.circular', { stepId }));
10
+ throw new Error(t("pipeline.circular", { stepId }));
11
11
  if (visited.has(stepId))
12
12
  return;
13
13
  inStack.add(stepId);
@@ -21,7 +21,7 @@ export class PipelineEngine {
21
21
  visited.add(stepId);
22
22
  order.push(stepId);
23
23
  }
24
- const stepMap = new Map(steps.map(s => [s.id, s]));
24
+ const stepMap = new Map(steps.map((s) => [s.id, s]));
25
25
  for (const step of steps) {
26
26
  if (!visited.has(step.id)) {
27
27
  visit(step.id, stepMap);
@@ -30,28 +30,28 @@ export class PipelineEngine {
30
30
  return order;
31
31
  }
32
32
  getReadySteps(steps, completed) {
33
- return steps.filter(s => {
33
+ return steps.filter((s) => {
34
34
  if (completed.has(s.id))
35
35
  return false;
36
- if (this.stepStatus.get(s.id) === 'running')
36
+ if (this.stepStatus.get(s.id) === "running")
37
37
  return false;
38
- if (this.stepStatus.get(s.id) === 'failed')
38
+ if (this.stepStatus.get(s.id) === "failed")
39
39
  return false;
40
40
  if (!s.depends_on || s.depends_on.length === 0)
41
41
  return true;
42
- return s.depends_on.every(d => completed.has(d));
42
+ return s.depends_on.every((d) => completed.has(d));
43
43
  });
44
44
  }
45
45
  setStepStatus(stepId, status) {
46
46
  this.stepStatus.set(stepId, status);
47
47
  }
48
48
  getStepStatus(stepId) {
49
- return this.stepStatus.get(stepId) || 'pending';
49
+ return this.stepStatus.get(stepId) || "pending";
50
50
  }
51
51
  isComplete(stepIds) {
52
- return stepIds.every(id => {
52
+ return stepIds.every((id) => {
53
53
  const s = this.stepStatus.get(id);
54
- return s === 'done' || s === 'skipped';
54
+ return s === "done" || s === "skipped";
55
55
  });
56
56
  }
57
57
  reset() {
@@ -1,3 +1,3 @@
1
- export { PipelineParser } from './parser';
2
- export { PipelineEngine } from './engine';
3
- export { TemplateEngine } from './template';
1
+ export { PipelineParser } from "./parser";
2
+ export { PipelineEngine } from "./engine";
3
+ export { TemplateEngine } from "./template";
@@ -1,23 +1,23 @@
1
- import { t } from '../../i18n/index';
1
+ import { t } from "../../i18n/index";
2
2
  export class PipelineParser {
3
3
  static parse(yaml) {
4
- const lines = yaml.split('\n').filter(l => l.trim());
4
+ const lines = yaml.split("\n").filter((l) => l.trim());
5
5
  const result = {};
6
6
  let currentSection = null;
7
7
  let currentSteps = [];
8
8
  for (const line of lines) {
9
- if (line.match(/^\w+:/) && !line.startsWith(' ') && !line.startsWith(' -')) {
10
- const [key, ...valParts] = line.split(':');
11
- const val = valParts.join(':').trim();
12
- if (key === 'steps') {
13
- currentSection = 'steps';
9
+ if (line.match(/^\w+:/) && !line.startsWith(" ") && !line.startsWith(" -")) {
10
+ const [key, ...valParts] = line.split(":");
11
+ const val = valParts.join(":").trim();
12
+ if (key === "steps") {
13
+ currentSection = "steps";
14
14
  }
15
15
  else {
16
16
  result[key] = val;
17
17
  currentSection = null;
18
18
  }
19
19
  }
20
- else if (currentSection === 'steps') {
20
+ else if (currentSection === "steps") {
21
21
  const stepMatch = line.match(/^\s+- id:\s*(\S+)/);
22
22
  if (stepMatch) {
23
23
  currentSteps.push({ id: stepMatch[1] });
@@ -28,12 +28,15 @@ export class PipelineParser {
28
28
  if (kvMatch) {
29
29
  const key = kvMatch[1];
30
30
  let val = kvMatch[2].trim();
31
- if (val === 'true')
31
+ if (val === "true")
32
32
  val = true;
33
- else if (val === 'false')
33
+ else if (val === "false")
34
34
  val = false;
35
- else if (val.startsWith('[') && val.endsWith(']')) {
36
- val = val.slice(1, -1).split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
35
+ else if (val.startsWith("[") && val.endsWith("]")) {
36
+ val = val
37
+ .slice(1, -1)
38
+ .split(",")
39
+ .map((s) => s.trim().replace(/^['"]|['"]$/g, ""));
37
40
  }
38
41
  lastStep[key] = val;
39
42
  }
@@ -41,11 +44,11 @@ export class PipelineParser {
41
44
  }
42
45
  }
43
46
  if (!result.name || currentSteps.length === 0) {
44
- throw new Error(t('pipeline.invalid'));
47
+ throw new Error(t("pipeline.invalid"));
45
48
  }
46
49
  for (const step of currentSteps) {
47
50
  if (!step.id || !step.agent || !step.prompt) {
48
- throw new Error(t('pipeline.step_missing_fields', { step: JSON.stringify(step) }));
51
+ throw new Error(t("pipeline.step_missing_fields", { step: JSON.stringify(step) }));
49
52
  }
50
53
  }
51
54
  return { name: result.name, steps: currentSteps };
@@ -2,7 +2,7 @@ export class TemplateEngine {
2
2
  static render(template, params, stepOutputs) {
3
3
  let result = template;
4
4
  for (const [key, val] of Object.entries(params)) {
5
- result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), val);
5
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
6
6
  }
7
7
  if (stepOutputs) {
8
8
  result = result.replace(/\{\{steps\.(\w+)\.output\}\}/g, (_, stepId) => {
@@ -2,6 +2,7 @@ import { spawn, execSync } from "child_process";
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { resolve, extname, join } from "path";
4
4
  import { platform } from "os";
5
+ import { findProjectRoot } from "../../lsp/project-root";
5
6
  const TYPE_CHECK_DEBOUNCE_MS = 2000;
6
7
  // Per-file syntax checks are cached by (path, content hash): re-writing a file
7
8
  // with identical content skips the (expensive) check. Content changes → cache
@@ -40,6 +41,7 @@ function getWinDecoder() {
40
41
  export class LintOnWritePlugin {
41
42
  name = "lint-on-write";
42
43
  priority = 10;
44
+ isBuiltin = true;
43
45
  _checkPromise = null;
44
46
  _checkTimestamp = 0;
45
47
  async onAfterTool(ctx, call, result) {
@@ -62,10 +64,10 @@ export class LintOnWritePlugin {
62
64
  return;
63
65
  }
64
66
  await this.runProjectLint(ctx, result);
65
- await this.runProjectTypeCheck(ctx, result);
67
+ await this.runProjectTypeCheck(fullPath, ctx.baseDir, result);
66
68
  }
67
69
  async checkSyntax(filePath, ext, baseDir) {
68
- if (ext === ".ts" || ext === ".tsx") {
70
+ if (ext === ".ts" || ext === ".tsx" || ext === ".cts" || ext === ".mts") {
69
71
  // Syntax-only check via bun's parser (~200ms) instead of `npx tsc`
70
72
  // (~3-5s per write). Full type errors are still surfaced by the
71
73
  // debounced project typecheck below when a tsconfig exists.
@@ -93,21 +95,19 @@ export class LintOnWritePlugin {
93
95
  return null;
94
96
  }
95
97
  const stderr = err.stderr?.toString() || err.stdout?.toString() || "";
96
- const firstError = stderr.split("\n").find((line) => line.trim()) ||
97
- "TypeScript syntax error";
98
+ const firstError = stderr.split("\n").find((line) => line.trim()) || "TypeScript syntax error";
98
99
  syntaxCache.set(filePath, { hash, error: firstError.trim() });
99
100
  return firstError.trim();
100
101
  }
101
102
  }
102
- if (ext === ".js" || ext === ".jsx") {
103
+ if (ext === ".js" || ext === ".jsx" || ext === ".cjs" || ext === ".mjs") {
103
104
  try {
104
105
  await runAsync(`node --check "${filePath}"`, baseDir, 5000);
105
106
  return null;
106
107
  }
107
108
  catch (err) {
108
109
  const stderr = err.stderr?.toString() || "";
109
- const firstError = stderr.split("\n").find((line) => line.trim()) ||
110
- "JavaScript syntax error";
110
+ const firstError = stderr.split("\n").find((line) => line.trim()) || "JavaScript syntax error";
111
111
  return firstError.trim();
112
112
  }
113
113
  }
@@ -129,18 +129,25 @@ export class LintOnWritePlugin {
129
129
  ctx.logger.debug("Lint passed");
130
130
  }
131
131
  catch (err) {
132
+ const stderr = (err.stderr?.toString?.() || "").trim();
133
+ const stdout = (err.stdout?.toString?.() || "").trim();
134
+ const detail = (stderr || stdout || err.message).split("\n").filter(Boolean).slice(-5).join("\n");
132
135
  ctx.logger.warn(`Lint failed: ${err.message}`);
133
- result.output += `\n\n[Project lint failed]: ${err.message}`;
136
+ result.output += `\n\n[Project lint failed]: ${detail}`;
134
137
  }
135
138
  }
136
- async runProjectTypeCheck(ctx, result) {
137
- const tsconfigPath = join(ctx.baseDir, "tsconfig.json");
139
+ async runProjectTypeCheck(filePath, baseDir, result) {
140
+ // Resolve the project root from the EDITED FILE, not the agent baseDir.
141
+ // In a nested layout (`baseDir/proj/tsconfig.json`) the old baseDir-based
142
+ // lookup silently skipped the typecheck on every write — the whole
143
+ // session then ran with zero type feedback (observed in ses_mst2r0r5).
144
+ const projectRoot = findProjectRoot(filePath, baseDir, ["tsconfig.json", "package.json"]);
145
+ const tsconfigPath = join(projectRoot, "tsconfig.json");
138
146
  if (!existsSync(tsconfigPath)) {
139
147
  return;
140
148
  }
141
149
  const now = Date.now();
142
- if (this._checkPromise &&
143
- now - this._checkTimestamp < TYPE_CHECK_DEBOUNCE_MS) {
150
+ if (this._checkPromise && now - this._checkTimestamp < TYPE_CHECK_DEBOUNCE_MS) {
144
151
  const error = await this._checkPromise;
145
152
  if (error) {
146
153
  result.output += `\n\n[Project typecheck failed]: ${error}`;
@@ -148,7 +155,7 @@ export class LintOnWritePlugin {
148
155
  return;
149
156
  }
150
157
  this._checkTimestamp = now;
151
- this._checkPromise = this.runTscCheck(ctx.baseDir);
158
+ this._checkPromise = this.runTscCheck(projectRoot);
152
159
  const error = await this._checkPromise;
153
160
  if (error) {
154
161
  result.output += `\n\n[Project typecheck failed]: ${error}`;
@@ -167,9 +174,7 @@ export class LintOnWritePlugin {
167
174
  }
168
175
  const stderr = err.stderr?.toString() || err.stdout?.toString() || "";
169
176
  if (stderr.includes("error TS")) {
170
- const firstError = stderr
171
- .split("\n")
172
- .find((line) => line.includes("error TS")) ||
177
+ const firstError = stderr.split("\n").find((line) => line.includes("error TS")) ||
173
178
  "TypeScript type error";
174
179
  return firstError.trim();
175
180
  }
@@ -1,8 +1,9 @@
1
1
  export class NotifyPlugin {
2
- name = 'notify';
2
+ name = "notify";
3
3
  priority = 5;
4
+ isBuiltin = true;
4
5
  onSessionEnd(ctx) {
5
- ctx.logger.info('Session ended');
6
+ ctx.logger.info("Session ended");
6
7
  }
7
8
  }
8
9
  export const plugin = new NotifyPlugin();
@@ -1 +1 @@
1
- export { PluginManager } from './manager';
1
+ export { PluginManager } from "./manager";
@@ -1,28 +1,70 @@
1
- import { readdirSync, existsSync, statSync } from 'fs';
2
- import { join } from 'path';
3
- import { t } from '../../i18n/index';
1
+ import { readdirSync, existsSync, statSync } from "fs";
2
+ import { join, basename } from "path";
3
+ import { t } from "../../i18n/index";
4
+ import { semverGt } from "../updater/checker";
5
+ /**
6
+ * Entry file candidates for folder plugins. Looked up in order:
7
+ * `plugin.ts` > `plugin.js` > `index.ts` > `index.js`.
8
+ */
9
+ const FOLDER_ENTRY_NAMES = ["plugin.ts", "plugin.js", "index.ts", "index.js"];
4
10
  export class PluginLoader {
5
- loadFromDir(dirPath, pluginManager, logger) {
11
+ loadFromDir(dirPath, pluginManager, logger, options) {
6
12
  if (!existsSync(dirPath))
7
13
  return;
8
- const entries = readdirSync(dirPath);
14
+ const entries = readdirSync(dirPath).sort();
9
15
  for (const entry of entries) {
10
16
  const fullPath = join(dirPath, entry);
11
- if (!statSync(fullPath).isFile())
12
- continue;
13
- if (!entry.endsWith('.ts') && !entry.endsWith('.js'))
14
- continue;
15
- try {
16
- const mod = require(fullPath);
17
- const plugin = mod.default || mod.plugin;
18
- if (plugin && plugin.name) {
19
- pluginManager.register(plugin);
20
- logger?.warn(t('plugin.loaded', { name: plugin.name }));
17
+ const stat = statSync(fullPath);
18
+ if (stat.isFile()) {
19
+ if (!entry.endsWith(".ts") && !entry.endsWith(".js"))
20
+ continue;
21
+ this.tryLoad(fullPath, entry, pluginManager, logger, options);
22
+ }
23
+ else if (stat.isDirectory()) {
24
+ const entryFile = this.findEntryFile(fullPath);
25
+ if (entryFile) {
26
+ this.tryLoad(entryFile, `${entry}/${basename(entryFile)}`, pluginManager, logger, options);
21
27
  }
22
28
  }
23
- catch (e) {
24
- logger?.warn(t('plugin.skipped', { entry, message: e.message }));
29
+ }
30
+ }
31
+ findEntryFile(dir) {
32
+ for (const name of FOLDER_ENTRY_NAMES) {
33
+ const candidate = join(dir, name);
34
+ if (existsSync(candidate))
35
+ return candidate;
36
+ }
37
+ return null;
38
+ }
39
+ tryLoad(fullPath, label, pluginManager, logger, options) {
40
+ try {
41
+ const mod = require(fullPath);
42
+ const plugin = mod.default || mod.plugin;
43
+ if (plugin && plugin.name) {
44
+ if (plugin.minMmaVersion &&
45
+ options?.mmaVersion &&
46
+ semverGt(plugin.minMmaVersion, options.mmaVersion)) {
47
+ logger?.warn(t("plugin.incompatible", {
48
+ name: plugin.name,
49
+ min: plugin.minMmaVersion,
50
+ current: options.mmaVersion,
51
+ }));
52
+ return;
53
+ }
54
+ const result = pluginManager.register(plugin, options?.source);
55
+ if (result.status === "registered") {
56
+ logger?.warn(t("plugin.loaded", { name: plugin.name }));
57
+ }
58
+ else if (result.status === "skipped_older") {
59
+ logger?.warn(t("plugin.dedup_older", {
60
+ name: plugin.name,
61
+ version: plugin.version || "0.0.0",
62
+ }));
63
+ }
25
64
  }
26
65
  }
66
+ catch (e) {
67
+ logger?.warn(t("plugin.skipped", { entry: label, message: e.message }));
68
+ }
27
69
  }
28
70
  }