@cortexkit/opencode-magic-context 0.27.0 → 0.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,6 +28,7 @@
28
28
  <a href="#-recall">Recall</a> ·
29
29
  <a href="https://docs.cortexkit.io/magic-context">Docs</a> ·
30
30
  <a href="./CONFIGURATION.md">Configuration</a> ·
31
+ <a href="https://github.com/cortexkit/magic-context/releases?q=dashboard&expanded=true">Dashboard</a> ·
31
32
  <a href="https://discord.gg/DSa65w8wuf">💬 Discord</a>
32
33
  </p>
33
34
 
@@ -58,6 +58,14 @@ export declare function resolveCortexKitProjectConfigPath(directory: string): st
58
58
  * `.jsonc` and a `.json` candidate; whichever exists migrates, target is always
59
59
  * `.jsonc`. The bare-root project source (`<root>/magic-context.*`) is unique to
60
60
  * Magic Context (AFT never had it) — omitting it would orphan repo-root configs.
61
+ *
62
+ * Project sources are filtered against the user-scope path set: when a session's
63
+ * project directory IS the user config home (e.g. opencode opened in
64
+ * `~/.config/cortexkit`), the bare-root project source `<root>/magic-context.jsonc`
65
+ * resolves to the USER config path. Without this guard the project migration
66
+ * would "migrate" the user's own config into `<root>/.cortexkit/` and rename the
67
+ * original aside, leaving the user on schema defaults (the config-eats-itself
68
+ * bug). A project migration must never touch a user-scope file.
61
69
  */
62
70
  export declare function resolveLegacyConfigSources(directory: string): {
63
71
  user: LegacyConfigSource[];
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-config-location.d.ts","sourceRoot":"","sources":["../../src/config/migrate-config-location.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,kBAAkB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IAClC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IACvC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC7C,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACtC,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAoBD,iFAAiF;AACjF,wBAAgB,2BAA2B,IAAI,MAAM,CAEpD;AAED,+EAA+E;AAC/E,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,2DAA2D;AAC3D,wBAAgB,8BAA8B,IAAI,MAAM,CAEvD;AAED,2DAA2D;AAC3D,wBAAgB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3E;AASD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG;IAC3D,IAAI,EAAE,kBAAkB,EAAE,CAAC;IAC3B,OAAO,EAAE,kBAAkB,EAAE,CAAC;CACjC,CAqBA;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,IAAI,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,oCAAoC,CAChD,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,GACvB;IAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC;IAAC,OAAO,EAAE,kBAAkB,EAAE,CAAA;CAAE,CA0B/D;AAmSD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,0BAA0B,GAAG,yBAAyB,CA0F7F;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAC9C,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,qBAAqB,GAC/B,MAAM,EAAE,CA6BV"}
1
+ {"version":3,"file":"migrate-config-location.d.ts","sourceRoot":"","sources":["../../src/config/migrate-config-location.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,kBAAkB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IAClC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IACvC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC7C,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACtC,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAoBD,iFAAiF;AACjF,wBAAgB,2BAA2B,IAAI,MAAM,CAEpD;AAED,+EAA+E;AAC/E,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,2DAA2D;AAC3D,wBAAgB,8BAA8B,IAAI,MAAM,CAEvD;AAED,2DAA2D;AAC3D,wBAAgB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3E;AA4BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG;IAC3D,IAAI,EAAE,kBAAkB,EAAE,CAAC;IAC3B,OAAO,EAAE,kBAAkB,EAAE,CAAC;CACjC,CAsBA;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,IAAI,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,oCAAoC,CAChD,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,GACvB;IAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC;IAAC,OAAO,EAAE,kBAAkB,EAAE,CAAA;CAAE,CA0B/D;AAmSD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,0BAA0B,GAAG,yBAAyB,CA0F7F;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAC9C,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,qBAAqB,GAC/B,MAAM,EAAE,CA6BV"}
@@ -1 +1 @@
1
- {"version":3,"file":"event-handler.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/event-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAyBvF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAKlE,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,oCAAoC,CAAC;AAqBpF,OAAO,EAAE,KAAK,kBAAkB,EAAsB,MAAM,6BAA6B,CAAC;AAM1F,KAAK,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEtD,UAAU,iBAAiB;IACvB,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAMD,MAAM,WAAW,gBAAgB;IAC7B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAChD,iBAAiB,EAAE,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;IAC9D,yBAAyB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,MAAM,EAAE;QACJ,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,4BAA4B,CAAC,EAAE,MAAM,GAAG;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAC;QACxF,wBAAwB,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;SAAE,CAAC;QACxF,SAAS,EAAE,cAAc,CAAC;QAC1B,sBAAsB,CAAC,EAAE;YAAE,OAAO,EAAE,OAAO,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE,CAAC;KACvE,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IAIf,EAAE,EAAE,OAAO,qBAAqB,EAAE,QAAQ,CAAC;IAC3C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,sBAAsB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,oBAAoB,EAAE,aAAa,CAAC,CAAC;IACjF,qBAAqB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,kBAAkB,CAAC;IAClE;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvC;AAqID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,IACvC,OAAO;IAAE,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,KAAG,OAAO,CAAC,IAAI,CAAC,CAyfzF"}
1
+ {"version":3,"file":"event-handler.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/event-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAyBvF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAKlE,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,oCAAoC,CAAC;AAwBpF,OAAO,EAAE,KAAK,kBAAkB,EAAsB,MAAM,6BAA6B,CAAC;AAM1F,KAAK,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEtD,UAAU,iBAAiB;IACvB,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAMD,MAAM,WAAW,gBAAgB;IAC7B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAChD,iBAAiB,EAAE,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;IAC9D,yBAAyB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,MAAM,EAAE;QACJ,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,4BAA4B,CAAC,EAAE,MAAM,GAAG;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAC;QACxF,wBAAwB,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;SAAE,CAAC;QACxF,SAAS,EAAE,cAAc,CAAC;QAC1B,sBAAsB,CAAC,EAAE;YAAE,OAAO,EAAE,OAAO,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE,CAAC;KACvE,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IAIf,EAAE,EAAE,OAAO,qBAAqB,EAAE,QAAQ,CAAC;IAC3C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,sBAAsB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,oBAAoB,EAAE,aAAa,CAAC,CAAC;IACjF,qBAAqB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,kBAAkB,CAAC;IAClE;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvC;AAqID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,IACvC,OAAO;IAAE,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,KAAG,OAAO,CAAC,IAAI,CAAC,CAmgBzF"}
package/dist/index.js CHANGED
@@ -176602,6 +176602,14 @@ async function refreshModelLimitsFromApi(client, options) {
176602
176602
  }
176603
176603
  }
176604
176604
  }
176605
+ async function refreshModelLimitsAfterAuthOnce(client) {
176606
+ if (authRewarmDone)
176607
+ return;
176608
+ authRewarmDone = true;
176609
+ const ok = await refreshModelLimitsOnce(client);
176610
+ if (!ok)
176611
+ authRewarmDone = false;
176612
+ }
176605
176613
  async function refreshModelLimitsOnce(client) {
176606
176614
  try {
176607
176615
  const result = await client.config.providers();
@@ -176655,7 +176663,7 @@ function lookupLimitWithTagFallback(cache, providerID, modelID) {
176655
176663
  }
176656
176664
  return;
176657
176665
  }
176658
- var MIN_SANE_LIMIT = 20000, MAX_SANE_LIMIT = 3000000, apiCache = null, apiLoadedAt = 0, persistSeedLoaded = false;
176666
+ var MIN_SANE_LIMIT = 20000, MAX_SANE_LIMIT = 3000000, apiCache = null, apiLoadedAt = 0, persistSeedLoaded = false, authRewarmDone = false;
176659
176667
  var init_models_dev_cache = __esm(() => {
176660
176668
  init_data_path();
176661
176669
  init_logger();
@@ -186475,7 +186483,7 @@ function resolveTuiConfigPath() {
186475
186483
  return jsoncPath;
186476
186484
  if (existsSync19(jsonPath))
186477
186485
  return jsonPath;
186478
- return jsonPath;
186486
+ return jsoncPath;
186479
186487
  }
186480
186488
  function ensureTuiPluginEntry() {
186481
186489
  try {
@@ -186811,7 +186819,18 @@ function legacySourcesForBase(basePath, label) {
186811
186819
  { path: `${basePath}.json`, label: `${label} magic-context.json` }
186812
186820
  ];
186813
186821
  }
186822
+ function userScopeConfigPaths() {
186823
+ return new Set([
186824
+ `${cortexKitUserConfigBasePath()}.jsonc`,
186825
+ `${cortexKitUserConfigBasePath()}.json`,
186826
+ join2(configHome(), "opencode", `${CONFIG_FILE_BASENAME}.jsonc`),
186827
+ join2(configHome(), "opencode", `${CONFIG_FILE_BASENAME}.json`),
186828
+ join2(homeDir(), ".pi", "agent", `${CONFIG_FILE_BASENAME}.jsonc`),
186829
+ join2(homeDir(), ".pi", "agent", `${CONFIG_FILE_BASENAME}.json`)
186830
+ ]);
186831
+ }
186814
186832
  function resolveLegacyConfigSources(directory) {
186833
+ const userPaths = userScopeConfigPaths();
186815
186834
  return {
186816
186835
  user: [
186817
186836
  ...legacySourcesForBase(join2(configHome(), "opencode", CONFIG_FILE_BASENAME), "OpenCode user"),
@@ -186821,7 +186840,7 @@ function resolveLegacyConfigSources(directory) {
186821
186840
  ...legacySourcesForBase(join2(directory, CONFIG_FILE_BASENAME), "project root"),
186822
186841
  ...legacySourcesForBase(join2(directory, ".opencode", CONFIG_FILE_BASENAME), "OpenCode project"),
186823
186842
  ...legacySourcesForBase(join2(directory, ".pi", CONFIG_FILE_BASENAME), "Pi project")
186824
- ]
186843
+ ].filter((source) => !userPaths.has(source.path))
186825
186844
  };
186826
186845
  }
186827
186846
  function resolveLegacyConfigSourcesForHarness(directory, harness) {
@@ -190163,6 +190182,12 @@ function redactionTypeForKey(key) {
190163
190182
  const suffix = normalized.split(".").filter(Boolean).at(-1) ?? normalized;
190164
190183
  return suffix || "secret";
190165
190184
  }
190185
+ function isNonSecretScalarValue(value) {
190186
+ const v = value.trim();
190187
+ if (v === "true" || v === "false" || v === "null" || v === "undefined")
190188
+ return true;
190189
+ return /^[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(v);
190190
+ }
190166
190191
  var SECRET_QUALIFIERS = new Set([
190167
190192
  "api",
190168
190193
  "access",
@@ -190242,11 +190267,11 @@ var SECRET_TEXT_PATTERNS = [
190242
190267
  },
190243
190268
  {
190244
190269
  pattern: /(["'])([^"']*(?:key|token|secret|password|auth|bearer|credential)[^"']*)\1(\s*:\s*)(["'])([^"']*)\4/gi,
190245
- replacement: (_full, quote, key, separator, valueQuote) => `${quote}${key}${quote}${separator}${valueQuote}<REDACTED:${redactionTypeForKey(key)}>${valueQuote}`
190270
+ replacement: (full, quote, key, separator, valueQuote, value) => isNonSecretScalarValue(value) ? full : `${quote}${key}${quote}${separator}${valueQuote}<REDACTED:${redactionTypeForKey(key)}>${valueQuote}`
190246
190271
  },
190247
190272
  {
190248
190273
  pattern: /\b([A-Za-z0-9_.-]*(?:key|token|secret|password|auth|bearer|credential)[A-Za-z0-9_.-]*)\s*=\s*([^\s'"`]+)/gi,
190249
- replacement: (_full, key) => `${key}=<REDACTED:${redactionTypeForKey(key)}>`
190274
+ replacement: (full, key, value) => isNonSecretScalarValue(value) ? full : `${key}=<REDACTED:${redactionTypeForKey(key)}>`
190250
190275
  }
190251
190276
  ];
190252
190277
  function redactSecretText(value) {
@@ -202311,6 +202336,9 @@ function createEventHandler2(deps) {
202311
202336
  }
202312
202337
  if (hasUsageTokens) {
202313
202338
  const totalInputTokens = (info.tokens?.input ?? 0) + (info.tokens?.cache?.read ?? 0) + (info.tokens?.cache?.write ?? 0);
202339
+ if (deps.client) {
202340
+ await refreshModelLimitsAfterAuthOnce(deps.client);
202341
+ }
202314
202342
  let contextLimit = resolveContextLimit(info.providerID, info.modelID, {
202315
202343
  db: deps.db,
202316
202344
  sessionID: info.sessionID
@@ -59,6 +59,29 @@ export declare function refreshModelLimitsFromApi(client: OpencodeClientLike, op
59
59
  retries?: number;
60
60
  retryDelayMs?: number;
61
61
  }): Promise<void>;
62
+ /**
63
+ * Re-warm the limit cache ONCE per process, after auth is provably live.
64
+ *
65
+ * The startup warm (index.ts) can run before the user's provider auth is
66
+ * loaded. When it does, an auth-conditional limit patch hasn't applied yet, so
67
+ * `config.providers()` returns the RAW catalog limit (e.g. OpenAI gpt-5.5 OAuth
68
+ * is downshifted to a 272k input cap by OpenCode's Codex auth plugin only when
69
+ * `ctx.auth.type === "oauth"`; before auth loads it reports the raw 922k). That
70
+ * too-high value gets cached AND persisted as last-known-good, survives
71
+ * restarts, and the existing recovery only re-resolves a too-LOW limit
72
+ * (overflow / `percentage > 100`), so a too-HIGH one never self-corrects: the
73
+ * sidebar shows huge headroom while the backend rejects at the real cap (#179).
74
+ *
75
+ * The first `message.updated` carrying usage tokens proves a request succeeded,
76
+ * so auth + providers are fully resolved. Re-warming there overwrites any stale
77
+ * pre-auth limit with the live auth-adjusted one. Idempotent and cheap: a single
78
+ * `config.providers()` round-trip, then a no-op for the rest of the process. The
79
+ * latch is set before the await so concurrent `message.updated` events don't
80
+ * stack duplicate warms; a failed warm resets it so a later message retries.
81
+ */
82
+ export declare function refreshModelLimitsAfterAuthOnce(client: OpencodeClientLike): Promise<void>;
83
+ /** Test-only: reset the after-auth re-warm latch between cases. */
84
+ export declare function resetAuthRewarmLatchForTest(): void;
62
85
  /**
63
86
  * Resolve a model's prompt limit from OpenCode's SDK (`config.providers()`),
64
87
  * the single source of truth: it already merges models.dev + compiled-in
@@ -1 +1 @@
1
- {"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAQH,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;AASD,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,eAAO,MAAM,cAAc,UAAY,CAAC;AAExC;;kFAEkF;AAClF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,IAAI,MAAM,CAEtE;AA+HD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,yBAAyB,CAC3C,MAAM,EAAE,kBAAkB,EAC1B,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GACtD,OAAO,CAAC,IAAI,CAAC,CAUf;AA+DD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI1F;AA6BD,gFAAgF;AAChF,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,oDAAoD;AACpD,wBAAgB,sBAAsB,IAAI;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB,CAMA"}
1
+ {"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAQH,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;AASD,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,eAAO,MAAM,cAAc,UAAY,CAAC;AAExC;;kFAEkF;AAClF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,IAAI,MAAM,CAEtE;AA+HD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,yBAAyB,CAC3C,MAAM,EAAE,kBAAkB,EAC1B,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GACtD,OAAO,CAAC,IAAI,CAAC,CAUf;AAKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,+BAA+B,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/F;AAED,mEAAmE;AACnE,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AA+DD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI1F;AA6BD,gFAAgF;AAChF,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,oDAAoD;AACpD,wBAAgB,sBAAsB,IAAI;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB,CAMA"}
@@ -1 +1 @@
1
- {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../../src/shared/redaction.ts"],"names":[],"mappings":"AAyDA,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAkChD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAcxD;AAkED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAatD;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D;AAsBD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOlE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,MAAM,EAAO,GAAG,OAAO,CAkBnF"}
1
+ {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../../src/shared/redaction.ts"],"names":[],"mappings":"AAyEA,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAkChD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAcxD;AA0ED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAatD;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D;AAsBD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOlE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,MAAM,EAAO,GAAG,OAAO,CAkBnF"}
@@ -1 +1 @@
1
- {"version":3,"file":"tui-config.d.ts","sourceRoot":"","sources":["../../src/shared/tui-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkEH;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAqD9C"}
1
+ {"version":3,"file":"tui-config.d.ts","sourceRoot":"","sources":["../../src/shared/tui-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyEH;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAqD9C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.27.0",
3
+ "version": "0.27.2",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -44,9 +44,12 @@
44
44
  "@jitl/quickjs-singlefile-cjs-release-asyncify": "0.32.0",
45
45
  "@opencode-ai/plugin": "^1.15.13",
46
46
  "@opencode-ai/sdk": "^1.15.13",
47
+ "@opentui/core": "^0.4.2",
48
+ "@opentui/solid": "^0.4.2",
47
49
  "ai-tokenizer": "^1.0.6",
48
50
  "comment-json": "^4.2.5",
49
51
  "quickjs-emscripten": "^0.32.0",
52
+ "solid-js": "1.9.12",
50
53
  "zod": "^4.1.8"
51
54
  },
52
55
  "devDependencies": {
@@ -6,7 +6,9 @@ import {
6
6
  clearModelsDevCache,
7
7
  getModelsDevCacheState,
8
8
  getSdkContextLimit,
9
+ refreshModelLimitsAfterAuthOnce,
9
10
  refreshModelLimitsFromApi,
11
+ resetAuthRewarmLatchForTest,
10
12
  } from "./models-dev-cache";
11
13
 
12
14
  /**
@@ -198,6 +200,86 @@ describe("models-dev-cache (SDK-only)", () => {
198
200
  });
199
201
  });
200
202
 
203
+ describe("after-auth re-warm (once per process)", () => {
204
+ // The startup warm can run before provider auth is loaded, caching a raw
205
+ // pre-downshift limit (gpt-5.5 922k) that then survives restarts and is
206
+ // never corrected by the too-low-only recovery. The first usage event
207
+ // proves auth is live; one re-warm there overwrites the stale value.
208
+ function makeCountingClient(input: number) {
209
+ let calls = 0;
210
+ return {
211
+ client: {
212
+ config: {
213
+ providers: async () => {
214
+ calls++;
215
+ return {
216
+ data: {
217
+ providers: [
218
+ {
219
+ id: "openai",
220
+ models: { "gpt-5.5": { limit: { input } } },
221
+ },
222
+ ],
223
+ },
224
+ };
225
+ },
226
+ },
227
+ },
228
+ calls: () => calls,
229
+ };
230
+ }
231
+
232
+ test("re-warm overwrites a stale pre-auth limit and runs only once per process", async () => {
233
+ resetAuthRewarmLatchForTest();
234
+ // Pre-auth startup warm cached the raw 922k.
235
+ await refreshModelLimitsFromApi(
236
+ makeClient([{ id: "openai", models: { "gpt-5.5": { limit: { input: 922000 } } } }]),
237
+ );
238
+ expect(getSdkContextLimit("openai", "gpt-5.5")).toBe(922000);
239
+
240
+ // First usage: auth is live, providers() now reports the 272k cap.
241
+ const { client, calls } = makeCountingClient(272000);
242
+ await refreshModelLimitsAfterAuthOnce(client);
243
+ expect(getSdkContextLimit("openai", "gpt-5.5")).toBe(272000);
244
+ expect(calls()).toBe(1);
245
+
246
+ // Subsequent usage events are a no-op (latch held).
247
+ await refreshModelLimitsAfterAuthOnce(client);
248
+ await refreshModelLimitsAfterAuthOnce(client);
249
+ expect(calls()).toBe(1);
250
+ });
251
+
252
+ test("a failed re-warm resets the latch so a later usage event retries", async () => {
253
+ resetAuthRewarmLatchForTest();
254
+ let calls = 0;
255
+ const flaky = {
256
+ config: {
257
+ providers: async () => {
258
+ calls++;
259
+ // First attempt: empty payload (auth still settling) → no warm.
260
+ if (calls === 1) return { data: { providers: [] } };
261
+ return {
262
+ data: {
263
+ providers: [
264
+ {
265
+ id: "openai",
266
+ models: { "gpt-5.5": { limit: { input: 272000 } } },
267
+ },
268
+ ],
269
+ },
270
+ };
271
+ },
272
+ },
273
+ };
274
+ await refreshModelLimitsAfterAuthOnce(flaky);
275
+ expect(getSdkContextLimit("openai", "gpt-5.5")).toBeUndefined();
276
+ // Latch was reset on failure, so the next event retries and succeeds.
277
+ await refreshModelLimitsAfterAuthOnce(flaky);
278
+ expect(getSdkContextLimit("openai", "gpt-5.5")).toBe(272000);
279
+ expect(calls).toBe(2);
280
+ });
281
+ });
282
+
201
283
  describe("startup retry", () => {
202
284
  test("retries when the provider payload is empty, then succeeds", async () => {
203
285
  let calls = 0;
@@ -210,6 +210,41 @@ export async function refreshModelLimitsFromApi(
210
210
  }
211
211
  }
212
212
 
213
+ // Once-per-process latch for the after-auth re-warm below.
214
+ let authRewarmDone = false;
215
+
216
+ /**
217
+ * Re-warm the limit cache ONCE per process, after auth is provably live.
218
+ *
219
+ * The startup warm (index.ts) can run before the user's provider auth is
220
+ * loaded. When it does, an auth-conditional limit patch hasn't applied yet, so
221
+ * `config.providers()` returns the RAW catalog limit (e.g. OpenAI gpt-5.5 OAuth
222
+ * is downshifted to a 272k input cap by OpenCode's Codex auth plugin only when
223
+ * `ctx.auth.type === "oauth"`; before auth loads it reports the raw 922k). That
224
+ * too-high value gets cached AND persisted as last-known-good, survives
225
+ * restarts, and the existing recovery only re-resolves a too-LOW limit
226
+ * (overflow / `percentage > 100`), so a too-HIGH one never self-corrects: the
227
+ * sidebar shows huge headroom while the backend rejects at the real cap (#179).
228
+ *
229
+ * The first `message.updated` carrying usage tokens proves a request succeeded,
230
+ * so auth + providers are fully resolved. Re-warming there overwrites any stale
231
+ * pre-auth limit with the live auth-adjusted one. Idempotent and cheap: a single
232
+ * `config.providers()` round-trip, then a no-op for the rest of the process. The
233
+ * latch is set before the await so concurrent `message.updated` events don't
234
+ * stack duplicate warms; a failed warm resets it so a later message retries.
235
+ */
236
+ export async function refreshModelLimitsAfterAuthOnce(client: OpencodeClientLike): Promise<void> {
237
+ if (authRewarmDone) return;
238
+ authRewarmDone = true;
239
+ const ok = await refreshModelLimitsOnce(client);
240
+ if (!ok) authRewarmDone = false;
241
+ }
242
+
243
+ /** Test-only: reset the after-auth re-warm latch between cases. */
244
+ export function resetAuthRewarmLatchForTest(): void {
245
+ authRewarmDone = false;
246
+ }
247
+
213
248
  /** Single SDK fetch + cache rebuild. Returns true when providers were loaded. */
214
249
  async function refreshModelLimitsOnce(client: OpencodeClientLike): Promise<boolean> {
215
250
  try {
@@ -2,7 +2,43 @@
2
2
 
3
3
  import { describe, expect, test } from "bun:test";
4
4
 
5
- import { hasShareabilitySensitiveText } from "./redaction";
5
+ import { hasShareabilitySensitiveText, redactSecretText } from "./redaction";
6
+
7
+ describe("redactSecretText — token counts and scalar diagnostics stay visible", () => {
8
+ test("keeps numeric/boolean values whose key merely contains a secret word", () => {
9
+ // These log shapes are counts/flags, not secrets, so they must stay readable.
10
+ expect(redactSecretText("tokens.input=45000 cache.read=0 cache.write=0")).toBe(
11
+ "tokens.input=45000 cache.read=0 cache.write=0",
12
+ );
13
+ expect(redactSecretText("hasUsageTokens=true")).toBe("hasUsageTokens=true");
14
+ expect(redactSecretText("totalInputTokens=132000")).toBe("totalInputTokens=132000");
15
+ expect(redactSecretText("max_tokens=4096")).toBe("max_tokens=4096");
16
+ });
17
+
18
+ test("keeps quoted numeric values matched only on the key word", () => {
19
+ expect(redactSecretText('"max_tokens": "4096"')).toBe('"max_tokens": "4096"');
20
+ });
21
+
22
+ test("still redacts real secret string values", () => {
23
+ // High-entropy / non-scalar values must always be redacted; only bare
24
+ // numeric/boolean scalars are exempt from the key-based match.
25
+ expect(redactSecretText("api_key=sk-abc123XYZsecretvalue")).toContain("<REDACTED:");
26
+ expect(redactSecretText("api_key=sk-abc123XYZsecretvalue")).not.toContain(
27
+ "sk-abc123XYZsecretvalue",
28
+ );
29
+ expect(redactSecretText('"auth_token": "tok_live_9f8e7d6c5b"')).toContain("<REDACTED:");
30
+ });
31
+
32
+ test("value-shaped secret patterns still fire independent of key name", () => {
33
+ // A bearer/JWT value is caught by its own pattern even if its key is bland.
34
+ expect(redactSecretText("Authorization: Bearer abc123def456ghi789")).toContain(
35
+ "<REDACTED:bearer>",
36
+ );
37
+ expect(redactSecretText("blob=eyJhbGciOi.eyJzdWIiOiIx.SflKxwRJSMeKKF2QT4")).toContain(
38
+ "<JWT_REDACTED>",
39
+ );
40
+ });
41
+ });
6
42
 
7
43
  describe("hasShareabilitySensitiveText", () => {
8
44
  test("safe project facts are shareable", () => {
@@ -33,6 +33,22 @@ function redactionTypeForKey(key: string): string {
33
33
  return suffix || "secret";
34
34
  }
35
35
 
36
+ // A bare number / boolean / null is never a secret — an API key, bearer token,
37
+ // password, or credential is always a high-entropy string. So when a key-based
38
+ // pattern (the `name=value` / `"name":"value"` forms below) matches purely on
39
+ // the KEY containing a word like "token", but the VALUE is numeric/boolean, it's
40
+ // a count or flag, not a secret. These must stay readable in logs:
41
+ // `tokens.input=45000`, `hasUsageTokens=true`, `max_tokens=4096` are diagnostics,
42
+ // not credentials. (High-entropy secret VALUES are still caught by the
43
+ // value-shaped patterns above — bearer, JWT, AKIA, gh*_, etc. — independent of
44
+ // the key name, so relaxing the key-based match for scalars loses no coverage.)
45
+ function isNonSecretScalarValue(value: string): boolean {
46
+ const v = value.trim();
47
+ if (v === "true" || v === "false" || v === "null" || v === "undefined") return true;
48
+ // Integer or decimal, optional sign/exponent — token counts, ports, sizes.
49
+ return /^[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(v);
50
+ }
51
+
36
52
  const SECRET_QUALIFIERS = new Set([
37
53
  "api",
38
54
  "access",
@@ -155,19 +171,27 @@ const SECRET_TEXT_PATTERNS: Array<{
155
171
  pattern:
156
172
  /(["'])([^"']*(?:key|token|secret|password|auth|bearer|credential)[^"']*)\1(\s*:\s*)(["'])([^"']*)\4/gi,
157
173
  replacement: (
158
- _full: string,
174
+ full: string,
159
175
  quote: string,
160
176
  key: string,
161
177
  separator: string,
162
178
  valueQuote: string,
179
+ value: string,
163
180
  ) =>
164
- `${quote}${key}${quote}${separator}${valueQuote}<REDACTED:${redactionTypeForKey(key)}>${valueQuote}`,
181
+ // A numeric/boolean value matched only because the KEY contains a
182
+ // secret word (e.g. "max_tokens": "4096") is a count, not a secret.
183
+ isNonSecretScalarValue(value)
184
+ ? full
185
+ : `${quote}${key}${quote}${separator}${valueQuote}<REDACTED:${redactionTypeForKey(key)}>${valueQuote}`,
165
186
  },
166
187
  {
167
188
  pattern:
168
189
  /\b([A-Za-z0-9_.-]*(?:key|token|secret|password|auth|bearer|credential)[A-Za-z0-9_.-]*)\s*=\s*([^\s'"`]+)/gi,
169
- replacement: (_full: string, key: string) =>
170
- `${key}=<REDACTED:${redactionTypeForKey(key)}>`,
190
+ replacement: (full: string, key: string, value: string) =>
191
+ // tokens.input=45000 / hasUsageTokens=true are diagnostics, not
192
+ // secrets — keep them readable. Real secret values are still caught
193
+ // by the value-shaped patterns above.
194
+ isNonSecretScalarValue(value) ? full : `${key}=<REDACTED:${redactionTypeForKey(key)}>`,
171
195
  },
172
196
  ];
173
197
 
@@ -60,4 +60,47 @@ describe("ensureTuiPluginEntry", () => {
60
60
  expect(entry[0]).toBe("@cortexkit/opencode-magic-context@latest");
61
61
  expect(entry[1]).toEqual({ enabled: true });
62
62
  });
63
+
64
+ it("creates tui.jsonc (not tui.json) on a fresh install", async () => {
65
+ const root = mkdtempSync(join(tmpdir(), "mc-tui-fresh-"));
66
+ roots.push(root);
67
+ process.env.OPENCODE_CONFIG_DIR = root;
68
+
69
+ const { ensureTuiPluginEntry } = await import("./tui-config");
70
+ expect(ensureTuiPluginEntry()).toBe(true);
71
+
72
+ // The new file must be tui.jsonc so a tui.json stub never ends up
73
+ // sitting next to a tui.jsonc the user writes later (#176).
74
+ expect(existsSync(join(root, "tui.jsonc"))).toBe(true);
75
+ expect(existsSync(join(root, "tui.json"))).toBe(false);
76
+ const parsed = JSON.parse(readFileSync(join(root, "tui.jsonc"), "utf-8")) as {
77
+ plugin: unknown[];
78
+ };
79
+ expect(parsed.plugin).toContain("@cortexkit/opencode-magic-context@latest");
80
+ });
81
+
82
+ it("writes into the existing tui.jsonc when both files exist", async () => {
83
+ const root = mkdtempSync(join(tmpdir(), "mc-tui-both-"));
84
+ roots.push(root);
85
+ process.env.OPENCODE_CONFIG_DIR = root;
86
+ // A real user config in tui.jsonc plus a leftover empty tui.json.
87
+ writeFileSync(
88
+ join(root, "tui.jsonc"),
89
+ `${JSON.stringify({ keybinds: { x: "y" } }, null, 2)}\n`,
90
+ );
91
+ writeFileSync(join(root, "tui.json"), "{}\n");
92
+
93
+ const { ensureTuiPluginEntry } = await import("./tui-config");
94
+ expect(ensureTuiPluginEntry()).toBe(true);
95
+
96
+ // The plugin entry must land in tui.jsonc (higher precedence), and the
97
+ // user's keybinds must survive; tui.json must be left untouched.
98
+ const jsonc = JSON.parse(readFileSync(join(root, "tui.jsonc"), "utf-8")) as {
99
+ plugin: unknown[];
100
+ keybinds: Record<string, string>;
101
+ };
102
+ expect(jsonc.plugin).toContain("@cortexkit/opencode-magic-context@latest");
103
+ expect(jsonc.keybinds).toEqual({ x: "y" });
104
+ expect(readFileSync(join(root, "tui.json"), "utf-8")).toBe("{}\n");
105
+ });
63
106
  });
@@ -62,9 +62,16 @@ function resolveTuiConfigPath(): string {
62
62
  const jsoncPath = join(configDir, "tui.jsonc");
63
63
  const jsonPath = join(configDir, "tui.json");
64
64
 
65
+ // OpenCode loads BOTH tui.json and tui.jsonc and merges them (tui.json first,
66
+ // tui.jsonc second, so .jsonc wins overlapping keys; plugin origins are
67
+ // deduped). So an existing tui.jsonc is the higher-precedence, user-facing
68
+ // file — write into it when present. Otherwise update an existing tui.json.
69
+ // For a fresh install create tui.jsonc, not tui.json: it lets the user add
70
+ // comments later and avoids leaving a second, lower-precedence config file
71
+ // alongside a tui.jsonc they create afterward (#176).
65
72
  if (existsSync(jsoncPath)) return jsoncPath;
66
73
  if (existsSync(jsonPath)) return jsonPath;
67
- return jsonPath; // default: create tui.json
74
+ return jsoncPath;
68
75
  }
69
76
 
70
77
  /**