@cortexkit/opencode-magic-context 0.24.0 → 0.24.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.
package/dist/index.js CHANGED
@@ -154606,15 +154606,22 @@ function ownerMessageIdForTagRow(row) {
154606
154606
  }
154607
154607
  return row.message_id.replace(CONTENT_ID_SUFFIX, "");
154608
154608
  }
154609
- function getActiveTagTokenAggregate(db, sessionId) {
154610
- const row = db.prepare(`SELECT
154609
+ function getActiveTagTokenAggregate(db, sessionId, protectedTags = 0) {
154610
+ const toolOutputExpr = protectedTags > 0 ? `COALESCE(SUM(CASE WHEN type = 'tool' AND tag_number < (
154611
+ SELECT tag_number FROM tags
154612
+ WHERE session_id = ? AND status = 'active'
154613
+ ORDER BY tag_number DESC LIMIT 1 OFFSET ?
154614
+ ) THEN COALESCE(token_count, 0) ELSE 0 END), 0)` : `COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0)`;
154615
+ const sql = `SELECT
154611
154616
  COALESCE(SUM(CASE WHEN type != 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0)
154612
154617
  + COALESCE(SUM(COALESCE(reasoning_token_count, 0)), 0) AS conversation,
154613
154618
  COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) + COALESCE(input_token_count, 0) ELSE 0 END), 0) AS tool_call,
154614
- COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0) AS tool_output,
154619
+ ${toolOutputExpr} AS tool_output,
154615
154620
  COALESCE(SUM(CASE WHEN token_count IS NULL THEN 1 ELSE 0 END), 0) AS null_count
154616
154621
  FROM tags
154617
- WHERE session_id = ? AND status = 'active'`).get(sessionId);
154622
+ WHERE session_id = ? AND status = 'active'`;
154623
+ const params = protectedTags > 0 ? [sessionId, protectedTags - 1, sessionId] : [sessionId];
154624
+ const row = db.prepare(sql).get(...params);
154618
154625
  return {
154619
154626
  conversation: row?.conversation ?? 0,
154620
154627
  toolCall: row?.tool_call ?? 0,
@@ -165369,9 +165376,10 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
165369
165376
  if (lines.length === 0 || endOrdinal < startOrdinal)
165370
165377
  return [];
165371
165378
  const normalizedMax = normalizeCompartmentChunkMaxInputTokens(maxInputTokens);
165379
+ const effectiveMax = Math.max(1, Math.floor(normalizedMax * CHUNK_WINDOW_SAFETY_RATIO));
165372
165380
  const fullText = lines.join(`
165373
165381
  `);
165374
- if (estimateTokens(fullText) <= normalizedMax) {
165382
+ if (estimateTokens(fullText) <= effectiveMax) {
165375
165383
  return [
165376
165384
  {
165377
165385
  windowIndex: 0,
@@ -165409,7 +165417,7 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
165409
165417
  const lineStart = range?.start ?? startOrdinal;
165410
165418
  const lineEnd = range?.end ?? lineStart;
165411
165419
  const lineTokens = estimateTokens(line);
165412
- if (currentLines.length > 0 && currentTokens + lineTokens > normalizedMax) {
165420
+ if (currentLines.length > 0 && currentTokens + lineTokens > effectiveMax) {
165413
165421
  flush2();
165414
165422
  }
165415
165423
  if (currentLines.length === 0) {
@@ -165564,7 +165572,7 @@ function countUnembeddedSessionCompartments(db, projectPath, sessionId, modelId)
165564
165572
  )`).get(projectPath, sessionId, projectPath, modelId);
165565
165573
  return typeof row?.n === "number" ? row.n : 0;
165566
165574
  }
165567
- var DEFAULT_COMPARTMENT_CHUNK_MAX_INPUT_TOKENS = 512, loadFtsRowsStatements, existingHashStatements, existingHashByProjectStatements, deleteByCompartmentStatements, insertEmbeddingStatements, distinctModelStatements, clearProjectStatements, clearProjectModelStatements, searchRowsStatements, searchRowsByModelStatements, backfillCandidateStatements, sessionBackfillCandidateStatements;
165575
+ var DEFAULT_COMPARTMENT_CHUNK_MAX_INPUT_TOKENS = 512, CHUNK_WINDOW_SAFETY_RATIO = 0.9, loadFtsRowsStatements, existingHashStatements, existingHashByProjectStatements, deleteByCompartmentStatements, insertEmbeddingStatements, distinctModelStatements, clearProjectStatements, clearProjectModelStatements, searchRowsStatements, searchRowsByModelStatements, backfillCandidateStatements, sessionBackfillCandidateStatements;
165568
165576
  var init_compartment_chunk_embedding = __esm(() => {
165569
165577
  init_read_session_formatting();
165570
165578
  loadFtsRowsStatements = new WeakMap;
@@ -166032,6 +166040,13 @@ var init_embedding_ssrf = __esm(() => {
166032
166040
  function normalizeEndpoint3(endpoint) {
166033
166041
  return endpoint?.trim().replace(/\/+$/, "") ?? "";
166034
166042
  }
166043
+ function embeddingModelsMatch(served, requested) {
166044
+ const a = served.trim().toLowerCase();
166045
+ const b = requested.trim().toLowerCase();
166046
+ if (a.length === 0 || b.length === 0)
166047
+ return true;
166048
+ return a === b || a.includes(b) || b.includes(a);
166049
+ }
166035
166050
 
166036
166051
  class OpenAICompatibleEmbeddingProvider {
166037
166052
  modelId;
@@ -166045,6 +166060,7 @@ class OpenAICompatibleEmbeddingProvider {
166045
166060
  failureTimes = [];
166046
166061
  circuitOpenUntil = 0;
166047
166062
  openLogged = false;
166063
+ modelMismatchLogged = false;
166048
166064
  halfOpenProbeInFlight = false;
166049
166065
  constructor(options) {
166050
166066
  this.endpoint = normalizeEndpoint3(options.endpoint);
@@ -166143,6 +166159,15 @@ class OpenAICompatibleEmbeddingProvider {
166143
166159
  this.recordFailure(isProbe);
166144
166160
  return Array.from({ length: texts.length }, () => null);
166145
166161
  }
166162
+ const servedModel = typeof body.model === "string" ? body.model : "";
166163
+ if (this.model && servedModel && !embeddingModelsMatch(servedModel, this.model)) {
166164
+ if (!this.modelMismatchLogged) {
166165
+ log(`[magic-context] embedding endpoint served a DIFFERENT model than requested — refusing the substituted vectors (they have the wrong dimensions/space). requested="${this.model}" served="${servedModel}". The endpoint likely substituted a loaded model; load/select "${this.model}" on the endpoint, or set embedding.model to the served model.`);
166166
+ this.modelMismatchLogged = true;
166167
+ }
166168
+ this.recordFailure(isProbe);
166169
+ return Array.from({ length: texts.length }, () => null);
166170
+ }
166146
166171
  const items = Array.isArray(body.data) ? body.data : [];
166147
166172
  const results = Array.from({ length: texts.length }, (_, index) => {
166148
166173
  const embedding = items[index]?.embedding;
@@ -166629,7 +166654,7 @@ function getChunkEmbeddingModelId(config2, providerIdentity) {
166629
166654
  }
166630
166655
  const chunkIdentity = {
166631
166656
  providerIdentity,
166632
- chunkerVersion: 1,
166657
+ chunkerVersion: 2,
166633
166658
  maxInputTokens: normalizeCompartmentChunkMaxInputTokens("max_input_tokens" in config2 ? config2.max_input_tokens : undefined),
166634
166659
  truncate: config2.provider === "openai-compatible" ? config2.truncate ?? "" : ""
166635
166660
  };
@@ -169268,7 +169293,7 @@ ${prepared.block}
169268
169293
  if (!firstMessage || !textPart || isDroppedPlaceholder(textPart.text)) {
169269
169294
  messages.unshift({
169270
169295
  info: { role: "user", sessionID: sessionId },
169271
- parts: [{ type: "text", text: historyBlock }]
169296
+ parts: [{ type: "text", text: historyBlock, synthetic: true }]
169272
169297
  });
169273
169298
  } else {
169274
169299
  textPart.text = `${historyBlock}
@@ -170158,10 +170183,16 @@ function softRefreshCachedM1(options) {
170158
170183
  function prependM0M1Messages(sessionId, messages, m0Text, m1Text) {
170159
170184
  messages.unshift({
170160
170185
  info: { role: "user", sessionID: sessionId },
170161
- parts: [{ type: "text", text: m0Text.length > 0 ? m0Text : M0_EMPTY_BODY }]
170186
+ parts: [
170187
+ {
170188
+ type: "text",
170189
+ text: m0Text.length > 0 ? m0Text : M0_EMPTY_BODY,
170190
+ synthetic: true
170191
+ }
170192
+ ]
170162
170193
  }, {
170163
170194
  info: { role: "user", sessionID: sessionId },
170164
- parts: [{ type: "text", text: m1Text }]
170195
+ parts: [{ type: "text", text: m1Text, synthetic: true }]
170165
170196
  });
170166
170197
  }
170167
170198
  function renderFreshM0NonPersisted(options) {
@@ -177079,6 +177110,11 @@ async function runManagedRecomp(ctx, sessionId, options) {
177079
177110
  try {
177080
177111
  const message = await executeContextRecomp(buildRecompDeps(ctx, sessionId), options);
177081
177112
  const terminalPhase = isRecompSkip(message) ? "skipped" : isRecompFailure(message) ? "failed" : "done";
177113
+ if (terminalPhase === "done") {
177114
+ try {
177115
+ clearEmergencyRecovery(ctx.db, sessionId);
177116
+ } catch {}
177117
+ }
177082
177118
  setRecompTerminal(ctx.liveSessionState, sessionId, terminalPhase, extractRecompReason(message));
177083
177119
  return message;
177084
177120
  } catch (error51) {
@@ -177177,6 +177213,7 @@ var RECOMP_DONE_GRACE_MS = 30000;
177177
177213
  var init_recomp_orchestrator = __esm(async () => {
177178
177214
  init_compartment_storage();
177179
177215
  init_project_identity2();
177216
+ init_storage_meta_persisted();
177180
177217
  await __promiseAll([
177181
177218
  init_memory_migration(),
177182
177219
  init_compartment_runner()
@@ -183375,8 +183412,8 @@ var CHANNEL1_SENTINEL = "<system-reminder>";
183375
183412
  var TOKENS_PER_BYTE = 0.25;
183376
183413
  var CHANNEL1_FLOOR_TOKENS = 1e4;
183377
183414
  var CHANNEL1_REFIRE_FLOOR_TOKENS = 1e4;
183378
- function channel1RefireTokens(historyBudgetTokens) {
183379
- const scaled = Math.round(0.05 * Math.max(0, historyBudgetTokens));
183415
+ function channel1RefireTokens(workingWindowTokens) {
183416
+ const scaled = Math.round(0.05 * Math.max(0, workingWindowTokens));
183380
183417
  return Math.max(CHANNEL1_REFIRE_FLOOR_TOKENS, scaled);
183381
183418
  }
183382
183419
  var S_GENTLE = 0.2;
@@ -183446,7 +183483,7 @@ function computeTailTokenEstimate(messages) {
183446
183483
  };
183447
183484
  }
183448
183485
  function decideChannel1(input) {
183449
- const { undroppedTokens, pressure, historyBudgetTokens, hasRecentReduce } = input;
183486
+ const { undroppedTokens, pressure, workingWindowTokens, hasRecentReduce } = input;
183450
183487
  const resetCycle = hasRecentReduce || undroppedTokens < input.lastNudgeUndropped;
183451
183488
  const lastNudge = resetCycle ? 0 : input.lastNudgeUndropped;
183452
183489
  const lastLevel = resetCycle ? "" : input.lastNudgeLevel;
@@ -183461,7 +183498,7 @@ function decideChannel1(input) {
183461
183498
  return quiet();
183462
183499
  if (undroppedTokens < CHANNEL1_FLOOR_TOKENS)
183463
183500
  return quiet();
183464
- const budget = historyBudgetTokens > 0 ? historyBudgetTokens : undroppedTokens || 1;
183501
+ const budget = workingWindowTokens > 0 ? workingWindowTokens : undroppedTokens || 1;
183465
183502
  const severity = undroppedTokens / budget * pressure;
183466
183503
  if (severity < S_GENTLE)
183467
183504
  return quiet();
@@ -183473,7 +183510,7 @@ function decideChannel1(input) {
183473
183510
  else
183474
183511
  level = "gentle";
183475
183512
  if (lastLevel === "") {
183476
- if (undroppedTokens < lastNudge + channel1RefireTokens(historyBudgetTokens)) {
183513
+ if (undroppedTokens < lastNudge + channel1RefireTokens(workingWindowTokens)) {
183477
183514
  return quiet();
183478
183515
  }
183479
183516
  } else if (LEVEL_RANK[level] <= LEVEL_RANK[lastLevel]) {
@@ -183518,13 +183555,13 @@ function buildChannel1Reminder(level, undroppedTokens) {
183518
183555
  let body;
183519
183556
  switch (level) {
183520
183557
  case "gentle":
183521
- body = `You have ~${amount} tokens of tool output you have not reduced. ` + `Once you are done with earlier outputs, drop them with ctx_reduce to keep context lean.`;
183558
+ body = `You have ~${amount} tokens of tool output you have not reduced. ` + `When you are done with earlier outputs, dropping them with ctx_reduce keeps context lean.`;
183522
183559
  break;
183523
183560
  case "firm":
183524
- body = `~${amount} tokens of unreduced tool output is accumulating. ` + `Drop what you have already processed with ctx_reduce before continuing.`;
183561
+ body = `~${amount} tokens of unreduced tool output has built up. ` + `At your next natural stopping point, consider dropping what you have already processed with ctx_reduce.`;
183525
183562
  break;
183526
183563
  case "urgent":
183527
- body = `~${amount} tokens of unreduced tool output remain. ` + `A large span of this session will be comparted soon; drop spent outputs with ctx_reduce first so the archived span is the part that matters.`;
183564
+ body = `~${amount} tokens of unreduced tool output remain, and a large span of this session will be comparted before long. ` + `Consider dropping spent outputs with ctx_reduce so the archived span is the part that matters.`;
183528
183565
  break;
183529
183566
  }
183530
183567
  return `
@@ -185112,8 +185149,55 @@ function appendReminderToUserMessage(message, reminder) {
185112
185149
  }
185113
185150
 
185114
185151
  // src/hooks/magic-context/apply-operations.ts
185115
- init_tag_part_guards();
185116
185152
  await init_storage();
185153
+
185154
+ // src/hooks/magic-context/system-injection-stripper.ts
185155
+ var SYSTEM_INJECTION_MARKERS = [
185156
+ "<!-- OMO_INTERNAL_INITIATOR -->",
185157
+ "[SYSTEM DIRECTIVE: MAGIC-CONTEXT",
185158
+ "[SYSTEM DIRECTIVE: OH-MY-OPENCODE",
185159
+ "[Category+Skill Reminder]",
185160
+ "[EDIT ERROR - IMMEDIATE ACTION REQUIRED]",
185161
+ "[task CALL FAILED - IMMEDIATE RETRY REQUIRED]",
185162
+ "[EMERGENCY CONTEXT WINDOW WARNING]",
185163
+ "Unstable background agent appears idle",
185164
+ "**THE SUBAGENT JUST CLAIMED THIS TASK IS DONE."
185165
+ ];
185166
+ var SYSTEM_REMINDER_REGEX = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
185167
+ var OMO_MARKER_REGEX = /<!-- OMO_INTERNAL_INITIATOR -->/g;
185168
+ function stripSystemInjection(text) {
185169
+ let hasInjection = false;
185170
+ for (const marker of SYSTEM_INJECTION_MARKERS) {
185171
+ if (text.includes(marker)) {
185172
+ hasInjection = true;
185173
+ break;
185174
+ }
185175
+ }
185176
+ if (SYSTEM_REMINDER_REGEX.test(text))
185177
+ hasInjection = true;
185178
+ SYSTEM_REMINDER_REGEX.lastIndex = 0;
185179
+ if (!hasInjection)
185180
+ return null;
185181
+ let cleaned = text;
185182
+ cleaned = cleaned.replace(SYSTEM_REMINDER_REGEX, "");
185183
+ cleaned = cleaned.replace(OMO_MARKER_REGEX, "");
185184
+ cleaned = cleaned.replace(/\[SYSTEM DIRECTIVE: OH-MY-(?:OPENCODE|CLAUDE)[^\]]*\][\s\S]*?(?=\n\n(?!\s*[-*])|$)/g, "");
185185
+ for (const marker of SYSTEM_INJECTION_MARKERS) {
185186
+ if (marker.startsWith("<!-- ") || marker.startsWith("[SYSTEM DIRECTIVE"))
185187
+ continue;
185188
+ const idx = cleaned.indexOf(marker);
185189
+ if (idx === -1)
185190
+ continue;
185191
+ const blockEnd = cleaned.indexOf(`
185192
+
185193
+ `, idx + marker.length);
185194
+ cleaned = blockEnd !== -1 ? cleaned.slice(0, idx) + cleaned.slice(blockEnd) : cleaned.slice(0, idx);
185195
+ }
185196
+ return cleaned.trim();
185197
+ }
185198
+
185199
+ // src/hooks/magic-context/apply-operations.ts
185200
+ init_tag_part_guards();
185117
185201
  var USER_DROP_PREVIEW_CHARS = 250;
185118
185202
  var RECENT_TOOL_SKELETON_WINDOW = 20;
185119
185203
  function buildReplacementContent(tagId, target) {
@@ -185122,6 +185206,10 @@ function buildReplacementContent(tagId, target) {
185122
185206
  return `[dropped §${tagId}§]`;
185123
185207
  }
185124
185208
  const currentContent = target.getContent?.() ?? "";
185209
+ const strippedInjection = stripSystemInjection(currentContent);
185210
+ if (strippedInjection !== null && stripTagPrefix(strippedInjection).trim().length === 0) {
185211
+ return `[dropped §${tagId}§]`;
185212
+ }
185125
185213
  const originalText = stripTagPrefix(currentContent);
185126
185214
  if (originalText.length <= USER_DROP_PREVIEW_CHARS) {
185127
185215
  return `[truncated §${tagId}§]
@@ -186828,51 +186916,6 @@ function planEmergencyDrop(input) {
186828
186916
  };
186829
186917
  }
186830
186918
 
186831
- // src/hooks/magic-context/system-injection-stripper.ts
186832
- var SYSTEM_INJECTION_MARKERS = [
186833
- "<!-- OMO_INTERNAL_INITIATOR -->",
186834
- "[SYSTEM DIRECTIVE: MAGIC-CONTEXT",
186835
- "[SYSTEM DIRECTIVE: OH-MY-OPENCODE",
186836
- "[Category+Skill Reminder]",
186837
- "[EDIT ERROR - IMMEDIATE ACTION REQUIRED]",
186838
- "[task CALL FAILED - IMMEDIATE RETRY REQUIRED]",
186839
- "[EMERGENCY CONTEXT WINDOW WARNING]",
186840
- "Unstable background agent appears idle",
186841
- "**THE SUBAGENT JUST CLAIMED THIS TASK IS DONE."
186842
- ];
186843
- var SYSTEM_REMINDER_REGEX = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
186844
- var OMO_MARKER_REGEX = /<!-- OMO_INTERNAL_INITIATOR -->/g;
186845
- function stripSystemInjection(text) {
186846
- let hasInjection = false;
186847
- for (const marker of SYSTEM_INJECTION_MARKERS) {
186848
- if (text.includes(marker)) {
186849
- hasInjection = true;
186850
- break;
186851
- }
186852
- }
186853
- if (SYSTEM_REMINDER_REGEX.test(text))
186854
- hasInjection = true;
186855
- SYSTEM_REMINDER_REGEX.lastIndex = 0;
186856
- if (!hasInjection)
186857
- return null;
186858
- let cleaned = text;
186859
- cleaned = cleaned.replace(SYSTEM_REMINDER_REGEX, "");
186860
- cleaned = cleaned.replace(OMO_MARKER_REGEX, "");
186861
- cleaned = cleaned.replace(/\[SYSTEM DIRECTIVE: OH-MY-(?:OPENCODE|CLAUDE)[^\]]*\][\s\S]*?(?=\n\n(?!\s*[-*])|$)/g, "");
186862
- for (const marker of SYSTEM_INJECTION_MARKERS) {
186863
- if (marker.startsWith("<!-- ") || marker.startsWith("[SYSTEM DIRECTIVE"))
186864
- continue;
186865
- const idx = cleaned.indexOf(marker);
186866
- if (idx === -1)
186867
- continue;
186868
- const blockEnd = cleaned.indexOf(`
186869
-
186870
- `, idx + marker.length);
186871
- cleaned = blockEnd !== -1 ? cleaned.slice(0, idx) + cleaned.slice(blockEnd) : cleaned.slice(0, idx);
186872
- }
186873
- return cleaned.trim();
186874
- }
186875
-
186876
186919
  // src/hooks/magic-context/heuristic-cleanup.ts
186877
186920
  init_tag_part_guards();
186878
186921
  var DEDUP_SAFE_TOOLS = new Set([
@@ -188336,7 +188379,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
188336
188379
  let tailToolTokens;
188337
188380
  let liveTailTokens;
188338
188381
  try {
188339
- const agg = getActiveTagTokenAggregate(db, sessionId);
188382
+ const agg = getActiveTagTokenAggregate(db, sessionId, deps.protectedTags);
188340
188383
  tailToolTokens = agg.toolOutput;
188341
188384
  liveTailTokens = agg.conversation + agg.toolCall;
188342
188385
  } catch {
@@ -188947,10 +188990,11 @@ function maybeInjectChannel1Nudge(args, sessionId, tool, output) {
188947
188990
  contextLimit: state.contextLimit,
188948
188991
  executeThresholdPercentage: state.executeThresholdPercentage
188949
188992
  });
188993
+ const workingWindowTokens = Math.round(state.contextLimit * state.executeThresholdPercentage / 100);
188950
188994
  const decision = decideChannel1({
188951
188995
  undroppedTokens,
188952
188996
  pressure,
188953
- historyBudgetTokens: state.historyBudgetTokens,
188997
+ workingWindowTokens,
188954
188998
  lastNudgeUndropped: getLastNudgeUndropped(args.db, sessionId),
188955
188999
  lastNudgeLevel: getLastNudgeLevel(args.db, sessionId),
188956
189000
  hasRecentReduce: false
@@ -0,0 +1,32 @@
1
+ export declare const TUI_PREFS_FILE_ENV = "OPENCODE_TUI_PREFERENCES_FILE";
2
+ export declare function getTuiPreferencesFile(): string;
3
+ export declare function readTuiPreferencesFile(): Promise<Record<string, unknown>>;
4
+ export declare function readTuiPreferencesFileSync(): Record<string, unknown>;
5
+ export declare const PLUGIN_KEY = "magic-context";
6
+ export declare const DEFAULT_SLOT_ORDER = 200;
7
+ export interface MagicContextTuiPrefs {
8
+ forceToTop: boolean;
9
+ order: number;
10
+ startCollapsed: boolean;
11
+ rememberCollapsed: boolean;
12
+ collapsed: boolean | null;
13
+ header: {
14
+ label: string;
15
+ };
16
+ sections: {
17
+ historian: boolean;
18
+ memory: boolean;
19
+ status: boolean;
20
+ dreamer: boolean;
21
+ stats: boolean;
22
+ };
23
+ }
24
+ export type TuiSections = MagicContextTuiPrefs["sections"];
25
+ export declare const DEFAULT_PREFS: MagicContextTuiPrefs;
26
+ export declare function resolveMagicContextPrefs(root: Record<string, unknown>): MagicContextTuiPrefs;
27
+ export declare function computeEffectiveOrder(root: Record<string, unknown>, pluginKey: string, defaultOrder: number): number;
28
+ type JsonValue = string | number | boolean | null;
29
+ export declare function queueTuiPreferenceUpdate(pluginKey: string, path: string[], value: JsonValue): Promise<void>;
30
+ export declare function watchTuiPreferences(onChange: () => void): () => void;
31
+ export {};
32
+ //# sourceMappingURL=tui-preferences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui-preferences.d.ts","sourceRoot":"","sources":["../../src/shared/tui-preferences.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,kBAAkB,kCAAkC,CAAC;AAGlE,wBAAgB,qBAAqB,IAAI,MAAM,CAO9C;AAQD,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAS/E;AAMD,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASpE;AAED,eAAO,MAAM,UAAU,kBAAkB,CAAC;AAC1C,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,MAAM,WAAW,oBAAoB;IACjC,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,EAAE,OAAO,CAAC;IAE3B,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE;QACN,SAAS,EAAE,OAAO,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,EAAE,OAAO,CAAC;KAClB,CAAC;CACL;AAED,MAAM,MAAM,WAAW,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;AAE3D,eAAO,MAAM,aAAa,EAAE,oBAc3B,CAAC;AAmBF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,oBAAoB,CAyB5F;AAgBD,wBAAgB,qBAAqB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACrB,MAAM,CAOR;AASD,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AA0DlD,wBAAgB,wBAAwB,CACpC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,SAAS,GACjB,OAAO,CAAC,IAAI,CAAC,CAGf;AAeD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAuCpE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,210 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { parse } from "comment-json";
6
+ import {
7
+ computeEffectiveOrder,
8
+ DEFAULT_PREFS,
9
+ DEFAULT_SLOT_ORDER,
10
+ getTuiPreferencesFile,
11
+ PLUGIN_KEY,
12
+ queueTuiPreferenceUpdate,
13
+ readTuiPreferencesFile,
14
+ resolveMagicContextPrefs,
15
+ TUI_PREFS_FILE_ENV,
16
+ } from "./tui-preferences";
17
+
18
+ let dir: string;
19
+ let file: string;
20
+ const savedEnv: Record<string, string | undefined> = {};
21
+ const ENV_KEYS = [TUI_PREFS_FILE_ENV, "OPENCODE_CONFIG_DIR", "XDG_CONFIG_HOME"];
22
+
23
+ beforeEach(async () => {
24
+ for (const key of ENV_KEYS) savedEnv[key] = process.env[key];
25
+ dir = await mkdtemp(join(tmpdir(), "mc-tui-prefs-test-"));
26
+ file = join(dir, "tui-preferences.jsonc");
27
+ process.env[TUI_PREFS_FILE_ENV] = file;
28
+ });
29
+
30
+ afterEach(async () => {
31
+ for (const key of ENV_KEYS) {
32
+ if (savedEnv[key] === undefined) delete process.env[key];
33
+ else process.env[key] = savedEnv[key];
34
+ }
35
+ await rm(dir, { recursive: true, force: true });
36
+ });
37
+
38
+ describe("getTuiPreferencesFile", () => {
39
+ test("env override wins", () => {
40
+ expect(getTuiPreferencesFile()).toBe(file);
41
+ });
42
+
43
+ test("falls back to OPENCODE_CONFIG_DIR then XDG then ~/.config", () => {
44
+ delete process.env[TUI_PREFS_FILE_ENV];
45
+ process.env.OPENCODE_CONFIG_DIR = "/tmp/cfgdir";
46
+ expect(getTuiPreferencesFile()).toBe("/tmp/cfgdir/tui-preferences.jsonc");
47
+ delete process.env.OPENCODE_CONFIG_DIR;
48
+ process.env.XDG_CONFIG_HOME = "/tmp/xdg";
49
+ expect(getTuiPreferencesFile()).toBe("/tmp/xdg/opencode/tui-preferences.jsonc");
50
+ });
51
+ });
52
+
53
+ describe("readTuiPreferencesFile (tolerant)", () => {
54
+ test("missing file → {}", async () => {
55
+ expect(await readTuiPreferencesFile()).toEqual({});
56
+ });
57
+
58
+ test("malformed JSON → {}", async () => {
59
+ await writeFile(file, "{ this is not json ", "utf8");
60
+ expect(await readTuiPreferencesFile()).toEqual({});
61
+ });
62
+
63
+ test("non-object root → {}", async () => {
64
+ await writeFile(file, "[1, 2, 3]", "utf8");
65
+ expect(await readTuiPreferencesFile()).toEqual({});
66
+ });
67
+
68
+ test("jsonc with comments + trailing comma parses", async () => {
69
+ await writeFile(
70
+ file,
71
+ `{
72
+ // a comment
73
+ "magic-context": { "order": 205, },
74
+ }`,
75
+ "utf8",
76
+ );
77
+ const root = await readTuiPreferencesFile();
78
+ expect(resolveMagicContextPrefs(root).order).toBe(205);
79
+ });
80
+ });
81
+
82
+ describe("resolveMagicContextPrefs (per-key validation)", () => {
83
+ test("missing key → full defaults clone", () => {
84
+ expect(resolveMagicContextPrefs({})).toEqual(DEFAULT_PREFS);
85
+ // clone, not the shared object
86
+ expect(resolveMagicContextPrefs({})).not.toBe(DEFAULT_PREFS);
87
+ });
88
+
89
+ test("one bad value never poisons the rest", () => {
90
+ const prefs = resolveMagicContextPrefs({
91
+ "magic-context": {
92
+ order: "nope",
93
+ rememberCollapsed: 1,
94
+ collapsed: true,
95
+ sections: { historian: false, memory: "bad" },
96
+ },
97
+ });
98
+ expect(prefs.order).toBe(DEFAULT_SLOT_ORDER); // bad → default
99
+ expect(prefs.rememberCollapsed).toBe(true); // bad → default true
100
+ expect(prefs.collapsed).toBe(true); // valid bool preserved
101
+ expect(prefs.sections.historian).toBe(false); // valid bool preserved
102
+ expect(prefs.sections.memory).toBe(true); // bad → default true
103
+ });
104
+
105
+ test("order clamps to -10000..10000", () => {
106
+ expect(resolveMagicContextPrefs({ "magic-context": { order: 99999 } }).order).toBe(10000);
107
+ expect(resolveMagicContextPrefs({ "magic-context": { order: -99999 } }).order).toBe(-10000);
108
+ });
109
+
110
+ test("collapsed non-boolean → null (seed from startCollapsed)", () => {
111
+ expect(resolveMagicContextPrefs({ "magic-context": {} }).collapsed).toBeNull();
112
+ });
113
+
114
+ test("header label clamps length, empty → default", () => {
115
+ expect(
116
+ resolveMagicContextPrefs({ "magic-context": { header: { label: "" } } }).header.label,
117
+ ).toBe(DEFAULT_PREFS.header.label);
118
+ expect(
119
+ resolveMagicContextPrefs({
120
+ "magic-context": { header: { label: "x".repeat(50) } },
121
+ }).header.label.length,
122
+ ).toBe(24);
123
+ });
124
+ });
125
+
126
+ describe("computeEffectiveOrder (cross-plugin convention)", () => {
127
+ test("default when key missing", () => {
128
+ expect(computeEffectiveOrder({}, PLUGIN_KEY, DEFAULT_SLOT_ORDER)).toBe(DEFAULT_SLOT_ORDER);
129
+ });
130
+
131
+ test("explicit order clamped", () => {
132
+ expect(computeEffectiveOrder({ "magic-context": { order: 250 } }, PLUGIN_KEY, 200)).toBe(
133
+ 250,
134
+ );
135
+ });
136
+
137
+ test("forceToTop sorts below FORCE_TOP_BASE by key position", () => {
138
+ const root = { aft: { forceToTop: true }, "magic-context": { forceToTop: true } };
139
+ expect(computeEffectiveOrder(root, "aft", 200)).toBe(-100000 + 0);
140
+ expect(computeEffectiveOrder(root, "magic-context", 200)).toBe(-100000 + 1);
141
+ // forced always beats any manual order (clamped band is strictly above)
142
+ expect(computeEffectiveOrder(root, "aft", 200)).toBeLessThan(-10000);
143
+ });
144
+ });
145
+
146
+ describe("write path — comment-json full round-trip", () => {
147
+ test("persists a nested key and reads back", async () => {
148
+ await queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], true);
149
+ const prefs = resolveMagicContextPrefs(await readTuiPreferencesFile());
150
+ expect(prefs.collapsed).toBe(true);
151
+ });
152
+
153
+ test("seeds the file from the template when absent", async () => {
154
+ await queueTuiPreferenceUpdate(PLUGIN_KEY, ["order"], 205);
155
+ const text = await readFile(file, "utf8");
156
+ expect(text).toContain("Shared preferences for OpenCode TUI plugins");
157
+ expect(resolveMagicContextPrefs(await readTuiPreferencesFile()).order).toBe(205);
158
+ });
159
+
160
+ test("INTEROP: a sibling plugin's values AND comments survive MC writing only its key", async () => {
161
+ // A shared file owned partly by anthropic-auth, with comments and an
162
+ // appearance block MC knows nothing about. MC must touch ONLY its key.
163
+ await writeFile(
164
+ file,
165
+ `{
166
+ // anthropic-auth section — DO NOT lose this BLOCK comment
167
+ "anthropic-auth": {
168
+ "order": 160,
169
+ "header": { "label": "CLAUDE" },
170
+ // bar appearance knobs MC has no schema for
171
+ "appearance": { "barWidth": 10, "barFilledChar": "#" },
172
+ "pollMs": 2000 // INLINE trailing comment — must survive too
173
+ },
174
+ "magic-context": { "order": 200 }
175
+ }
176
+ `,
177
+ "utf8",
178
+ );
179
+
180
+ await queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], true);
181
+
182
+ const text = await readFile(file, "utf8");
183
+ // sibling comments preserved — BOTH block and inline trailing
184
+ // (comment-json round-trips both faithfully; enforce the guarantee).
185
+ expect(text).toContain("anthropic-auth section — DO NOT lose this BLOCK comment");
186
+ expect(text).toContain("bar appearance knobs MC has no schema for");
187
+ expect(text).toContain("INLINE trailing comment — must survive too");
188
+
189
+ // sibling VALUES intact (incl. nested keys MC has no schema for)
190
+ const root = parse(text) as Record<string, Record<string, unknown>>;
191
+ const aa = root["anthropic-auth"] as Record<string, unknown>;
192
+ expect(aa.order).toBe(160);
193
+ expect((aa.header as Record<string, unknown>).label).toBe("CLAUDE");
194
+ const appearance = aa.appearance as Record<string, unknown>;
195
+ expect(appearance.barWidth).toBe(10);
196
+ expect(appearance.barFilledChar).toBe("#");
197
+
198
+ // MC's own change landed
199
+ expect(resolveMagicContextPrefs(root).collapsed).toBe(true);
200
+ expect(resolveMagicContextPrefs(root).order).toBe(200);
201
+ });
202
+
203
+ test("malformed existing file → write is a no-op, sibling content untouched", async () => {
204
+ const broken = `{ "anthropic-auth": { "order": 160 } broken `;
205
+ await writeFile(file, broken, "utf8");
206
+ await queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], true);
207
+ // unchanged — we never clobber a file we can't safely parse
208
+ expect(await readFile(file, "utf8")).toBe(broken);
209
+ });
210
+ });