@cortexkit/opencode-magic-context 0.16.2 → 0.17.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 (79) hide show
  1. package/README.md +8 -8
  2. package/dist/features/magic-context/message-index-async.d.ts +12 -0
  3. package/dist/features/magic-context/message-index-async.d.ts.map +1 -0
  4. package/dist/features/magic-context/message-index.d.ts +4 -0
  5. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  6. package/dist/features/magic-context/migrations.d.ts +7 -0
  7. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  8. package/dist/features/magic-context/search.d.ts +2 -2
  9. package/dist/features/magic-context/search.d.ts.map +1 -1
  10. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  11. package/dist/features/magic-context/storage-meta-persisted.d.ts +3 -6
  12. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-tags.d.ts +163 -1
  14. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  15. package/dist/features/magic-context/storage.d.ts +1 -1
  16. package/dist/features/magic-context/storage.d.ts.map +1 -1
  17. package/dist/features/magic-context/tagger.d.ts +52 -2
  18. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  19. package/dist/features/magic-context/tool-definition-tokens.d.ts +26 -3
  20. package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
  21. package/dist/features/magic-context/tool-owner-backfill.d.ts +90 -0
  22. package/dist/features/magic-context/tool-owner-backfill.d.ts.map +1 -0
  23. package/dist/features/magic-context/types.d.ts +17 -0
  24. package/dist/features/magic-context/types.d.ts.map +1 -1
  25. package/dist/hooks/auto-update-checker/cache.d.ts +12 -1
  26. package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  28. package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts +23 -0
  29. package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/event-payloads.d.ts +8 -0
  32. package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/hook-handlers.d.ts +6 -0
  35. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/inject-compartments.d.ts +16 -0
  38. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/live-session-state.d.ts +13 -0
  40. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/nudger.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/read-session-chunk.d.ts +24 -1
  43. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/read-session-db.d.ts +1 -0
  45. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/read-session-raw.d.ts +1 -0
  47. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/strip-content.d.ts +9 -6
  49. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/tag-messages.d.ts +1 -1
  51. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  52. package/dist/hooks/magic-context/tool-drop-target.d.ts +16 -1
  53. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +0 -11
  55. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  56. package/dist/hooks/magic-context/transform.d.ts +7 -0
  57. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2073 -768
  60. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  61. package/dist/plugin/rpc-handlers.d.ts +3 -0
  62. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  63. package/dist/shared/models-dev-cache.d.ts +3 -10
  64. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  65. package/dist/shared/native-binding.d.ts +87 -0
  66. package/dist/shared/native-binding.d.ts.map +1 -0
  67. package/dist/shared/sqlite.d.ts +0 -12
  68. package/dist/shared/sqlite.d.ts.map +1 -1
  69. package/dist/shared/tag-transcript.d.ts.map +1 -1
  70. package/package.json +2 -1
  71. package/src/shared/conflict-detector.ts +1 -1
  72. package/src/shared/models-dev-cache.test.ts +64 -57
  73. package/src/shared/models-dev-cache.ts +49 -68
  74. package/src/shared/native-binding.ts +311 -0
  75. package/src/shared/sqlite.ts +57 -14
  76. package/src/shared/tag-transcript.ts +137 -126
  77. package/src/tui/index.tsx +2 -2
  78. package/dist/hooks/magic-context/reasoning-capability.d.ts +0 -23
  79. package/dist/hooks/magic-context/reasoning-capability.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"create-session-hooks.d.ts","sourceRoot":"","sources":["../../../src/plugin/hooks/create-session-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAU7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,aAAa,CAAC;IACnB,YAAY,EAAE,wBAAwB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;CACtC;;;;;;qBAqDyvJ,CAAC;;;;;;;;;;;;qBAArnE,CAAC;mBAAyB,CAAC;iBAAuB,CAAC;iBAAuB,CAAC;0BAAc,CAAC;uBAAiB,CAAC;;;;;;0BAA0qpB,CAAC;;;;;;EAD55uB"}
1
+ {"version":3,"file":"create-session-hooks.d.ts","sourceRoot":"","sources":["../../../src/plugin/hooks/create-session-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAU7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,aAAa,CAAC;IACnB,YAAY,EAAE,wBAAwB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;CACtC;;;;;;qBAqDyvJ,CAAC;;;;;;;;;;;;qBAA/1D,CAAC;mBAAyB,CAAC;iBAAuB,CAAC;iBAAuB,CAAC;0BAAc,CAAC;uBAAiB,CAAC;;;;;;0BAAulpB,CAAC;;;;;;EAD/lvB"}
@@ -3,8 +3,11 @@
3
3
  * and returns typed responses for TUI consumption.
4
4
  */
5
5
  import type { MagicContextConfig } from "../config/schema/magic-context";
6
+ import { type ContextDatabase as Database } from "../features/magic-context/storage";
6
7
  import type { LiveSessionState } from "../hooks/magic-context/live-session-state";
7
8
  import type { MagicContextRpcServer } from "../shared/rpc-server";
9
+ import type { SidebarSnapshot } from "../shared/rpc-types";
10
+ export declare function buildSidebarSnapshot(db: Database, sessionId: string, directory: string, liveSessionState?: LiveSessionState, injectionBudgetTokens?: number): SidebarSnapshot;
8
11
  /**
9
12
  * Register all RPC handlers on the server.
10
13
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAMzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAQlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAkflE;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CA8HN"}
1
+ {"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,KAAK,eAAe,IAAI,QAAQ,EAAgB,MAAM,mCAAmC,CAAC;AAQnG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AASlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,qBAAqB,CAAC;AAmDzE,wBAAgB,oBAAoB,CAChC,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,qBAAqB,CAAC,EAAE,MAAM,GAC/B,eAAe,CAySjB;AAoMD;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CAoIN"}
@@ -14,10 +14,9 @@
14
14
  * Used during cold starts before the API cache warms up and in any
15
15
  * code path that cannot reach the SDK client.
16
16
  *
17
- * The public getters (`getModelsDevContextLimit()` and
18
- * `getModelsDevInterleavedField()`) are synchronous: they check the API cache
19
- * first, then the file cache. The plugin warms and refreshes the API cache
20
- * from `src/index.ts` at startup and on a timer.
17
+ * The public getter (`getModelsDevContextLimit()`) is synchronous: it checks
18
+ * the API cache first, then the file cache. The plugin warms and refreshes
19
+ * the API cache from `src/index.ts` at startup and on a timer.
21
20
  */
22
21
  interface OpencodeClientLike {
23
22
  config: {
@@ -52,12 +51,6 @@ export declare function refreshModelLimitsFromApi(client: OpencodeClientLike): P
52
51
  * Returns `undefined` if neither layer knows the model.
53
52
  */
54
53
  export declare function getModelsDevContextLimit(providerID: string, modelID: string): number | undefined;
55
- /**
56
- * Returns the provider-specific interleaved reasoning field when the model
57
- * requires one (for example `reasoning_content` for Moonshot/Kimi style
58
- * providers). Undefined means the cache has no such capability recorded.
59
- */
60
- export declare function getModelsDevInterleavedField(providerID: string, modelID: string): string | undefined;
61
54
  /** Clear in-memory caches (for testing). */
62
55
  export declare function clearModelsDevCache(): void;
63
56
  /** Inspection helpers (for logging / debugging). */
@@ -1 +1 @@
1
- {"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH,UAAU,kBAAkB;IACxB,MAAM,EAAE;QACJ,SAAS,EAAE,MAAM,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,SAAS,CAAC,EAAE,OAAO,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC;KAChE,CAAC;CACL;AAqND;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAchG;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAkBpB;AAED,4CAA4C;AAC5C,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED,oDAAoD;AACpD,wBAAgB,sBAAsB,IAAI;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB,CAOA"}
1
+ {"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,UAAU,kBAAkB;IACxB,MAAM,EAAE;QACJ,SAAS,EAAE,MAAM,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,SAAS,CAAC,EAAE,OAAO,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC;KAChE,CAAC;CACL;AA4MD;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmEzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAchG;AAED,4CAA4C;AAC5C,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C;AAED,oDAAoD;AACpD,wBAAgB,sBAAsB,IAAI;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB,CAOA"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Native-binding resolver for Electron-runtime mismatches.
3
+ *
4
+ * Why this module exists
5
+ * ----------------------
6
+ * `better-sqlite3` uses raw v8/nan bindings, so its compiled `.node` file is
7
+ * locked to a specific NODE_MODULE_VERSION (Node-API ABI revision). When
8
+ * OpenCode Desktop (Electron 41 → ABI 145) loads a plugin whose `node_modules`
9
+ * was populated by `bun install` / `npm install` under a different runtime,
10
+ * the fetched prebuild is for `node-vNNN` (e.g. ABI 137 for Node 22) and
11
+ * Electron refuses to load it with:
12
+ *
13
+ * The module '...better_sqlite3.node' was compiled against a different
14
+ * Node.js version using NODE_MODULE_VERSION 137. This version of Node.js
15
+ * requires NODE_MODULE_VERSION 145.
16
+ *
17
+ * `onnxruntime-node` (used by `@huggingface/transformers` for local
18
+ * embeddings) is N-API v3 and is ABI-stable across runtimes, so it does NOT
19
+ * have this problem. Only `better-sqlite3` is affected.
20
+ *
21
+ * What this does
22
+ * --------------
23
+ * On plugin load, before constructing any `Database`:
24
+ *
25
+ * 1. If we are NOT running under Electron (`process.versions.electron` is
26
+ * unset), return null. Bun uses `bun:sqlite` and never reaches this code;
27
+ * Pi/Node-CLI install matching Node-vNNN prebuilds via npm, which work
28
+ * natively against the on-disk binary.
29
+ *
30
+ * 2. If we ARE on Electron, locate the `.node` file path that
31
+ * `better-sqlite3`'s default `bindings()` lookup would try first. Probe
32
+ * its ABI by attempting a sandboxed `process.dlopen`. If it succeeds
33
+ * (Electron-compatible), return null — the default lookup will work.
34
+ *
35
+ * 3. Otherwise, look for a cached Electron prebuild at
36
+ * `<XDG_CACHE_HOME>/cortexkit/native-bindings/better-sqlite3/v<version>/electron-v<abi>-<platform>-<arch>/better_sqlite3.node`.
37
+ * Download it from the `WiseLibs/better-sqlite3` GitHub release if
38
+ * missing, extract with `nanotar` (pure-JS, ~45 KB, zero deps), validate
39
+ * the ABI, and return the absolute path.
40
+ *
41
+ * The caller (`sqlite.ts`) then passes this path through `better-sqlite3`'s
42
+ * documented `nativeBinding` constructor option:
43
+ *
44
+ * new Database(filename, { nativeBinding: <our cached path> })
45
+ *
46
+ * `better-sqlite3` calls `require()` directly on that path, bypassing the
47
+ * normal `bindings()` lookup chain. This is a first-class API the maintainer
48
+ * added for exactly this kind of cross-runtime extension scenario — see
49
+ * `node_modules/better-sqlite3/lib/database.js` `nativeBinding` handling.
50
+ *
51
+ * Why we don't replace the on-disk binary
52
+ * ---------------------------------------
53
+ * An earlier iteration copied the cached Electron binary over the in-tree
54
+ * `node_modules/.../better_sqlite3.node`. That worked but mutates a shared
55
+ * resource: in monorepo dev setups (or any case where multiple runtimes
56
+ * share one `node_modules`), a Pi process opening the plugin from the same
57
+ * workspace would then load the Electron-ABI binary and fail. Returning a
58
+ * separate cached path keeps the on-disk file untouched so each runtime
59
+ * sees the binary it needs.
60
+ *
61
+ * Failure modes
62
+ * -------------
63
+ * If GitHub is unreachable (corporate firewall, offline laptop, rate limit)
64
+ * AND no cached binary exists, this throws. The caller (sqlite.ts →
65
+ * openDatabase) surfaces a `storage unavailable` error and Magic Context
66
+ * disables itself for the run. The user can connect to the network and
67
+ * restart, or wait for a cached binary from a previous successful launch.
68
+ */
69
+ /**
70
+ * Resolve the absolute path to a `better-sqlite3` `.node` binary that the
71
+ * current runtime can load.
72
+ *
73
+ * - Returns `null` outside Electron (Bun uses `bun:sqlite`; Pi/Node CLI
74
+ * loads matching prebuilds from `node_modules` natively).
75
+ * - Returns `null` on Electron when the on-disk binary already matches
76
+ * the runtime's ABI (rare but possible — a future OpenCode build that
77
+ * post-install-rebuilds for Electron would hit this fast path).
78
+ * - Returns the cached/downloaded prebuild path on Electron when the
79
+ * on-disk binary's ABI doesn't match.
80
+ *
81
+ * The returned path is suitable as the `nativeBinding` option to
82
+ * `new Database(filename, { nativeBinding })`. better-sqlite3 calls `require()`
83
+ * directly on it, bypassing the default `bindings()` lookup chain — this is
84
+ * a documented public API in better-sqlite3, not an internal hack.
85
+ */
86
+ export declare function resolveBetterSqliteNativeBinding(): Promise<string | null>;
87
+ //# sourceMappingURL=native-binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../../src/shared/native-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AA2IH;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gCAAgC,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuF/E"}
@@ -25,18 +25,6 @@
25
25
  * is either rewritten to common-subset patterns or hidden behind the helpers
26
26
  * in `./sqlite-helpers.ts`.
27
27
  */
28
- /**
29
- * Database constructor compatible with both bun:sqlite and better-sqlite3.
30
- *
31
- * The TypeScript type intentionally references @types/better-sqlite3 because
32
- * its definitions are richer than @types/bun's bun:sqlite types and bun:sqlite
33
- * is a structural superset for the API surface we use. Calls written against
34
- * this type work correctly under both runtimes at runtime.
35
- *
36
- * @types/better-sqlite3 uses `export = Database` (CommonJS interop), which
37
- * surfaces in TypeScript as `import Database = require("better-sqlite3")`.
38
- * We capture the DatabaseConstructor type from the namespace re-export.
39
- */
40
28
  import type BetterSqlite3 from "better-sqlite3";
41
29
  export declare const Database: typeof BetterSqlite3;
42
30
  /** Instance type alias used by helpers and storage modules. */
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/shared/sqlite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAmCH;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,aAAa,MAAM,gBAAgB,CAAC;AAEhD,eAAO,MAAM,QAAQ,EAAE,OAAO,aAA4B,CAAC;AAE3D,+DAA+D;AAC/D,MAAM,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,MAAM,SAAS,GAAG,aAAa,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC"}
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/shared/sqlite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,OAAO,KAAK,aAAa,MAAM,gBAAgB,CAAC;AAsFhD,eAAO,MAAM,QAAQ,EAAE,OAAO,aAA4B,CAAC;AAE3D,+DAA+D;AAC/D,MAAM,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,MAAM,SAAS,GAAG,aAAa,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tag-transcript.d.ts","sourceRoot":"","sources":["../../src/shared/tag-transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAGzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAM/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACjC;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACnC;AAyDD,wBAAgB,aAAa,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,eAAe,EACnB,OAAO,GAAE,oBAAyB,GACnC,mBAAmB,CA+JrB"}
1
+ {"version":3,"file":"tag-transcript.d.ts","sourceRoot":"","sources":["../../src/shared/tag-transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAGzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAM/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACjC;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACnC;AAyDD,wBAAgB,aAAa,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,eAAe,EACnB,OAAO,GAAE,oBAAyB,GACnC,mBAAmB,CAoKrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.16.2",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -46,6 +46,7 @@
46
46
  "ai-tokenizer": "^1.0.6",
47
47
  "better-sqlite3": "^12.9.0",
48
48
  "comment-json": "^4.2.5",
49
+ "nanotar": "^0.3.0",
49
50
  "zod": "^4.1.8"
50
51
  },
51
52
  "devDependencies": {
@@ -365,7 +365,7 @@ export function formatConflictShort(result: ConflictResult): string {
365
365
  "",
366
366
  ...result.reasons.map((r) => `• ${r}`),
367
367
  "",
368
- "Fix: run `bunx --bun @cortexkit/opencode-magic-context@latest doctor`",
368
+ "Fix: run `npx @cortexkit/opencode-magic-context@latest doctor`",
369
369
  ];
370
370
  return lines.join("\n");
371
371
  }
@@ -6,7 +6,6 @@ import {
6
6
  clearModelsDevCache,
7
7
  getModelsDevCacheState,
8
8
  getModelsDevContextLimit,
9
- getModelsDevInterleavedField,
10
9
  refreshModelLimitsFromApi,
11
10
  } from "./models-dev-cache";
12
11
 
@@ -276,62 +275,6 @@ describe("models-dev-cache", () => {
276
275
  expect(state.apiCount).toBe(1);
277
276
  });
278
277
 
279
- test("reads interleaved reasoning field metadata from models.json", () => {
280
- const opencodeDir = join(tempDir, "opencode");
281
- mkdirSync(opencodeDir, { recursive: true });
282
- writeFileSync(
283
- join(opencodeDir, "models.json"),
284
- JSON.stringify({
285
- "opencode-go": {
286
- models: {
287
- "kimi-k2.6": {
288
- limit: { context: 262144 },
289
- interleaved: { field: "reasoning_content" },
290
- },
291
- "plain-model": {
292
- limit: { context: 262144 },
293
- },
294
- },
295
- },
296
- }),
297
- );
298
-
299
- expect(getModelsDevInterleavedField("opencode-go", "kimi-k2.6")).toBe("reasoning_content");
300
- expect(getModelsDevInterleavedField("opencode-go", "plain-model")).toBeUndefined();
301
- });
302
-
303
- test("API cache exposes interleaved reasoning metadata", async () => {
304
- const mockClient = {
305
- config: {
306
- providers: async () => ({
307
- data: {
308
- providers: [
309
- {
310
- id: "opencode-go",
311
- models: {
312
- "kimi-k2.6": {
313
- limit: { context: 262144 },
314
- capabilities: {
315
- interleaved: { field: "reasoning_content" },
316
- },
317
- },
318
- "plain-model": {
319
- limit: { context: 262144 },
320
- capabilities: { interleaved: false },
321
- },
322
- },
323
- },
324
- ],
325
- },
326
- }),
327
- },
328
- };
329
- await refreshModelLimitsFromApi(mockClient);
330
-
331
- expect(getModelsDevInterleavedField("opencode-go", "kimi-k2.6")).toBe("reasoning_content");
332
- expect(getModelsDevInterleavedField("opencode-go", "plain-model")).toBeUndefined();
333
- });
334
-
335
278
  test("refreshModelLimitsFromApi tolerates empty/malformed responses", async () => {
336
279
  // Undefined data.
337
280
  await refreshModelLimitsFromApi({
@@ -356,6 +299,70 @@ describe("models-dev-cache", () => {
356
299
  expect(getModelsDevCacheState().apiLoaded).toBe(false);
357
300
  });
358
301
 
302
+ test("suppresses repeated logs when API count oscillates between known sizes", async () => {
303
+ // Simulates github-copilot's /models endpoint returning different model sets
304
+ // between calls. We want first sighting of each new size to log, but once a
305
+ // size has been seen before, further flips between known sizes should be
306
+ // silent (with one "oscillating" notice).
307
+ const sizeA = {
308
+ data: {
309
+ providers: [
310
+ {
311
+ id: "p",
312
+ models: {
313
+ m1: { limit: { context: 100 } },
314
+ m2: { limit: { context: 100 } },
315
+ m3: { limit: { context: 100 } },
316
+ },
317
+ },
318
+ ],
319
+ },
320
+ };
321
+ const sizeB = {
322
+ data: {
323
+ providers: [
324
+ {
325
+ id: "p",
326
+ models: {
327
+ m1: { limit: { context: 100 } },
328
+ m2: { limit: { context: 100 } },
329
+ },
330
+ },
331
+ ],
332
+ },
333
+ };
334
+
335
+ const clientA = { config: { providers: async () => sizeA } };
336
+ const clientB = { config: { providers: async () => sizeB } };
337
+
338
+ // First sighting of size 3 → logs "loaded 3 entries".
339
+ await refreshModelLimitsFromApi(clientA);
340
+ expect(getModelsDevCacheState().apiCount).toBe(3);
341
+
342
+ // First sighting of size 2 → logs "loaded 2 entries (was 3)".
343
+ await refreshModelLimitsFromApi(clientB);
344
+ expect(getModelsDevCacheState().apiCount).toBe(2);
345
+
346
+ // Second sighting of size 3 → logs the "oscillating" notice once.
347
+ await refreshModelLimitsFromApi(clientA);
348
+ expect(getModelsDevCacheState().apiCount).toBe(3);
349
+
350
+ // Second sighting of size 2 → silent (no new log expected).
351
+ await refreshModelLimitsFromApi(clientB);
352
+ expect(getModelsDevCacheState().apiCount).toBe(2);
353
+
354
+ // Third sighting of size 3 → still silent.
355
+ await refreshModelLimitsFromApi(clientA);
356
+ expect(getModelsDevCacheState().apiCount).toBe(3);
357
+
358
+ // The cache itself still updates on every call (model contents are correct
359
+ // for whichever provider response just arrived). The suppression is purely
360
+ // a logging concern. Last call was clientA → all three models present.
361
+ expect(getModelsDevContextLimit("p", "m1")).toBe(100);
362
+ expect(getModelsDevContextLimit("p", "m2")).toBe(100);
363
+ expect(getModelsDevContextLimit("p", "m3")).toBe(100);
364
+ });
365
+
359
366
  test("falls back to file layer when API provider/model key is missing", async () => {
360
367
  const opencodeDir = join(tempDir, "opencode");
361
368
  mkdirSync(opencodeDir, { recursive: true });
@@ -14,10 +14,9 @@
14
14
  * Used during cold starts before the API cache warms up and in any
15
15
  * code path that cannot reach the SDK client.
16
16
  *
17
- * The public getters (`getModelsDevContextLimit()` and
18
- * `getModelsDevInterleavedField()`) are synchronous: they check the API cache
19
- * first, then the file cache. The plugin warms and refreshes the API cache
20
- * from `src/index.ts` at startup and on a timer.
17
+ * The public getter (`getModelsDevContextLimit()`) is synchronous: it checks
18
+ * the API cache first, then the file cache. The plugin warms and refreshes
19
+ * the API cache from `src/index.ts` at startup and on a timer.
21
20
  */
22
21
 
23
22
  import { createHash } from "node:crypto";
@@ -33,18 +32,30 @@ interface OpencodeClientLike {
33
32
  };
34
33
  }
35
34
 
36
- const RELOAD_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, matches OpenCode's TTL
35
+ // File-cache fallback only. The primary `models.json` API refresh is driven
36
+ // by `setInterval(refreshModelLimitsFromApi, ...)` in `index.ts` at a 1-hour
37
+ // cadence; this 5-minute interval governs the on-disk-cache fallback path
38
+ // when the API loader hasn't run yet (e.g. during plugin warmup).
39
+ const RELOAD_INTERVAL_MS = 5 * 60 * 1000;
37
40
 
38
41
  interface CachedModelMetadata {
39
42
  limit?: number;
40
- interleavedField?: string;
41
43
  }
42
44
 
43
- type InterleavedConfig = boolean | { field?: string } | undefined;
44
-
45
45
  /** Populated async from OpenCode SDK. Primary source of truth when available. */
46
46
  let apiCache: Map<string, CachedModelMetadata> | null = null;
47
47
  let apiLoadedAt = 0;
48
+ /**
49
+ * Recently-seen API cache sizes, used to detect oscillation between two
50
+ * stable values (typically caused by upstream provider plugins like
51
+ * github-copilot whose `/models` endpoint returns slightly different model
52
+ * sets between calls based on `model_picker_enabled` toggles). Once the
53
+ * same size has been observed before, we stop logging count changes —
54
+ * the count is a function of upstream behavior we can't control, and
55
+ * repeated logs only add noise.
56
+ */
57
+ const recentlySeenApiSizes = new Set<number>();
58
+ let oscillationLogged = false;
48
59
 
49
60
  /** Populated sync from disk as fallback. */
50
61
  let fileCache: Map<string, CachedModelMetadata> | null = null;
@@ -110,47 +121,28 @@ function resolveLimit(limit: { context?: number; input?: number } | undefined):
110
121
  return undefined;
111
122
  }
112
123
 
113
- function resolveInterleavedField(interleaved: InterleavedConfig): string | undefined {
114
- if (
115
- interleaved &&
116
- typeof interleaved === "object" &&
117
- typeof interleaved.field === "string" &&
118
- interleaved.field.length > 0
119
- ) {
120
- return interleaved.field;
121
- }
122
- return undefined;
123
- }
124
-
125
124
  function setCachedModelMetadata(
126
125
  cache: Map<string, CachedModelMetadata>,
127
126
  key: string,
128
127
  model:
129
128
  | {
130
129
  limit?: { context?: number; input?: number };
131
- capabilities?: { interleaved?: InterleavedConfig };
132
- interleaved?: InterleavedConfig;
133
130
  experimental?: { modes?: Record<string, unknown> };
134
131
  }
135
132
  | undefined,
136
133
  ): void {
137
134
  const limit = resolveLimit(model?.limit);
138
- const interleavedField =
139
- resolveInterleavedField(model?.capabilities?.interleaved) ??
140
- resolveInterleavedField(model?.interleaved);
141
135
 
142
- if (limit === undefined && interleavedField === undefined) {
136
+ if (limit === undefined) {
143
137
  return;
144
138
  }
145
139
 
146
- const value: CachedModelMetadata = {};
147
- if (limit !== undefined) value.limit = limit;
148
- if (interleavedField !== undefined) value.interleavedField = interleavedField;
140
+ const value: CachedModelMetadata = { limit };
149
141
  cache.set(key, value);
150
142
 
151
143
  // OpenCode creates derived model IDs from experimental.modes
152
144
  // e.g. gpt-5.4 + modes.fast → gpt-5.4-fast. These inherit the same
153
- // context limit and interleaved-reasoning contract as the parent model.
145
+ // context limit as the parent model.
154
146
  const modes = model?.experimental?.modes;
155
147
  if (modes && typeof modes === "object") {
156
148
  for (const mode of Object.keys(modes)) {
@@ -176,8 +168,6 @@ function loadModelsDevMetadataFromFile(): Map<string, CachedModelMetadata> {
176
168
  string,
177
169
  {
178
170
  limit?: { context?: number; input?: number };
179
- capabilities?: { interleaved?: InterleavedConfig };
180
- interleaved?: InterleavedConfig;
181
171
  experimental?: { modes?: Record<string, unknown> };
182
172
  }
183
173
  >;
@@ -273,8 +263,6 @@ export async function refreshModelLimitsFromApi(client: OpencodeClientLike): Pro
273
263
  string,
274
264
  {
275
265
  limit?: { context?: number; input?: number };
276
- capabilities?: { interleaved?: InterleavedConfig };
277
- interleaved?: InterleavedConfig;
278
266
  experimental?: { modes?: Record<string, unknown> };
279
267
  }
280
268
  >;
@@ -288,15 +276,34 @@ export async function refreshModelLimitsFromApi(client: OpencodeClientLike): Pro
288
276
  const previousSize = apiCache?.size ?? null;
289
277
  apiCache = map;
290
278
  apiLoadedAt = Date.now();
291
- // Log only on first successful load or when the model count changes,
292
- // so the 5-minute periodic refresh doesn't spam the log.
293
- if (previousSize === null || previousSize !== map.size) {
279
+
280
+ // Log policy:
281
+ // - Always log the first successful load.
282
+ // - Log a count change once per new size we haven't seen before.
283
+ // - When the count returns to a previously-seen size, log an
284
+ // "oscillation" message exactly once explaining the cause, then
285
+ // stay silent on further flips between known sizes.
286
+ if (previousSize === null) {
287
+ recentlySeenApiSizes.add(map.size);
294
288
  sessionLog(
295
289
  "global",
296
- `models-dev-cache: API layer loaded ${map.size} model metadata entries${
297
- previousSize !== null ? ` (was ${previousSize})` : ""
298
- }`,
290
+ `models-dev-cache: API layer loaded ${map.size} model metadata entries`,
299
291
  );
292
+ } else if (previousSize !== map.size) {
293
+ const sizeAlreadySeen = recentlySeenApiSizes.has(map.size);
294
+ recentlySeenApiSizes.add(map.size);
295
+ if (!sizeAlreadySeen) {
296
+ sessionLog(
297
+ "global",
298
+ `models-dev-cache: API layer loaded ${map.size} model metadata entries (was ${previousSize})`,
299
+ );
300
+ } else if (!oscillationLogged) {
301
+ oscillationLogged = true;
302
+ sessionLog(
303
+ "global",
304
+ `models-dev-cache: API count oscillating between ${[...recentlySeenApiSizes].sort((a, b) => a - b).join(" ↔ ")} — likely upstream provider plugin returning slightly different model sets between calls (e.g. github-copilot's /models endpoint toggling model_picker_enabled). Suppressing further size-change logs.`,
305
+ );
306
+ }
300
307
  }
301
308
  } catch (error) {
302
309
  sessionLog(
@@ -334,38 +341,12 @@ export function getModelsDevContextLimit(providerID: string, modelID: string): n
334
341
  return fileCache.get(key)?.limit;
335
342
  }
336
343
 
337
- /**
338
- * Returns the provider-specific interleaved reasoning field when the model
339
- * requires one (for example `reasoning_content` for Moonshot/Kimi style
340
- * providers). Undefined means the cache has no such capability recorded.
341
- */
342
- export function getModelsDevInterleavedField(
343
- providerID: string,
344
- modelID: string,
345
- ): string | undefined {
346
- const key = `${providerID}/${modelID}`;
347
-
348
- if (apiCache) {
349
- const fromApi = apiCache.get(key)?.interleavedField;
350
- if (typeof fromApi === "string" && fromApi.length > 0) {
351
- return fromApi;
352
- }
353
- }
354
-
355
- const now = Date.now();
356
- if (!fileCache || now - fileLastAttempt > RELOAD_INTERVAL_MS) {
357
- fileLastAttempt = now;
358
- fileCache = loadModelsDevMetadataFromFile();
359
- }
360
-
361
- const fromFile = fileCache.get(key)?.interleavedField;
362
- return typeof fromFile === "string" && fromFile.length > 0 ? fromFile : undefined;
363
- }
364
-
365
344
  /** Clear in-memory caches (for testing). */
366
345
  export function clearModelsDevCache(): void {
367
346
  apiCache = null;
368
347
  apiLoadedAt = 0;
348
+ recentlySeenApiSizes.clear();
349
+ oscillationLogged = false;
369
350
  fileCache = null;
370
351
  fileLastAttempt = 0;
371
352
  }