magi-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +377 -0
  3. package/README.md +377 -0
  4. package/dist/bin/magi-benchmark.d.ts +14 -0
  5. package/dist/bin/magi-benchmark.js +93 -0
  6. package/dist/bin/magi-mcp.d.ts +8 -0
  7. package/dist/bin/magi-mcp.js +28 -0
  8. package/dist/bin/magi.d.ts +2 -0
  9. package/dist/bin/magi.js +634 -0
  10. package/dist/src/adapters/base.d.ts +34 -0
  11. package/dist/src/adapters/base.js +149 -0
  12. package/dist/src/adapters/claude.d.ts +29 -0
  13. package/dist/src/adapters/claude.js +65 -0
  14. package/dist/src/adapters/codex.d.ts +21 -0
  15. package/dist/src/adapters/codex.js +41 -0
  16. package/dist/src/adapters/gemini.d.ts +18 -0
  17. package/dist/src/adapters/gemini.js +31 -0
  18. package/dist/src/adapters/registry.d.ts +19 -0
  19. package/dist/src/adapters/registry.js +59 -0
  20. package/dist/src/audit/hash-chain.d.ts +21 -0
  21. package/dist/src/audit/hash-chain.js +70 -0
  22. package/dist/src/audit/types.d.ts +25 -0
  23. package/dist/src/audit/types.js +1 -0
  24. package/dist/src/audit/writer.d.ts +18 -0
  25. package/dist/src/audit/writer.js +100 -0
  26. package/dist/src/benchmark/golden-tasks.d.ts +9 -0
  27. package/dist/src/benchmark/golden-tasks.js +476 -0
  28. package/dist/src/benchmark/reporter.d.ts +5 -0
  29. package/dist/src/benchmark/reporter.js +107 -0
  30. package/dist/src/benchmark/runner.d.ts +30 -0
  31. package/dist/src/benchmark/runner.js +224 -0
  32. package/dist/src/benchmark/scorer.d.ts +12 -0
  33. package/dist/src/benchmark/scorer.js +124 -0
  34. package/dist/src/benchmark/types.d.ts +54 -0
  35. package/dist/src/benchmark/types.js +1 -0
  36. package/dist/src/cache/deliberation-cache.d.ts +49 -0
  37. package/dist/src/cache/deliberation-cache.js +127 -0
  38. package/dist/src/cli/commands/config-cmd.d.ts +11 -0
  39. package/dist/src/cli/commands/config-cmd.js +190 -0
  40. package/dist/src/cli/commands/demo.d.ts +12 -0
  41. package/dist/src/cli/commands/demo.js +66 -0
  42. package/dist/src/cli/commands/setup.d.ts +7 -0
  43. package/dist/src/cli/commands/setup.js +182 -0
  44. package/dist/src/cli/i18n.d.ts +89 -0
  45. package/dist/src/cli/i18n.js +176 -0
  46. package/dist/src/cli/interactive-select.d.ts +27 -0
  47. package/dist/src/cli/interactive-select.js +130 -0
  48. package/dist/src/cli/tui-setup.d.ts +24 -0
  49. package/dist/src/cli/tui-setup.js +42 -0
  50. package/dist/src/config/cli-detector.d.ts +37 -0
  51. package/dist/src/config/cli-detector.js +99 -0
  52. package/dist/src/config/user-config.d.ts +81 -0
  53. package/dist/src/config/user-config.js +134 -0
  54. package/dist/src/context/auto-collector.d.ts +43 -0
  55. package/dist/src/context/auto-collector.js +337 -0
  56. package/dist/src/context/manager.d.ts +35 -0
  57. package/dist/src/context/manager.js +162 -0
  58. package/dist/src/context/serializer.d.ts +20 -0
  59. package/dist/src/context/serializer.js +52 -0
  60. package/dist/src/demo/recorded-deliberation.d.ts +13 -0
  61. package/dist/src/demo/recorded-deliberation.js +277 -0
  62. package/dist/src/engine/angel-detector.d.ts +83 -0
  63. package/dist/src/engine/angel-detector.js +334 -0
  64. package/dist/src/engine/at-field.d.ts +40 -0
  65. package/dist/src/engine/at-field.js +195 -0
  66. package/dist/src/engine/berserk-orchestrator.d.ts +66 -0
  67. package/dist/src/engine/berserk-orchestrator.js +378 -0
  68. package/dist/src/engine/change-metrics.d.ts +56 -0
  69. package/dist/src/engine/change-metrics.js +214 -0
  70. package/dist/src/engine/consensus.d.ts +20 -0
  71. package/dist/src/engine/consensus.js +146 -0
  72. package/dist/src/engine/dead-sea-scrolls.d.ts +132 -0
  73. package/dist/src/engine/dead-sea-scrolls.js +610 -0
  74. package/dist/src/engine/drift-detector.d.ts +39 -0
  75. package/dist/src/engine/drift-detector.js +225 -0
  76. package/dist/src/engine/dummy-plug.d.ts +44 -0
  77. package/dist/src/engine/dummy-plug.js +190 -0
  78. package/dist/src/engine/engram-manager.d.ts +55 -0
  79. package/dist/src/engine/engram-manager.js +306 -0
  80. package/dist/src/engine/events.d.ts +130 -0
  81. package/dist/src/engine/events.js +44 -0
  82. package/dist/src/engine/gospel.d.ts +30 -0
  83. package/dist/src/engine/gospel.js +129 -0
  84. package/dist/src/engine/hallucination-detector.d.ts +33 -0
  85. package/dist/src/engine/hallucination-detector.js +215 -0
  86. package/dist/src/engine/human-resolver.d.ts +19 -0
  87. package/dist/src/engine/human-resolver.js +89 -0
  88. package/dist/src/engine/instrumentality.d.ts +64 -0
  89. package/dist/src/engine/instrumentality.js +297 -0
  90. package/dist/src/engine/iruel-battle.d.ts +79 -0
  91. package/dist/src/engine/iruel-battle.js +319 -0
  92. package/dist/src/engine/kernel/deliberation-kernel.d.ts +12 -0
  93. package/dist/src/engine/kernel/deliberation-kernel.js +303 -0
  94. package/dist/src/engine/kernel/index.d.ts +8 -0
  95. package/dist/src/engine/kernel/index.js +7 -0
  96. package/dist/src/engine/kernel/phase-runner.d.ts +10 -0
  97. package/dist/src/engine/kernel/phase-runner.js +155 -0
  98. package/dist/src/engine/kernel/post-processor.d.ts +17 -0
  99. package/dist/src/engine/kernel/post-processor.js +131 -0
  100. package/dist/src/engine/kernel/types.d.ts +107 -0
  101. package/dist/src/engine/kernel/types.js +1 -0
  102. package/dist/src/engine/kernel/unit-executor.d.ts +6 -0
  103. package/dist/src/engine/kernel/unit-executor.js +132 -0
  104. package/dist/src/engine/lcl-manager.d.ts +44 -0
  105. package/dist/src/engine/lcl-manager.js +143 -0
  106. package/dist/src/engine/middleware/cache.d.ts +7 -0
  107. package/dist/src/engine/middleware/cache.js +29 -0
  108. package/dist/src/engine/middleware/chain.d.ts +18 -0
  109. package/dist/src/engine/middleware/chain.js +45 -0
  110. package/dist/src/engine/middleware/firewall.d.ts +8 -0
  111. package/dist/src/engine/middleware/firewall.js +24 -0
  112. package/dist/src/engine/middleware/index.d.ts +4 -0
  113. package/dist/src/engine/middleware/index.js +3 -0
  114. package/dist/src/engine/middleware/types.d.ts +43 -0
  115. package/dist/src/engine/middleware/types.js +1 -0
  116. package/dist/src/engine/nebuchadnezzar-key.d.ts +61 -0
  117. package/dist/src/engine/nebuchadnezzar-key.js +203 -0
  118. package/dist/src/engine/neon-genesis.d.ts +52 -0
  119. package/dist/src/engine/neon-genesis.js +203 -0
  120. package/dist/src/engine/objective-judge.d.ts +53 -0
  121. package/dist/src/engine/objective-judge.js +214 -0
  122. package/dist/src/engine/offline-mode.d.ts +18 -0
  123. package/dist/src/engine/offline-mode.js +46 -0
  124. package/dist/src/engine/orchestrator.d.ts +79 -0
  125. package/dist/src/engine/orchestrator.js +58 -0
  126. package/dist/src/engine/secret-cipher.d.ts +26 -0
  127. package/dist/src/engine/secret-cipher.js +114 -0
  128. package/dist/src/engine/seele-council.d.ts +90 -0
  129. package/dist/src/engine/seele-council.js +482 -0
  130. package/dist/src/engine/self-destruct.d.ts +61 -0
  131. package/dist/src/engine/self-destruct.js +231 -0
  132. package/dist/src/engine/self-evolution.d.ts +64 -0
  133. package/dist/src/engine/self-evolution.js +368 -0
  134. package/dist/src/engine/sync-rate.d.ts +45 -0
  135. package/dist/src/engine/sync-rate.js +151 -0
  136. package/dist/src/engine/type666-firewall.d.ts +76 -0
  137. package/dist/src/engine/type666-firewall.js +343 -0
  138. package/dist/src/engine/umbilical-cable.d.ts +41 -0
  139. package/dist/src/engine/umbilical-cable.js +192 -0
  140. package/dist/src/index.d.ts +106 -0
  141. package/dist/src/index.js +426 -0
  142. package/dist/src/mcp/server.d.ts +38 -0
  143. package/dist/src/mcp/server.js +196 -0
  144. package/dist/src/metrics/token-tracker.d.ts +38 -0
  145. package/dist/src/metrics/token-tracker.js +112 -0
  146. package/dist/src/parsers/json-extractor.d.ts +9 -0
  147. package/dist/src/parsers/json-extractor.js +239 -0
  148. package/dist/src/parsers/opinion-schema.d.ts +81 -0
  149. package/dist/src/parsers/opinion-schema.js +147 -0
  150. package/dist/src/parsers/unstructured-parser.d.ts +20 -0
  151. package/dist/src/parsers/unstructured-parser.js +122 -0
  152. package/dist/src/pipelines/architecture.d.ts +10 -0
  153. package/dist/src/pipelines/architecture.js +9 -0
  154. package/dist/src/pipelines/bug-analysis.d.ts +9 -0
  155. package/dist/src/pipelines/bug-analysis.js +8 -0
  156. package/dist/src/pipelines/code-review.d.ts +10 -0
  157. package/dist/src/pipelines/code-review.js +30 -0
  158. package/dist/src/pipelines/custom.d.ts +14 -0
  159. package/dist/src/pipelines/custom.js +29 -0
  160. package/dist/src/pipelines/registry.d.ts +9 -0
  161. package/dist/src/pipelines/registry.js +20 -0
  162. package/dist/src/prompts/personas.d.ts +6 -0
  163. package/dist/src/prompts/personas.js +44 -0
  164. package/dist/src/prompts/schemas.d.ts +4 -0
  165. package/dist/src/prompts/schemas.js +24 -0
  166. package/dist/src/prompts/templates.d.ts +6 -0
  167. package/dist/src/prompts/templates.js +91 -0
  168. package/dist/src/repl/accessibility.d.ts +23 -0
  169. package/dist/src/repl/accessibility.js +46 -0
  170. package/dist/src/repl/banner.d.ts +4 -0
  171. package/dist/src/repl/banner.js +28 -0
  172. package/dist/src/repl/boot-animation.d.ts +13 -0
  173. package/dist/src/repl/boot-animation.js +143 -0
  174. package/dist/src/repl/completer.d.ts +21 -0
  175. package/dist/src/repl/completer.js +168 -0
  176. package/dist/src/repl/context.d.ts +24 -0
  177. package/dist/src/repl/context.js +42 -0
  178. package/dist/src/repl/display-utils.d.ts +13 -0
  179. package/dist/src/repl/display-utils.js +65 -0
  180. package/dist/src/repl/event-listener.d.ts +18 -0
  181. package/dist/src/repl/event-listener.js +112 -0
  182. package/dist/src/repl/export-formatter.d.ts +8 -0
  183. package/dist/src/repl/export-formatter.js +73 -0
  184. package/dist/src/repl/ghost-text.d.ts +31 -0
  185. package/dist/src/repl/ghost-text.js +119 -0
  186. package/dist/src/repl/handoff-animation.d.ts +15 -0
  187. package/dist/src/repl/handoff-animation.js +65 -0
  188. package/dist/src/repl/history.d.ts +16 -0
  189. package/dist/src/repl/history.js +130 -0
  190. package/dist/src/repl/job-registry.d.ts +26 -0
  191. package/dist/src/repl/job-registry.js +80 -0
  192. package/dist/src/repl/magi-repl.d.ts +72 -0
  193. package/dist/src/repl/magi-repl.js +1008 -0
  194. package/dist/src/repl/multiline-input.d.ts +45 -0
  195. package/dist/src/repl/multiline-input.js +78 -0
  196. package/dist/src/repl/prompt-builder.d.ts +19 -0
  197. package/dist/src/repl/prompt-builder.js +36 -0
  198. package/dist/src/repl/repl-state.d.ts +5 -0
  199. package/dist/src/repl/repl-state.js +19 -0
  200. package/dist/src/repl/result-display.d.ts +8 -0
  201. package/dist/src/repl/result-display.js +195 -0
  202. package/dist/src/repl/session-stats.d.ts +26 -0
  203. package/dist/src/repl/session-stats.js +119 -0
  204. package/dist/src/repl/slash-commands.d.ts +60 -0
  205. package/dist/src/repl/slash-commands.js +725 -0
  206. package/dist/src/repl/terminal-sanitize.d.ts +14 -0
  207. package/dist/src/repl/terminal-sanitize.js +19 -0
  208. package/dist/src/reporters/console.d.ts +7 -0
  209. package/dist/src/reporters/console.js +78 -0
  210. package/dist/src/reporters/json.d.ts +2 -0
  211. package/dist/src/reporters/json.js +3 -0
  212. package/dist/src/reporters/markdown.d.ts +2 -0
  213. package/dist/src/reporters/markdown.js +65 -0
  214. package/dist/src/reporters/streaming.d.ts +20 -0
  215. package/dist/src/reporters/streaming.js +178 -0
  216. package/dist/src/tui/activity-log.d.ts +23 -0
  217. package/dist/src/tui/activity-log.js +67 -0
  218. package/dist/src/tui/animations.d.ts +39 -0
  219. package/dist/src/tui/animations.js +167 -0
  220. package/dist/src/tui/ansi.d.ts +28 -0
  221. package/dist/src/tui/ansi.js +51 -0
  222. package/dist/src/tui/boot-sequence.d.ts +11 -0
  223. package/dist/src/tui/boot-sequence.js +98 -0
  224. package/dist/src/tui/colors.d.ts +101 -0
  225. package/dist/src/tui/colors.js +71 -0
  226. package/dist/src/tui/header.d.ts +24 -0
  227. package/dist/src/tui/header.js +122 -0
  228. package/dist/src/tui/index.d.ts +3 -0
  229. package/dist/src/tui/index.js +3 -0
  230. package/dist/src/tui/keypress.d.ts +25 -0
  231. package/dist/src/tui/keypress.js +95 -0
  232. package/dist/src/tui/layout.d.ts +74 -0
  233. package/dist/src/tui/layout.js +171 -0
  234. package/dist/src/tui/magi-tui.d.ts +101 -0
  235. package/dist/src/tui/magi-tui.js +754 -0
  236. package/dist/src/tui/panel.d.ts +45 -0
  237. package/dist/src/tui/panel.js +292 -0
  238. package/dist/src/tui/screen-buffer.d.ts +54 -0
  239. package/dist/src/tui/screen-buffer.js +262 -0
  240. package/dist/src/tui/status-bar.d.ts +25 -0
  241. package/dist/src/tui/status-bar.js +124 -0
  242. package/dist/src/tui/terminal-detect.d.ts +26 -0
  243. package/dist/src/tui/terminal-detect.js +44 -0
  244. package/dist/src/tui/tui-helpers.d.ts +12 -0
  245. package/dist/src/tui/tui-helpers.js +37 -0
  246. package/dist/src/types/adapter.d.ts +75 -0
  247. package/dist/src/types/adapter.js +36 -0
  248. package/dist/src/types/config.d.ts +108 -0
  249. package/dist/src/types/config.js +85 -0
  250. package/dist/src/types/consensus.d.ts +55 -0
  251. package/dist/src/types/consensus.js +17 -0
  252. package/dist/src/types/core.d.ts +178 -0
  253. package/dist/src/types/core.js +85 -0
  254. package/dist/src/types/magi-api.d.ts +62 -0
  255. package/dist/src/types/magi-api.js +7 -0
  256. package/dist/src/types/phase-h.d.ts +142 -0
  257. package/dist/src/types/phase-h.js +7 -0
  258. package/dist/src/types/phase-i.d.ts +186 -0
  259. package/dist/src/types/phase-i.js +6 -0
  260. package/dist/src/types/phase-k.d.ts +259 -0
  261. package/dist/src/types/phase-k.js +6 -0
  262. package/dist/src/types/phase-l.d.ts +199 -0
  263. package/dist/src/types/phase-l.js +6 -0
  264. package/dist/src/types/pipeline.d.ts +37 -0
  265. package/dist/src/types/pipeline.js +2 -0
  266. package/dist/src/utils/abstain-factory.d.ts +2 -0
  267. package/dist/src/utils/abstain-factory.js +18 -0
  268. package/dist/src/utils/errors.d.ts +34 -0
  269. package/dist/src/utils/errors.js +59 -0
  270. package/dist/src/utils/file-validator.d.ts +50 -0
  271. package/dist/src/utils/file-validator.js +124 -0
  272. package/dist/src/utils/fire-and-forget.d.ts +5 -0
  273. package/dist/src/utils/fire-and-forget.js +10 -0
  274. package/dist/src/utils/flag-validator.d.ts +21 -0
  275. package/dist/src/utils/flag-validator.js +79 -0
  276. package/dist/src/utils/freeze.d.ts +8 -0
  277. package/dist/src/utils/freeze.js +16 -0
  278. package/dist/src/utils/language-detector.d.ts +16 -0
  279. package/dist/src/utils/language-detector.js +159 -0
  280. package/dist/src/utils/latency-tracker.d.ts +45 -0
  281. package/dist/src/utils/latency-tracker.js +100 -0
  282. package/dist/src/utils/logger.d.ts +33 -0
  283. package/dist/src/utils/logger.js +112 -0
  284. package/dist/src/utils/process.d.ts +40 -0
  285. package/dist/src/utils/process.js +253 -0
  286. package/dist/src/utils/retry.d.ts +12 -0
  287. package/dist/src/utils/retry.js +30 -0
  288. package/dist/src/utils/safe-fs.d.ts +38 -0
  289. package/dist/src/utils/safe-fs.js +56 -0
  290. package/dist/src/utils/safe-json-parse.d.ts +15 -0
  291. package/dist/src/utils/safe-json-parse.js +49 -0
  292. package/dist/src/utils/sanitize.d.ts +14 -0
  293. package/dist/src/utils/sanitize.js +186 -0
  294. package/dist/src/utils/semaphore.d.ts +22 -0
  295. package/dist/src/utils/semaphore.js +57 -0
  296. package/dist/src/utils/shutdown.d.ts +6 -0
  297. package/dist/src/utils/shutdown.js +51 -0
  298. package/dist/src/utils/tty.d.ts +5 -0
  299. package/dist/src/utils/tty.js +7 -0
  300. package/package.json +82 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * User configuration management for .magi/config.json
3
+ *
4
+ * Handles loading, saving, validating, and merging user-level
5
+ * settings with the immutable DEFAULT_CONFIG.
6
+ */
7
+ import { readFile } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ import { z } from 'zod/v4';
10
+ import { DEFAULT_CONFIG } from '../types/config.js';
11
+ import { safePersist } from '../utils/safe-fs.js';
12
+ import { logger } from '../utils/logger.js';
13
+ // ── User Config Schema ──────────────────────────────────────────
14
+ const DeadlockStrategySchema = z.enum([
15
+ 'escalate', 'melchior-tiebreak', 'highest-confidence', 'human-in-the-loop',
16
+ ]);
17
+ export const UserConfigSchema = z.object({
18
+ enabledUnits: z.array(z.enum(['MELCHIOR', 'BALTHASAR', 'CASPER'])).min(1).max(3).optional(),
19
+ adapters: z.record(z.string(), z.object({
20
+ timeoutMs: z.number().int().min(1_000).max(600_000).optional(),
21
+ maxRetries: z.number().int().min(0).max(5).optional(),
22
+ model: z.string().optional(),
23
+ })).optional(),
24
+ defaultPipeline: z.object({
25
+ maxRounds: z.number().int().min(1).max(10).optional(),
26
+ earlyConsensusExit: z.boolean().optional(),
27
+ consensus: z.object({
28
+ deadlockStrategy: DeadlockStrategySchema.optional(),
29
+ quorum: z.number().int().min(1).max(7).optional(),
30
+ }).optional(),
31
+ phaseTimeoutMs: z.number().int().min(1_000).max(3_600_000).optional(),
32
+ }).optional(),
33
+ logLevel: z.enum(['debug', 'info', 'warn', 'error']).optional(),
34
+ outputFormat: z.enum(['console', 'json', 'markdown']).optional(),
35
+ tui: z.object({
36
+ enabled: z.boolean().optional(),
37
+ soundEnabled: z.boolean().optional(),
38
+ }).optional(),
39
+ cacheEnabled: z.boolean().optional(),
40
+ language: z.enum(['ja', 'en']).optional(),
41
+ fileAccess: z.object({
42
+ enabled: z.boolean().optional(),
43
+ allowedPaths: z.array(z.string()).optional(),
44
+ deniedPatterns: z.array(z.string()).optional(),
45
+ }).optional(),
46
+ }).strict();
47
+ // ── Paths ───────────────────────────────────────────────────────
48
+ const CONFIG_FILENAME = 'config.json';
49
+ export function getUserConfigPath(workspaceDir = '.magi') {
50
+ return join(workspaceDir, CONFIG_FILENAME);
51
+ }
52
+ // ── Load / Save ─────────────────────────────────────────────────
53
+ /**
54
+ * Load user config from .magi/config.json.
55
+ * Returns undefined if file does not exist.
56
+ * Throws on invalid JSON or schema validation failure.
57
+ */
58
+ export async function loadUserConfig(workspaceDir = '.magi') {
59
+ const configPath = getUserConfigPath(workspaceDir);
60
+ let raw;
61
+ try {
62
+ raw = await readFile(configPath, 'utf-8');
63
+ }
64
+ catch (err) {
65
+ if (err.code === 'ENOENT') {
66
+ return undefined;
67
+ }
68
+ throw err;
69
+ }
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(raw);
73
+ }
74
+ catch {
75
+ throw new Error(`Invalid JSON in ${configPath}`);
76
+ }
77
+ return UserConfigSchema.parse(parsed);
78
+ }
79
+ /**
80
+ * Load user config with graceful fallback.
81
+ * Logs a warning on error and returns undefined instead of throwing.
82
+ */
83
+ export async function loadUserConfigSafe(workspaceDir = '.magi') {
84
+ try {
85
+ return await loadUserConfig(workspaceDir);
86
+ }
87
+ catch (err) {
88
+ logger.warn(`Failed to load user config: ${err instanceof Error ? err.message : String(err)}`);
89
+ return undefined;
90
+ }
91
+ }
92
+ /**
93
+ * Save user config to .magi/config.json using safePersist (0o600).
94
+ */
95
+ export async function saveUserConfig(config, workspaceDir = '.magi') {
96
+ const validated = UserConfigSchema.parse(config);
97
+ const configPath = getUserConfigPath(workspaceDir);
98
+ await safePersist(configPath, JSON.stringify(validated, null, 2) + '\n');
99
+ }
100
+ // ── Deep Merge ──────────────────────────────────────────────────
101
+ function isPlainObject(val) {
102
+ return val !== null && typeof val === 'object' && !Array.isArray(val);
103
+ }
104
+ /**
105
+ * Recursively merge user overrides into a base object.
106
+ * Arrays are replaced (not merged). undefined values in user are skipped.
107
+ */
108
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
109
+ function deepMerge(base, overrides) {
110
+ const result = { ...base };
111
+ for (const key of Object.keys(overrides)) {
112
+ if (UNSAFE_KEYS.has(key))
113
+ continue;
114
+ const userVal = overrides[key];
115
+ if (userVal === undefined)
116
+ continue;
117
+ const baseVal = base[key];
118
+ if (isPlainObject(baseVal) && isPlainObject(userVal)) {
119
+ result[key] = deepMerge(baseVal, userVal);
120
+ }
121
+ else {
122
+ result[key] = userVal;
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ /**
128
+ * Merge user config with DEFAULT_CONFIG, producing a full MagiConfig.
129
+ * The `tui` and `language` fields are stripped (consumed by the CLI layer, not Magi core).
130
+ */
131
+ export function mergeWithDefaults(userConfig) {
132
+ const { tui: _tui, language: _language, ...magiFields } = userConfig;
133
+ return deepMerge(DEFAULT_CONFIG, magiFields);
134
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Auto-context collector — gathers project context and related files
3
+ * to enrich MAGI deliberation quality.
4
+ *
5
+ * Two main capabilities:
6
+ * 1. Project context: git status, package.json, directory tree
7
+ * 2. Related files: imports, type definitions, tests for a target file
8
+ *
9
+ * All collection is graceful — failures are silently skipped so that
10
+ * deliberation proceeds even without context.
11
+ */
12
+ import type { TaskArtifact } from '../types/core.js';
13
+ /**
14
+ * Collect project-level context: git status, package.json, directory tree.
15
+ * Returns a structured text block ready for injection into task.context.
16
+ */
17
+ export declare function collectProjectContext(cwd?: string): Promise<{
18
+ text: string;
19
+ charCount: number;
20
+ }>;
21
+ /**
22
+ * Collect files related to a target file: imports, type definitions, tests.
23
+ * Returns TaskArtifact[] ready for injection into task.artifacts.
24
+ */
25
+ export declare function collectRelatedFiles(targetFile: string, fileContent: string, options?: {
26
+ basedir?: string;
27
+ }): Promise<TaskArtifact[]>;
28
+ /**
29
+ * Extract relative import paths from source code.
30
+ *
31
+ * Supports:
32
+ * - import ... from './path'
33
+ * - import('./path')
34
+ * - require('./path')
35
+ * - export ... from './path'
36
+ *
37
+ * Only extracts relative paths (starting with ./ or ../).
38
+ * Package imports are excluded.
39
+ *
40
+ * Limitation: single-line only, no tsconfig paths alias support.
41
+ * The function signature is kept simple for future AST migration.
42
+ */
43
+ export declare function extractImports(content: string): string[];
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Auto-context collector — gathers project context and related files
3
+ * to enrich MAGI deliberation quality.
4
+ *
5
+ * Two main capabilities:
6
+ * 1. Project context: git status, package.json, directory tree
7
+ * 2. Related files: imports, type definitions, tests for a target file
8
+ *
9
+ * All collection is graceful — failures are silently skipped so that
10
+ * deliberation proceeds even without context.
11
+ */
12
+ import { readFile, readdir, stat } from 'node:fs/promises';
13
+ import { join, dirname, resolve, extname, basename, relative, isAbsolute } from 'node:path';
14
+ import { execGit } from '../engine/change-metrics.js';
15
+ import { validateFile } from '../utils/file-validator.js';
16
+ import { detectLanguage } from '../utils/language-detector.js';
17
+ // ── Constants ────────────────────────────────────────────────
18
+ /** Directories excluded from tree traversal */
19
+ const TREE_EXCLUDE = new Set([
20
+ 'node_modules', '.git', 'dist', 'build', 'coverage',
21
+ '.magi', '.next', '.nuxt', '.output', '__pycache__',
22
+ '.tox', '.venv', 'venv', '.cache', '.turbo',
23
+ ]);
24
+ /** Maximum tree depth */
25
+ const TREE_MAX_DEPTH = 3;
26
+ /** Extensions to try when resolving imports */
27
+ const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
28
+ /** Index file names to try when resolving directory imports */
29
+ const INDEX_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.mjs'];
30
+ /** Sensitive file patterns — never collected or exposed to AI CLIs */
31
+ const SENSITIVE_PATTERNS = [
32
+ /\.env($|\.)/, /secret/i, /credential/i, /password/i,
33
+ /\.pem$/, /\.key$/, /\.p12$/, /\.pfx$/, /id_rsa/,
34
+ /\.npmrc$/, /\.pypirc$/, /\.netrc$/, /\.aws\//,
35
+ /\.docker\/config\.json$/,
36
+ /\.ssh\//, /authorized_keys$/,
37
+ ];
38
+ // ── Project context ──────────────────────────────────────────
39
+ /**
40
+ * Collect project-level context: git status, package.json, directory tree.
41
+ * Returns a structured text block ready for injection into task.context.
42
+ */
43
+ export async function collectProjectContext(cwd) {
44
+ const dir = cwd ?? process.cwd();
45
+ const sections = [];
46
+ // Run all collectors in parallel
47
+ const [gitStatus, packageJson, tree] = await Promise.allSettled([
48
+ collectGitStatus(dir),
49
+ collectPackageJson(dir),
50
+ collectDirectoryTree(dir),
51
+ ]);
52
+ if (gitStatus.status === 'fulfilled' && gitStatus.value) {
53
+ sections.push('## Git Status\n```\n' + gitStatus.value + '\n```');
54
+ }
55
+ if (packageJson.status === 'fulfilled' && packageJson.value) {
56
+ sections.push('## package.json\n```json\n' + packageJson.value + '\n```');
57
+ }
58
+ if (tree.status === 'fulfilled' && tree.value) {
59
+ sections.push('## Directory Structure\n```\n' + tree.value + '\n```');
60
+ }
61
+ if (sections.length === 0) {
62
+ return { text: '', charCount: 0 };
63
+ }
64
+ const MAX_PROJECT_CONTEXT = 524_288; // 512KB safety cap
65
+ let text = '# Project Context\n\n' + sections.join('\n\n');
66
+ if (text.length > MAX_PROJECT_CONTEXT) {
67
+ text = text.slice(0, MAX_PROJECT_CONTEXT) + '\n\n[truncated]';
68
+ }
69
+ return { text, charCount: text.length };
70
+ }
71
+ // ── Related files ────────────────────────────────────────────
72
+ /**
73
+ * Collect files related to a target file: imports, type definitions, tests.
74
+ * Returns TaskArtifact[] ready for injection into task.artifacts.
75
+ */
76
+ export async function collectRelatedFiles(targetFile, fileContent, options) {
77
+ const basedir = options?.basedir ?? process.cwd();
78
+ const targetAbsolute = resolve(basedir, targetFile);
79
+ const targetDir = dirname(targetAbsolute);
80
+ const artifacts = [];
81
+ const seen = new Set([targetAbsolute]);
82
+ // Collect import targets and test files in parallel
83
+ const imports = extractImports(fileContent);
84
+ const resolvedImports = await resolveImportPaths(imports, targetDir, basedir);
85
+ const testFiles = await findTestFiles(targetAbsolute, basedir);
86
+ const candidates = [...resolvedImports, ...testFiles];
87
+ // Read all candidates in parallel
88
+ const reads = await Promise.allSettled(candidates.map(async (absPath) => {
89
+ if (seen.has(absPath))
90
+ return null;
91
+ seen.add(absPath);
92
+ // Skip sensitive files (secrets, credentials, keys)
93
+ if (isSensitivePath(absPath))
94
+ return null;
95
+ const validation = await validateFile(absPath, { basedir, rejectBinary: true, maxBytes: 524_288 });
96
+ if (!validation.ok)
97
+ return null;
98
+ const content = await readFile(absPath, 'utf-8');
99
+ const language = detectLanguage(absPath);
100
+ const relativePath = absPath.startsWith(basedir)
101
+ ? absPath.slice(basedir.length + 1)
102
+ : absPath;
103
+ return {
104
+ type: 'file',
105
+ path: relativePath,
106
+ content,
107
+ language,
108
+ label: testFiles.includes(absPath) ? 'test' : 'import',
109
+ };
110
+ }));
111
+ for (const r of reads) {
112
+ if (r.status === 'fulfilled' && r.value) {
113
+ artifacts.push(r.value);
114
+ }
115
+ }
116
+ return artifacts;
117
+ }
118
+ // ── Import extraction ────────────────────────────────────────
119
+ /**
120
+ * Extract relative import paths from source code.
121
+ *
122
+ * Supports:
123
+ * - import ... from './path'
124
+ * - import('./path')
125
+ * - require('./path')
126
+ * - export ... from './path'
127
+ *
128
+ * Only extracts relative paths (starting with ./ or ../).
129
+ * Package imports are excluded.
130
+ *
131
+ * Limitation: single-line only, no tsconfig paths alias support.
132
+ * The function signature is kept simple for future AST migration.
133
+ */
134
+ export function extractImports(content) {
135
+ const imports = new Set();
136
+ // Match: import/export ... from '...' or "..."
137
+ // Also matches: require('...') and import('...')
138
+ const patterns = [
139
+ // import ... from './path' / export ... from './path'
140
+ /(?:import|export)\s+.*?\s+from\s+['"](\.[^'"]+)['"]/g,
141
+ // import './path' (side-effect import)
142
+ /import\s+['"](\.[^'"]+)['"]/g,
143
+ // require('./path')
144
+ /require\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g,
145
+ // import('./path') (dynamic import)
146
+ /import\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g,
147
+ ];
148
+ for (const pattern of patterns) {
149
+ let match;
150
+ while ((match = pattern.exec(content)) !== null) {
151
+ const importPath = match[1];
152
+ if (importPath) {
153
+ imports.add(importPath);
154
+ }
155
+ }
156
+ }
157
+ return [...imports];
158
+ }
159
+ // ── Internal helpers ─────────────────────────────────────────
160
+ async function collectGitStatus(cwd) {
161
+ try {
162
+ return await execGit(['status', '--short'], cwd);
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ }
168
+ async function collectPackageJson(cwd) {
169
+ try {
170
+ return await readFile(join(cwd, 'package.json'), 'utf-8');
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ async function collectDirectoryTree(dir, depth = 0, prefix = '') {
177
+ if (depth > TREE_MAX_DEPTH)
178
+ return null;
179
+ try {
180
+ const entries = await readdir(dir, { withFileTypes: true });
181
+ // Sort: directories first, then files, alphabetical
182
+ entries.sort((a, b) => {
183
+ if (a.isDirectory() !== b.isDirectory()) {
184
+ return a.isDirectory() ? -1 : 1;
185
+ }
186
+ return a.name.localeCompare(b.name);
187
+ });
188
+ const lines = [];
189
+ const filtered = entries.filter((e) => !TREE_EXCLUDE.has(e.name) && !e.name.startsWith('.'));
190
+ for (let i = 0; i < filtered.length; i++) {
191
+ const entry = filtered[i];
192
+ const isLast = i === filtered.length - 1;
193
+ const connector = isLast ? '└── ' : '├── ';
194
+ const childPrefix = isLast ? ' ' : '│ ';
195
+ if (entry.isDirectory()) {
196
+ lines.push(prefix + connector + entry.name + '/');
197
+ const subtree = await collectDirectoryTree(join(dir, entry.name), depth + 1, prefix + childPrefix);
198
+ if (subtree)
199
+ lines.push(subtree);
200
+ }
201
+ else {
202
+ lines.push(prefix + connector + entry.name);
203
+ }
204
+ }
205
+ return lines.join('\n');
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
211
+ /**
212
+ * Resolve relative import paths to absolute file paths.
213
+ * Tries multiple extensions and index files.
214
+ * All resolved paths are confined within basedir.
215
+ */
216
+ async function resolveImportPaths(imports, fromDir, basedir) {
217
+ const resolved = [];
218
+ const results = await Promise.allSettled(imports.map(async (imp) => {
219
+ const base = resolve(fromDir, imp);
220
+ // Basedir confinement: reject paths escaping the project root
221
+ if (basedir) {
222
+ const rel = relative(basedir, base);
223
+ if (rel.startsWith('..') || isAbsolute(rel))
224
+ return null;
225
+ }
226
+ // Try exact path first (already has extension)
227
+ const ext = extname(base);
228
+ if (ext) {
229
+ try {
230
+ const s = await stat(base);
231
+ if (s.isFile())
232
+ return base;
233
+ }
234
+ catch { /* continue */ }
235
+ // TypeScript convention: import './foo.js' may refer to './foo.ts'
236
+ // Try replacing .js/.mjs/.cjs with .ts/.mts/.cts and .tsx
237
+ const remapped = remapExtension(base, ext);
238
+ for (const candidate of remapped) {
239
+ try {
240
+ const s = await stat(candidate);
241
+ if (s.isFile())
242
+ return candidate;
243
+ }
244
+ catch { /* continue */ }
245
+ }
246
+ }
247
+ // Try appending extensions (for extensionless imports)
248
+ for (const tryExt of RESOLVE_EXTENSIONS) {
249
+ try {
250
+ const candidate = base + tryExt;
251
+ const s = await stat(candidate);
252
+ if (s.isFile())
253
+ return candidate;
254
+ }
255
+ catch { /* continue */ }
256
+ }
257
+ // Try as directory with index file
258
+ for (const idx of INDEX_FILES) {
259
+ try {
260
+ const candidate = join(base, idx);
261
+ const s = await stat(candidate);
262
+ if (s.isFile())
263
+ return candidate;
264
+ }
265
+ catch { /* continue */ }
266
+ }
267
+ return null;
268
+ }));
269
+ for (const r of results) {
270
+ if (r.status === 'fulfilled' && r.value) {
271
+ resolved.push(r.value);
272
+ }
273
+ }
274
+ return resolved;
275
+ }
276
+ /** JS/MJS/CJS → TS/MTS/CTS extension remap (TypeScript convention) */
277
+ const EXTENSION_REMAP = {
278
+ '.js': ['.ts', '.tsx'],
279
+ '.jsx': ['.tsx', '.ts'],
280
+ '.mjs': ['.mts'],
281
+ '.cjs': ['.cts'],
282
+ };
283
+ function remapExtension(filePath, ext) {
284
+ const targets = EXTENSION_REMAP[ext];
285
+ if (!targets)
286
+ return [];
287
+ const base = filePath.slice(0, -ext.length);
288
+ return targets.map((t) => base + t);
289
+ }
290
+ /**
291
+ * Find test files associated with a target file.
292
+ *
293
+ * Looks for:
294
+ * - Same directory: foo.test.ts, foo.spec.ts
295
+ * - test/ directory: test/foo.test.ts
296
+ * - test/unit/ directory: test/unit/<subdir>/foo.test.ts
297
+ * - __tests__/ directory: __tests__/foo.test.ts
298
+ */
299
+ async function findTestFiles(targetAbsolute, basedir) {
300
+ const found = [];
301
+ const targetBase = basename(targetAbsolute);
302
+ const nameWithoutExt = targetBase.replace(/\.[^.]+$/, '');
303
+ const targetDir = dirname(targetAbsolute);
304
+ // Derive relative path from basedir to figure out test/unit/<subdir>
305
+ const relDir = targetDir.startsWith(basedir)
306
+ ? targetDir.slice(basedir.length + 1)
307
+ : '';
308
+ // e.g. "src/utils" → strip "src/" → "utils"
309
+ const subdir = relDir.replace(/^src\//, '');
310
+ const candidates = [
311
+ // Same directory
312
+ join(targetDir, `${nameWithoutExt}.test.ts`),
313
+ join(targetDir, `${nameWithoutExt}.spec.ts`),
314
+ join(targetDir, `${nameWithoutExt}.test.js`),
315
+ // test/ root
316
+ join(basedir, 'test', `${nameWithoutExt}.test.ts`),
317
+ // test/unit/<subdir>/
318
+ ...(subdir ? [join(basedir, 'test', 'unit', subdir, `${nameWithoutExt}.test.ts`)] : []),
319
+ // __tests__/
320
+ join(targetDir, '__tests__', `${nameWithoutExt}.test.ts`),
321
+ join(targetDir, '__tests__', `${nameWithoutExt}.test.js`),
322
+ ];
323
+ const checks = await Promise.allSettled(candidates.map(async (path) => {
324
+ const s = await stat(path);
325
+ return s.isFile() ? path : null;
326
+ }));
327
+ for (const r of checks) {
328
+ if (r.status === 'fulfilled' && r.value) {
329
+ found.push(r.value);
330
+ }
331
+ }
332
+ return found;
333
+ }
334
+ /** Check if a file path matches any sensitive file pattern. */
335
+ function isSensitivePath(filePath) {
336
+ return SENSITIVE_PATTERNS.some((pattern) => pattern.test(filePath));
337
+ }
@@ -0,0 +1,35 @@
1
+ import type { MagiTask, DeliberationRound, MagiDeliberation } from '../types/core.js';
2
+ import { MagiError } from '../utils/errors.js';
3
+ export declare class PathTraversalError extends MagiError {
4
+ constructor(message: string);
5
+ }
6
+ /**
7
+ * File-System-as-State context manager with path traversal prevention.
8
+ *
9
+ * Manages the .magi/ workspace directory where deliberation state is persisted.
10
+ * Each deliberation gets its own subdirectory with round-by-round JSON files.
11
+ *
12
+ * Security hardening:
13
+ * - Deliberation IDs are validated as UUID v4 format
14
+ * - All paths are resolved and verified to stay within the base directory
15
+ * - Null bytes are rejected
16
+ * - Symlinks at target directories are rejected
17
+ * - File permissions: directories 0o700, files 0o600
18
+ */
19
+ export declare class ContextManager {
20
+ private readonly resolvedBaseDir;
21
+ constructor(baseDir: string);
22
+ /** Validate deliberation ID format and resolve path safely. */
23
+ private validateAndResolvePath;
24
+ /** Create directory with 0o700 permissions, rejecting symlinks. */
25
+ private safeMkdir;
26
+ /** Write file with 0o600 permissions. */
27
+ private safeWriteFile;
28
+ /** Write JSON with safe permissions. */
29
+ private writeJson;
30
+ initWorkspace(deliberationId: string, task: MagiTask): Promise<string>;
31
+ saveRound(deliberationId: string, round: DeliberationRound): Promise<void>;
32
+ saveDeliberation(deliberation: MagiDeliberation): Promise<void>;
33
+ listDeliberations(): Promise<string[]>;
34
+ loadTask(deliberationId: string): Promise<MagiTask | null>;
35
+ }