@cortexkit/opencode-magic-context 0.33.1 → 0.34.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 (240) hide show
  1. package/README.md +20 -3
  2. package/dist/config/agent-disable.d.ts +22 -0
  3. package/dist/config/agent-disable.d.ts.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/migrate-experimental.d.ts +2 -1
  6. package/dist/config/migrate-experimental.d.ts.map +1 -1
  7. package/dist/config/project-security.d.ts +9 -0
  8. package/dist/config/project-security.d.ts.map +1 -1
  9. package/dist/config/schema/magic-context.d.ts +90 -17
  10. package/dist/config/schema/magic-context.d.ts.map +1 -1
  11. package/dist/config/transform-mode.d.ts +2 -0
  12. package/dist/config/transform-mode.d.ts.map +1 -1
  13. package/dist/{conflict-warning-hook-n48c783v.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
  14. package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
  15. package/dist/features/builtin-commands/commands.d.ts +1 -1
  16. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  17. package/dist/features/magic-context/compaction-marker.d.ts +57 -0
  18. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  19. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/classify.d.ts +1 -0
  21. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/map-memories.d.ts +1 -0
  24. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +1 -0
  26. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  27. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +2 -0
  28. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  29. package/dist/features/magic-context/dreamer/task-executor.d.ts +4 -1
  30. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  31. package/dist/features/magic-context/dreamer/task-gates.d.ts +19 -1
  32. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  33. package/dist/features/magic-context/dreamer/task-registry.d.ts +28 -0
  34. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  35. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +7 -1
  36. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  37. package/dist/features/magic-context/dreamer/verify.d.ts +1 -0
  38. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  39. package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
  40. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  41. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
  42. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
  43. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  44. package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
  45. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  46. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  47. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
  48. package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
  49. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  50. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  51. package/dist/features/magic-context/migrations.d.ts +23 -1
  52. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  53. package/dist/features/magic-context/mural/compress-cues.d.ts +1 -0
  54. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  55. package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
  56. package/dist/features/magic-context/overflow-detection.d.ts +8 -1
  57. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  58. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  59. package/dist/features/magic-context/scheduler.d.ts.map +1 -1
  60. package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
  61. package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
  62. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  63. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  64. package/dist/features/magic-context/storage-db.d.ts +37 -2
  65. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  66. package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -4
  67. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  68. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  69. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  70. package/dist/features/magic-context/storage.d.ts +2 -2
  71. package/dist/features/magic-context/storage.d.ts.map +1 -1
  72. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
  74. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -0
  76. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
  78. package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
  79. package/dist/hooks/magic-context/command-handler.d.ts +3 -0
  80. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
  82. package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
  83. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
  84. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
  90. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
  92. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
  94. package/dist/hooks/magic-context/event-handler.d.ts +7 -0
  95. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
  97. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/execute-status.d.ts +5 -1
  99. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  101. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
  103. package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
  104. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/hook.d.ts +14 -6
  106. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/inject-compartments.d.ts +10 -1
  108. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/live-session-state.d.ts +3 -0
  110. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  111. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
  115. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  116. package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
  117. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
  119. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  120. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +31 -1
  121. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/transform.d.ts +29 -2
  123. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  124. package/dist/{index-56yjaeb2.js → index-2y0ybv5x.js} +11 -1
  125. package/dist/{index-ey1bc14z.js → index-b563sga5.js} +7 -264
  126. package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
  127. package/dist/{index-xyaekybw.js → index-dtas91s5.js} +6 -4
  128. package/dist/index-fcyvznch.js +279 -0
  129. package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
  130. package/dist/{index-c2gahrks.js → index-x8spt5wy.js} +1682 -893
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +1677 -723
  133. package/dist/plugin/dream-timer.d.ts +4 -2
  134. package/dist/plugin/dream-timer.d.ts.map +1 -1
  135. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  136. package/dist/plugin/hooks/create-session-hooks.d.ts +12 -1
  137. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  138. package/dist/plugin/messages-transform.d.ts +10 -0
  139. package/dist/plugin/messages-transform.d.ts.map +1 -1
  140. package/dist/plugin/rpc-handlers.d.ts +3 -7
  141. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  142. package/dist/plugin/tool-registry.d.ts +8 -0
  143. package/dist/plugin/tool-registry.d.ts.map +1 -1
  144. package/dist/{recomp-orchestrator-2yb2hfh2.js → recomp-orchestrator-k72ejkkh.js} +6 -4
  145. package/dist/send-session-notification-6njqewzk.js +20 -0
  146. package/dist/shared/conflict-detector.d.ts +38 -1
  147. package/dist/shared/conflict-detector.d.ts.map +1 -1
  148. package/dist/shared/conflict-fixer.d.ts +18 -1
  149. package/dist/shared/conflict-fixer.d.ts.map +1 -1
  150. package/dist/shared/context-limit-provenance.d.ts +3 -0
  151. package/dist/shared/context-limit-provenance.d.ts.map +1 -0
  152. package/dist/shared/escalation-bands.d.ts +9 -0
  153. package/dist/shared/escalation-bands.d.ts.map +1 -0
  154. package/dist/shared/format-threshold.d.ts +1 -1
  155. package/dist/shared/models-dev-cache.d.ts +29 -3
  156. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  157. package/dist/shared/rpc-server.d.ts.map +1 -1
  158. package/dist/shared/rpc-types.d.ts +51 -1
  159. package/dist/shared/rpc-types.d.ts.map +1 -1
  160. package/dist/shared/rpc-utils.d.ts +19 -0
  161. package/dist/shared/rpc-utils.d.ts.map +1 -1
  162. package/dist/shared/storage-permissions.d.ts +5 -0
  163. package/dist/shared/storage-permissions.d.ts.map +1 -0
  164. package/dist/shared/tui-runtime-specifiers.d.ts +20 -0
  165. package/dist/shared/tui-runtime-specifiers.d.ts.map +1 -0
  166. package/dist/{storage-meta-session-td6bwtky.js → storage-meta-session-xt41zbw4.js} +2 -1
  167. package/dist/{task-config-5nx3qfz8.js → task-config-gbp7r23g.js} +1 -1
  168. package/dist/tui/compaction-off.d.ts +13 -0
  169. package/dist/tui/compaction-off.d.ts.map +1 -0
  170. package/dist/tui/data/context-db.d.ts.map +1 -1
  171. package/dist/tui-compiled/compaction-off.d.ts +13 -0
  172. package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
  173. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  174. package/package.json +6 -1
  175. package/src/agents/dreamer.ts +68 -0
  176. package/src/agents/hidden-agent-registrations.ts +331 -0
  177. package/src/agents/historian.ts +3 -0
  178. package/src/agents/language-directive.test.ts +138 -0
  179. package/src/agents/language-directive.ts +121 -0
  180. package/src/agents/magic-context-prompt.language.test.ts +35 -0
  181. package/src/agents/magic-context-prompt.test.ts +292 -0
  182. package/src/agents/magic-context-prompt.ts +182 -0
  183. package/src/agents/permissions.test.ts +252 -0
  184. package/src/agents/permissions.ts +186 -0
  185. package/src/agents/sidekick.ts +1 -0
  186. package/src/agents/smart-note-compiler.ts +1 -0
  187. package/src/config/agent-disable.ts +106 -0
  188. package/src/config/compaction-accessor-guard.test.ts +82 -0
  189. package/src/config/compaction-backcompat.test.ts +52 -0
  190. package/src/config/index.test.ts +936 -0
  191. package/src/config/index.ts +601 -0
  192. package/src/config/migrate-config-location.test.ts +332 -0
  193. package/src/config/migrate-config-location.ts +635 -0
  194. package/src/config/migrate-dreamer-v2.test.ts +287 -0
  195. package/src/config/migrate-dreamer-v2.ts +336 -0
  196. package/src/config/migrate-experimental.test.ts +41 -0
  197. package/src/config/migrate-experimental.ts +185 -0
  198. package/src/config/project-security.test.ts +388 -0
  199. package/src/config/project-security.ts +611 -0
  200. package/src/config/prune-config-leaf.test.ts +78 -0
  201. package/src/config/prune-config-leaf.ts +68 -0
  202. package/src/config/removed-config-keys.test.ts +116 -0
  203. package/src/config/schema/agent-overrides.ts +45 -0
  204. package/src/config/schema/magic-context.test.ts +359 -0
  205. package/src/config/schema/magic-context.ts +991 -0
  206. package/src/config/transform-mode.test.ts +62 -0
  207. package/src/config/transform-mode.ts +34 -0
  208. package/src/config/variable.test.ts +323 -0
  209. package/src/config/variable.ts +192 -0
  210. package/src/features/magic-context/defaults.ts +1 -0
  211. package/src/features/magic-context/dreamer/cron.ts +243 -0
  212. package/src/features/magic-context/dreamer/task-registry.ts +151 -0
  213. package/src/shared/conflict-detector.test.ts +121 -0
  214. package/src/shared/conflict-detector.ts +50 -3
  215. package/src/shared/conflict-fixer.test.ts +309 -0
  216. package/src/shared/conflict-fixer.ts +82 -5
  217. package/src/shared/context-limit-provenance.ts +6 -0
  218. package/src/shared/escalation-bands.test.ts +23 -0
  219. package/src/shared/escalation-bands.ts +18 -0
  220. package/src/shared/format-threshold.ts +1 -1
  221. package/src/shared/models-dev-cache.test.ts +171 -0
  222. package/src/shared/models-dev-cache.ts +195 -38
  223. package/src/shared/rpc-server.ts +27 -18
  224. package/src/shared/rpc-types.ts +55 -1
  225. package/src/shared/rpc-utils.test.ts +157 -0
  226. package/src/shared/rpc-utils.ts +137 -0
  227. package/src/shared/storage-permissions.ts +20 -0
  228. package/src/shared/tui-runtime-specifiers.ts +32 -0
  229. package/src/tui/compaction-off.test.ts +56 -0
  230. package/src/tui/compaction-off.ts +29 -0
  231. package/src/tui/data/context-db.ts +6 -0
  232. package/src/tui/index.tsx +113 -65
  233. package/src/tui/slots/sidebar-content.tsx +189 -91
  234. package/src/tui/tui-compiled-runtime-imports.test.ts +151 -0
  235. package/src/tui-compiled/compaction-off.ts +29 -0
  236. package/src/tui-compiled/data/context-db.ts +6 -0
  237. package/src/tui-compiled/index.tsx +558 -455
  238. package/src/tui-compiled/slots/sidebar-content.tsx +334 -169
  239. package/dist/send-session-notification-tghq9gnh.js +0 -11
  240. /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
@@ -0,0 +1,635 @@
1
+ import {
2
+ closeSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ openSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ statSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { basename, dirname, isAbsolute, join } from "node:path";
15
+
16
+ /**
17
+ * Config-LOCATION migration: move Magic Context config from the per-harness
18
+ * legacy locations to one shared CortexKit location, mirroring AFT's proven
19
+ * move-and-marker design (NOT copy-in-place, which is a silent stale-edit trap).
20
+ *
21
+ * Legacy (read by old builds):
22
+ * user: ~/.config/opencode/magic-context.{jsonc,json}
23
+ * ~/.pi/agent/magic-context.{jsonc,json}
24
+ * project: <root>/magic-context.{jsonc,json} (bare root)
25
+ * <root>/.opencode/magic-context.{jsonc,json}
26
+ * <root>/.pi/magic-context.{jsonc,json}
27
+ *
28
+ * Target (the only thing new builds read — HARD CUTOVER, harness-agnostic):
29
+ * user: ~/.config/cortexkit/magic-context.jsonc
30
+ * project: <root>/.cortexkit/magic-context.jsonc
31
+ *
32
+ * The migrator runs at plugin init before the loader. Idempotency comes from
33
+ * the legacy file being renamed away (not from a sentinel): once a source is
34
+ * moved aside to `<name>.MOVED_READPLEASE`, later runs find no legacy source
35
+ * and no-op. The marker is a pure human breadcrumb — the loader never reads it.
36
+ *
37
+ * NB: this is config-LOCATION migration. It is unrelated to the schema/key
38
+ * migrations (migrate-dreamer-v2, migrate-experimental) that rewrite keys
39
+ * INSIDE an already-located config file.
40
+ */
41
+
42
+ export interface LegacyConfigSource {
43
+ path: string;
44
+ label: string;
45
+ }
46
+
47
+ export interface ConfigMigrationLogger {
48
+ warn?: (msg: string) => void;
49
+ info?: (msg: string) => void;
50
+ log?: (msg: string) => void;
51
+ }
52
+
53
+ export interface ConfigFileMigrationOptions {
54
+ scope: "user" | "project";
55
+ targetPath: string;
56
+ legacySources: readonly LegacyConfigSource[];
57
+ logger?: ConfigMigrationLogger;
58
+ }
59
+
60
+ export interface ConfigFileMigrationResult {
61
+ migrated: boolean;
62
+ conflict: boolean;
63
+ sourcePath?: string;
64
+ targetPath: string;
65
+ warnings: string[];
66
+ }
67
+
68
+ const CONFIG_FILE_BASENAME = "magic-context";
69
+ const MOVED_MARKER_SUFFIX = ".MOVED_READPLEASE";
70
+
71
+ // ── Path resolution ──────────────────────────────────────────
72
+
73
+ function homeDir(): string {
74
+ if (process.platform === "win32") {
75
+ return process.env.USERPROFILE || process.env.HOME || homedir();
76
+ }
77
+ return process.env.HOME || homedir();
78
+ }
79
+
80
+ function configHome(): string {
81
+ const xdg = process.env.XDG_CONFIG_HOME;
82
+ if (xdg && isAbsolute(xdg)) return xdg;
83
+ return join(homeDir(), ".config");
84
+ }
85
+
86
+ /** `~/.config/cortexkit/magic-context` (no extension — for detectConfigFile). */
87
+ export function cortexKitUserConfigBasePath(): string {
88
+ return join(configHome(), "cortexkit", CONFIG_FILE_BASENAME);
89
+ }
90
+
91
+ /** `<root>/.cortexkit/magic-context` (no extension — for detectConfigFile). */
92
+ export function cortexKitProjectConfigBasePath(directory: string): string {
93
+ return join(directory, ".cortexkit", CONFIG_FILE_BASENAME);
94
+ }
95
+
96
+ /** The migration target: always normalized to `.jsonc`. */
97
+ export function resolveCortexKitUserConfigPath(): string {
98
+ return `${cortexKitUserConfigBasePath()}.jsonc`;
99
+ }
100
+
101
+ /** The migration target: always normalized to `.jsonc`. */
102
+ export function resolveCortexKitProjectConfigPath(directory: string): string {
103
+ return `${cortexKitProjectConfigBasePath(directory)}.jsonc`;
104
+ }
105
+
106
+ function legacySourcesForBase(basePath: string, label: string): LegacyConfigSource[] {
107
+ return [
108
+ { path: `${basePath}.jsonc`, label: `${label} magic-context.jsonc` },
109
+ { path: `${basePath}.json`, label: `${label} magic-context.json` },
110
+ ];
111
+ }
112
+
113
+ /**
114
+ * Every user-scope config path the migrator knows: the CortexKit user target
115
+ * plus the user legacy sources. A project-scope migration must NEVER move any of
116
+ * these (they are user-scope files, not project files).
117
+ */
118
+ function userScopeConfigPaths(): Set<string> {
119
+ return new Set<string>([
120
+ // The CortexKit user target, both extensions: the bare-root project
121
+ // source produces `.jsonc` AND `.json`, so exclude both or a stray
122
+ // `~/.config/cortexkit/magic-context.json` would still be eaten.
123
+ `${cortexKitUserConfigBasePath()}.jsonc`,
124
+ `${cortexKitUserConfigBasePath()}.json`,
125
+ join(configHome(), "opencode", `${CONFIG_FILE_BASENAME}.jsonc`),
126
+ join(configHome(), "opencode", `${CONFIG_FILE_BASENAME}.json`),
127
+ join(homeDir(), ".pi", "agent", `${CONFIG_FILE_BASENAME}.jsonc`),
128
+ join(homeDir(), ".pi", "agent", `${CONFIG_FILE_BASENAME}.json`),
129
+ ]);
130
+ }
131
+
132
+ /**
133
+ * The legacy config locations to migrate FROM, by scope. Each base produces a
134
+ * `.jsonc` and a `.json` candidate; whichever exists migrates, target is always
135
+ * `.jsonc`. The bare-root project source (`<root>/magic-context.*`) is unique to
136
+ * Magic Context (AFT never had it) — omitting it would orphan repo-root configs.
137
+ *
138
+ * Project sources are filtered against the user-scope path set: when a session's
139
+ * project directory IS the user config home (e.g. opencode opened in
140
+ * `~/.config/cortexkit`), the bare-root project source `<root>/magic-context.jsonc`
141
+ * resolves to the USER config path. Without this guard the project migration
142
+ * would "migrate" the user's own config into `<root>/.cortexkit/` and rename the
143
+ * original aside, leaving the user on schema defaults (the config-eats-itself
144
+ * bug). A project migration must never touch a user-scope file.
145
+ */
146
+ export function resolveLegacyConfigSources(directory: string): {
147
+ user: LegacyConfigSource[];
148
+ project: LegacyConfigSource[];
149
+ } {
150
+ const userPaths = userScopeConfigPaths();
151
+ return {
152
+ user: [
153
+ ...legacySourcesForBase(
154
+ join(configHome(), "opencode", CONFIG_FILE_BASENAME),
155
+ "OpenCode user",
156
+ ),
157
+ ...legacySourcesForBase(
158
+ join(homeDir(), ".pi", "agent", CONFIG_FILE_BASENAME),
159
+ "Pi user",
160
+ ),
161
+ ],
162
+ project: [
163
+ ...legacySourcesForBase(join(directory, CONFIG_FILE_BASENAME), "project root"),
164
+ ...legacySourcesForBase(
165
+ join(directory, ".opencode", CONFIG_FILE_BASENAME),
166
+ "OpenCode project",
167
+ ),
168
+ ...legacySourcesForBase(join(directory, ".pi", CONFIG_FILE_BASENAME), "Pi project"),
169
+ ].filter((source) => !userPaths.has(source.path)),
170
+ };
171
+ }
172
+
173
+ export type ConfigHarness = "opencode" | "pi";
174
+
175
+ /**
176
+ * Legacy sources owned by ONE harness, most-specific first. Used by the loaders
177
+ * as a NON-DESTRUCTIVE read fallback: when the shared CortexKit base is absent
178
+ * (migration not yet run, or refused because OpenCode and Pi legacy configs
179
+ * differ), the running harness reads its OWN legacy config rather than silently
180
+ * falling back to schema defaults — which would re-enable features the legacy
181
+ * config disabled. Each harness reads only its own files, so a differing pair
182
+ * stays correct per-harness until the user consolidates. The bare project-root
183
+ * `<root>/magic-context.*` was OpenCode-only historically.
184
+ */
185
+ export function resolveLegacyConfigSourcesForHarness(
186
+ directory: string,
187
+ harness: ConfigHarness,
188
+ ): { user: LegacyConfigSource[]; project: LegacyConfigSource[] } {
189
+ if (harness === "pi") {
190
+ return {
191
+ user: legacySourcesForBase(
192
+ join(homeDir(), ".pi", "agent", CONFIG_FILE_BASENAME),
193
+ "Pi user",
194
+ ),
195
+ project: legacySourcesForBase(
196
+ join(directory, ".pi", CONFIG_FILE_BASENAME),
197
+ "Pi project",
198
+ ),
199
+ };
200
+ }
201
+ return {
202
+ user: legacySourcesForBase(
203
+ join(configHome(), "opencode", CONFIG_FILE_BASENAME),
204
+ "OpenCode user",
205
+ ),
206
+ project: [
207
+ ...legacySourcesForBase(join(directory, CONFIG_FILE_BASENAME), "project root"),
208
+ ...legacySourcesForBase(
209
+ join(directory, ".opencode", CONFIG_FILE_BASENAME),
210
+ "OpenCode project",
211
+ ),
212
+ ],
213
+ };
214
+ }
215
+
216
+ // ── JSONC semantics comparison (dependency-free, ported from AFT) ─────────────
217
+ // A legacy source that semantically MATCHES an existing target is moved aside
218
+ // (target wins); one that DIFFERS triggers refuse-and-warn (never auto-clobber).
219
+ // We strip comments/trailing-commas and sort keys so formatting/comment/order
220
+ // differences don't read as a conflict.
221
+
222
+ function stripJsoncForParse(input: string): string {
223
+ let out = "";
224
+ let inString = false;
225
+ let escaped = false;
226
+ for (let i = 0; i < input.length; i++) {
227
+ const ch = input[i];
228
+ const next = input[i + 1];
229
+ if (inString) {
230
+ out += ch;
231
+ if (escaped) escaped = false;
232
+ else if (ch === "\\") escaped = true;
233
+ else if (ch === '"') inString = false;
234
+ continue;
235
+ }
236
+ if (ch === '"') {
237
+ inString = true;
238
+ out += ch;
239
+ continue;
240
+ }
241
+ if (ch === "/" && next === "/") {
242
+ while (i < input.length && input[i] !== "\n") i++;
243
+ out += "\n";
244
+ continue;
245
+ }
246
+ if (ch === "/" && next === "*") {
247
+ i += 2;
248
+ while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
249
+ i++;
250
+ out += " ";
251
+ continue;
252
+ }
253
+ out += ch;
254
+ }
255
+ let withoutTrailingCommas = "";
256
+ inString = false;
257
+ escaped = false;
258
+ for (let i = 0; i < out.length; i++) {
259
+ const ch = out[i];
260
+ if (inString) {
261
+ withoutTrailingCommas += ch;
262
+ if (escaped) escaped = false;
263
+ else if (ch === "\\") escaped = true;
264
+ else if (ch === '"') inString = false;
265
+ continue;
266
+ }
267
+ if (ch === '"') {
268
+ inString = true;
269
+ withoutTrailingCommas += ch;
270
+ continue;
271
+ }
272
+ if (ch === ",") {
273
+ let j = i + 1;
274
+ while (j < out.length && /\s/.test(out[j])) j++;
275
+ if (out[j] === "}" || out[j] === "]") continue;
276
+ }
277
+ withoutTrailingCommas += ch;
278
+ }
279
+ return withoutTrailingCommas;
280
+ }
281
+
282
+ function sortJson(value: unknown): unknown {
283
+ if (Array.isArray(value)) return value.map(sortJson);
284
+ if (value && typeof value === "object") {
285
+ const sorted: Record<string, unknown> = {};
286
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
287
+ sorted[key] = sortJson((value as Record<string, unknown>)[key]);
288
+ }
289
+ return sorted;
290
+ }
291
+ return value;
292
+ }
293
+
294
+ function normalizedJsoncSemantics(content: string): string {
295
+ return JSON.stringify(sortJson(JSON.parse(stripJsoncForParse(content))));
296
+ }
297
+
298
+ function fileSemanticsMatch(a: string, b: string): boolean {
299
+ try {
300
+ return normalizedJsoncSemantics(a) === normalizedJsoncSemantics(b);
301
+ } catch {
302
+ // If either side can't be parsed (truly malformed), fall back to an
303
+ // exact-bytes comparison — a parse-broken legacy file that differs byte
304
+ // for byte is conservatively treated as a conflict, never auto-clobbered.
305
+ return a === b;
306
+ }
307
+ }
308
+
309
+ // ── Cross-process lock (Desktop runs many instances concurrently) ────────────
310
+
311
+ // A real migration is a sub-second rename+write, so a lock older than this was
312
+ // almost certainly left by a crashed holder and is safe to reclaim.
313
+ const CONFIG_LOCK_STALE_MS = 4_000;
314
+
315
+ /**
316
+ * Directory-mkdir lock. `mkdir` is atomic; EEXIST means another instance holds
317
+ * it. We do NOT block plugin init: migration is not correctness-critical this
318
+ * run (the loaders read-legacy-on-conflict when the CortexKit base is absent),
319
+ * so on a LIVE competing holder we return null immediately and skip migration
320
+ * this run — the holder finishes its sub-second rename+write, and the next init
321
+ * sees target-present and consolidates. The ONLY retry is a single one-shot
322
+ * stale reclaim: a crashed holder's lock dir (older than the stale threshold) is
323
+ * removed and we re-attempt mkdir once. No busy-wait, no synchronous sleep — the
324
+ * init path never blocks on lock contention.
325
+ */
326
+ function acquireConfigMigrationLock(lockDir: string): (() => void) | null {
327
+ for (let attempt = 0; attempt < 2; attempt++) {
328
+ try {
329
+ mkdirSync(lockDir, { recursive: false });
330
+ return () => {
331
+ try {
332
+ rmSync(lockDir, { recursive: true, force: true });
333
+ } catch {
334
+ // best-effort lock cleanup
335
+ }
336
+ };
337
+ } catch (err) {
338
+ const code = (err as { code?: unknown })?.code;
339
+ if (code !== "EEXIST") throw err;
340
+ // EEXIST: another instance holds the lock. Reclaim ONLY if it's stale
341
+ // (crashed holder) and retry mkdir once; otherwise a live holder owns
342
+ // it → skip this run rather than block init.
343
+ try {
344
+ const ageMs = Date.now() - statSync(lockDir).mtimeMs;
345
+ if (ageMs > CONFIG_LOCK_STALE_MS) {
346
+ rmSync(lockDir, { recursive: true, force: true });
347
+ continue;
348
+ }
349
+ } catch {
350
+ // Lock vanished between stat and now — retry mkdir once.
351
+ continue;
352
+ }
353
+ return null;
354
+ }
355
+ }
356
+ return null;
357
+ }
358
+
359
+ // ── Atomic writes ────────────────────────────────────────────
360
+
361
+ function atomicCopyConfigFile(sourcePath: string, targetPath: string): void {
362
+ mkdirSync(dirname(targetPath), { recursive: true });
363
+ const tmpPath = join(
364
+ dirname(targetPath),
365
+ `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`,
366
+ );
367
+ let fd: number | null = null;
368
+ try {
369
+ fd = openSync(tmpPath, "wx", 0o600);
370
+ writeFileSync(fd, readFileSync(sourcePath));
371
+ closeSync(fd);
372
+ fd = null;
373
+ renameSync(tmpPath, targetPath);
374
+ } catch (err) {
375
+ if (fd !== null) {
376
+ try {
377
+ closeSync(fd);
378
+ } catch {
379
+ // best-effort close before cleanup
380
+ }
381
+ }
382
+ try {
383
+ unlinkSync(tmpPath);
384
+ } catch {
385
+ // best-effort temp cleanup
386
+ }
387
+ throw err;
388
+ }
389
+ }
390
+
391
+ function atomicWriteConfigFile(targetPath: string, content: string): void {
392
+ mkdirSync(dirname(targetPath), { recursive: true });
393
+ const tmpPath = join(
394
+ dirname(targetPath),
395
+ `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`,
396
+ );
397
+ let fd: number | null = null;
398
+ try {
399
+ fd = openSync(tmpPath, "wx", 0o600);
400
+ writeFileSync(fd, content);
401
+ closeSync(fd);
402
+ fd = null;
403
+ renameSync(tmpPath, targetPath);
404
+ } catch (err) {
405
+ if (fd !== null) {
406
+ try {
407
+ closeSync(fd);
408
+ } catch {
409
+ // best-effort close before cleanup
410
+ }
411
+ }
412
+ try {
413
+ unlinkSync(tmpPath);
414
+ } catch {
415
+ // best-effort temp cleanup
416
+ }
417
+ throw err;
418
+ }
419
+ }
420
+
421
+ // ── Marker ───────────────────────────────────────────────────
422
+
423
+ function movedMarkerContent(
424
+ targetPath: string,
425
+ originalName: string,
426
+ originalContent: string,
427
+ ): string {
428
+ const header = [
429
+ "// Magic Context configuration moved.",
430
+ "//",
431
+ "// Magic Context now reads its configuration from one shared CortexKit",
432
+ "// location instead of a per-agent path. The settings that were in this",
433
+ "// file have been moved to:",
434
+ "//",
435
+ `// ${targetPath}`,
436
+ "//",
437
+ "// Edit that file to change Magic Context settings. This location is no",
438
+ "// longer read by Magic Context.",
439
+ "//",
440
+ `// To undo, rename this file back to "${originalName}" (and remove the`,
441
+ "// CortexKit copy above if you want this location to take precedence).",
442
+ "//",
443
+ "// Your original settings are preserved below for reference.",
444
+ "",
445
+ "",
446
+ ].join("\n");
447
+ return `${header}${originalContent}`;
448
+ }
449
+
450
+ /**
451
+ * After the live config is safely at the CortexKit target, rename each legacy
452
+ * source aside to `<name>.MOVED_READPLEASE` so a user editing the old path
453
+ * notices it is no longer read (a copy-in-place leaves a silent stale-edit
454
+ * trap). A failure here is non-fatal: the content is already at the target, so
455
+ * we warn and leave the legacy file in place.
456
+ */
457
+ function markLegacySourcesMovedAside(
458
+ sources: readonly { path: string }[],
459
+ targetPath: string,
460
+ logger?: ConfigMigrationLogger,
461
+ ): string[] {
462
+ const warnings: string[] = [];
463
+ const info = logger?.info ?? logger?.log;
464
+ for (const source of sources) {
465
+ const markerPath = `${source.path}${MOVED_MARKER_SUFFIX}`;
466
+ try {
467
+ const original = readFileSync(source.path, "utf-8");
468
+ atomicWriteConfigFile(
469
+ markerPath,
470
+ movedMarkerContent(targetPath, basename(source.path), original),
471
+ );
472
+ unlinkSync(source.path);
473
+ info?.(
474
+ `Moved legacy Magic Context config ${source.path} aside to ${markerPath}; now reading ${targetPath}`,
475
+ );
476
+ } catch (err) {
477
+ const msg = err instanceof Error ? err.message : String(err);
478
+ warnings.push(
479
+ `Magic Context could not move legacy config ${source.path} aside (${msg}); it is now stale and ignored. Delete it manually — config is read from ${targetPath}.`,
480
+ );
481
+ logger?.warn?.(
482
+ `Could not move legacy Magic Context config ${source.path} aside (${msg}); reading ${targetPath}`,
483
+ );
484
+ }
485
+ }
486
+ return warnings;
487
+ }
488
+
489
+ function visibleConfigMigrationWarning(
490
+ scope: "user" | "project",
491
+ targetPath: string,
492
+ paths: readonly string[],
493
+ reason: string,
494
+ ): string {
495
+ const uniquePaths = [...new Set([targetPath, ...paths])];
496
+ return (
497
+ `Magic Context ${scope} config migration refused: ${reason}. ` +
498
+ `Legacy and CortexKit config paths collapse to one file, but Magic Context will not overwrite or merge them automatically. ` +
499
+ `Please consolidate manually into ${targetPath}. Paths: ${uniquePaths.join(" ; ")}`
500
+ );
501
+ }
502
+
503
+ // ── Core per-file migration (ported from AFT migrateAftConfigFile) ───────────
504
+
505
+ export function migrateConfigFile(opts: ConfigFileMigrationOptions): ConfigFileMigrationResult {
506
+ const warnings: string[] = [];
507
+ const existingSources = opts.legacySources.filter((source) => existsSync(source.path));
508
+ const info = opts.logger?.info ?? opts.logger?.log;
509
+
510
+ if (existingSources.length === 0) {
511
+ return { migrated: false, conflict: false, targetPath: opts.targetPath, warnings };
512
+ }
513
+
514
+ mkdirSync(dirname(opts.targetPath), { recursive: true });
515
+ const release = acquireConfigMigrationLock(`${opts.targetPath}.lock`);
516
+ if (!release) {
517
+ // Another instance holds the lock past our short budget. Skip rather than
518
+ // block init; the loaders read-legacy-on-conflict so config is correct
519
+ // this run, and the next init retries the migration.
520
+ warnings.push(
521
+ `Config migration for ${opts.scope} skipped this run (another instance is migrating); will retry on next start.`,
522
+ );
523
+ return { migrated: false, conflict: false, targetPath: opts.targetPath, warnings };
524
+ }
525
+ try {
526
+ const sources = existingSources.map((source) => ({
527
+ ...source,
528
+ content: readFileSync(source.path, "utf-8"),
529
+ }));
530
+
531
+ if (existsSync(opts.targetPath)) {
532
+ const targetContent = readFileSync(opts.targetPath, "utf-8");
533
+ const differing = sources.filter(
534
+ (source) => !fileSemanticsMatch(source.content, targetContent),
535
+ );
536
+ if (differing.length > 0) {
537
+ const message = visibleConfigMigrationWarning(
538
+ opts.scope,
539
+ opts.targetPath,
540
+ differing.map((source) => source.path),
541
+ "the CortexKit target already exists with different settings",
542
+ );
543
+ warnings.push(message);
544
+ opts.logger?.warn?.(message);
545
+ return { migrated: false, conflict: true, targetPath: opts.targetPath, warnings };
546
+ }
547
+ info?.(
548
+ `Magic Context ${opts.scope} config already present at ${opts.targetPath}; legacy copies match`,
549
+ );
550
+ warnings.push(...markLegacySourcesMovedAside(sources, opts.targetPath, opts.logger));
551
+ return { migrated: false, conflict: false, targetPath: opts.targetPath, warnings };
552
+ }
553
+
554
+ const first = sources[0];
555
+ const differing = sources.filter(
556
+ (source) => !fileSemanticsMatch(source.content, first.content),
557
+ );
558
+ if (differing.length > 0) {
559
+ const message = visibleConfigMigrationWarning(
560
+ opts.scope,
561
+ opts.targetPath,
562
+ sources.map((source) => source.path),
563
+ "multiple legacy sources have different settings",
564
+ );
565
+ warnings.push(message);
566
+ opts.logger?.warn?.(message);
567
+ return { migrated: false, conflict: true, targetPath: opts.targetPath, warnings };
568
+ }
569
+
570
+ atomicCopyConfigFile(first.path, opts.targetPath);
571
+ info?.(
572
+ `Migrated Magic Context ${opts.scope} config from ${first.path} to ${opts.targetPath}`,
573
+ );
574
+ warnings.push(...markLegacySourcesMovedAside(sources, opts.targetPath, opts.logger));
575
+ return {
576
+ migrated: true,
577
+ conflict: false,
578
+ sourcePath: first.path,
579
+ targetPath: opts.targetPath,
580
+ warnings,
581
+ };
582
+ } catch (err) {
583
+ const message = visibleConfigMigrationWarning(
584
+ opts.scope,
585
+ opts.targetPath,
586
+ existingSources.map((source) => source.path),
587
+ `migration failed (${err instanceof Error ? err.message : String(err)})`,
588
+ );
589
+ warnings.push(message);
590
+ opts.logger?.warn?.(message);
591
+ return { migrated: false, conflict: true, targetPath: opts.targetPath, warnings };
592
+ } finally {
593
+ release();
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Run both the user-scope and project-scope config-location migrations for a
599
+ * project directory. Idempotent and cheap when nothing to migrate (a few
600
+ * existsSync calls). Call once at plugin init, before loading config. Returns
601
+ * any warnings (conflicts / partial failures) for the host to surface + log.
602
+ */
603
+ export function migrateMagicContextConfigLocations(
604
+ directory: string,
605
+ logger?: ConfigMigrationLogger,
606
+ ): string[] {
607
+ const warnings: string[] = [];
608
+ const legacy = resolveLegacyConfigSources(directory);
609
+ try {
610
+ warnings.push(
611
+ ...migrateConfigFile({
612
+ scope: "user",
613
+ targetPath: resolveCortexKitUserConfigPath(),
614
+ legacySources: legacy.user,
615
+ logger,
616
+ }).warnings,
617
+ );
618
+ warnings.push(
619
+ ...migrateConfigFile({
620
+ scope: "project",
621
+ targetPath: resolveCortexKitProjectConfigPath(directory),
622
+ legacySources: legacy.project,
623
+ logger,
624
+ }).warnings,
625
+ );
626
+ } catch (err) {
627
+ // Fail OPEN: a migration failure must never abort plugin init. Worst
628
+ // case the loader reads the CortexKit path (possibly empty) and a legacy
629
+ // file is left in place for the user to consolidate.
630
+ const msg = err instanceof Error ? err.message : String(err);
631
+ logger?.warn?.(`Magic Context config-location migration error (continuing): ${msg}`);
632
+ warnings.push(`Magic Context config-location migration error: ${msg}`);
633
+ }
634
+ return warnings;
635
+ }