@wolfx/opencode-magic-context 0.21.8-patch.1 → 0.21.8-patch.4

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
@@ -14924,7 +14924,6 @@ var init_magic_context = __esm(() => {
14924
14924
  });
14925
14925
  MagicContextConfigSchema = exports_external.object({
14926
14926
  enabled: exports_external.boolean().default(true),
14927
- auto_update: exports_external.boolean().optional(),
14928
14927
  ctx_reduce_enabled: exports_external.boolean().default(true),
14929
14928
  historian: HistorianConfigSchema,
14930
14929
  dreamer: DreamerConfigSchema.optional(),
@@ -14986,6 +14985,10 @@ var init_magic_context = __esm(() => {
14986
14985
  score_threshold: exports_external.number().min(0.3).max(0.95).default(0.6),
14987
14986
  min_prompt_chars: exports_external.number().min(5).max(500).default(20)
14988
14987
  }).default({ enabled: false, score_threshold: 0.6, min_prompt_chars: 20 }),
14988
+ text_complete_strip: exports_external.object({
14989
+ enabled: exports_external.boolean().default(true),
14990
+ patterns: exports_external.array(exports_external.string()).optional()
14991
+ }).optional(),
14989
14992
  caveman_text_compression: exports_external.object({
14990
14993
  enabled: exports_external.boolean().default(false),
14991
14994
  min_chars: exports_external.number().min(100).max(1e4).default(500)
@@ -152939,6 +152942,907 @@ var init_subagent_token_capture = __esm(() => {
152939
152942
  init_logger();
152940
152943
  });
152941
152944
 
152945
+ // src/hooks/magic-context/compartment-prompt.ts
152946
+ function buildHistorianEditorPrompt(draft) {
152947
+ return [
152948
+ "This is a historian draft. Clean it up following the rules in your system prompt.",
152949
+ "",
152950
+ "<draft>",
152951
+ draft,
152952
+ "</draft>",
152953
+ "",
152954
+ "Return the cleaned draft as valid XML matching the original structure."
152955
+ ].join(`
152956
+ `);
152957
+ }
152958
+ function buildCompressorPrompt(compartments, currentTokens, targetTokens, outputDepth, outputCount) {
152959
+ const lines = [];
152960
+ const densityLabel = outputDepth === 1 ? "MERGE ONLY" : outputDepth === 2 ? "LITE TIGHTEN" : outputDepth === 3 ? "FULL CONDENSE" : "ULTRA TELEGRAPH";
152961
+ const resolvedOutputCount = outputCount ?? Math.max(1, Math.ceil(compartments.length / 2));
152962
+ lines.push(`Density target: LEVEL ${outputDepth} (${densityLabel}). See system prompt for level rules.`);
152963
+ lines.push(`Input: ${compartments.length} compartments, ~${currentTokens} tokens. Target output: exactly ${resolvedOutputCount} compartments, ~${targetTokens} tokens total.`);
152964
+ lines.push("");
152965
+ if (outputDepth === 1) {
152966
+ lines.push("Merge only. Preserve narrative and all U: lines. Drop only genuine duplicate sentences spanning compartments.");
152967
+ } else if (outputDepth === 2) {
152968
+ lines.push("Merge + drop filler words and hedging. Keep grammar, keep U: lines verbatim.");
152969
+ } else if (outputDepth === 3) {
152970
+ lines.push("Merge into single-paragraph compartments. Drop articles and weak auxiliaries. Keep only IRREPLACEABLE U: lines.");
152971
+ } else {
152972
+ lines.push("Merge into telegraphic fragments with symbol connectives (→ + // |). U: lines only if truly irreplaceable.");
152973
+ }
152974
+ lines.push("");
152975
+ lines.push("Preserved literally at all levels: commit hashes, file paths, URLs, code spans.");
152976
+ lines.push("");
152977
+ for (const c of compartments) {
152978
+ lines.push(`<compartment start="${c.startMessage}" end="${c.endMessage}" title="${escapeXmlAttr(c.title)}">`);
152979
+ lines.push(escapeXmlContent(c.content));
152980
+ lines.push("</compartment>");
152981
+ lines.push("");
152982
+ }
152983
+ lines.push("Return merged compartments as XML.");
152984
+ return lines.join(`
152985
+ `);
152986
+ }
152987
+ function buildCompartmentAgentPrompt(existingState, inputSource, options) {
152988
+ const existingStateBlock = options?.stateFilePath ? `Read the existing session state from this file before proceeding:
152989
+ ${options.stateFilePath}
152990
+
152991
+ The file contains the full XML existing state (compartments, facts, memories). Read it first, then process the new messages below.` : existingState;
152992
+ return [
152993
+ "Existing state (read-only context for continuity and fact normalization — do NOT re-emit these compartments):",
152994
+ existingStateBlock,
152995
+ "",
152996
+ "<new_messages>",
152997
+ inputSource,
152998
+ "</new_messages>",
152999
+ "",
153000
+ "Instructions:",
153001
+ "- Return ONLY new compartments for the messages inside <new_messages>, plus the full normalized fact list.",
153002
+ "- Use the exact absolute raw ordinals from the input ranges for every compartment start/end and for <unprocessed_from>.",
153003
+ "- Rewrite every fact into terse, present-tense operational form. Merge semantic duplicates within each category.",
153004
+ "- Drop any session fact already covered by a project memory in the existing state.",
153005
+ "- Do not preserve prior narrative wording verbatim; if a fact is already canonical and still correct, keep or lightly normalize it.",
153006
+ "- Drop obsolete or task-local facts.",
153007
+ ...options?.userMemoriesEnabled ? [
153008
+ "- Also emit <user_observations> with universal behavioral observations about the user (see system prompt for rules)."
153009
+ ] : []
153010
+ ].join(`
153011
+ `);
153012
+ }
153013
+ var COMPARTMENT_AGENT_SYSTEM_PROMPT = `You condense long AI coding sessions into two outputs:
153014
+
153015
+ 1. compartments: completed logical work units
153016
+ 2. facts: persistent cross-cutting information for future work
153017
+
153018
+ Compartment rules:
153019
+ - A compartment is one contiguous completed work unit: investigation, fix, refactor, docs update, feature, or decision.
153020
+ - Start a new compartment only when the work clearly pivots to a different objective.
153021
+ - If one broad effort contains multiple completed sub-pivots with distinct outcomes, prefer multiple smaller compartments over one umbrella compartment with many U: lines.
153022
+ - Do not create compartments for magic-context commands or tool-only noise.
153023
+ - If the input ends mid-topic, leave it out and report its first message index in <unprocessed_from>.
153024
+ - All compartment start/end ordinals and <unprocessed_from> must use the absolute raw message numbers shown in the input. Never renumber relative to this chunk.
153025
+ - Every displayed raw message ordinal in the input MUST appear in exactly one compartment. Gaps between compartments are invalid. When a displayed block is pure tool noise (e.g. a long "TC: ..." run with no narrative text), do NOT skip it — extend the preceding compartment's \`end\` to absorb the range, or include it inside the current compartment if the block falls within an ongoing work unit. Never create a dedicated compartment just to cover a tool-only run.
153026
+ - Only emit NEW compartments for the new messages. Do not re-emit existing compartments from the existing state.
153027
+ - Write comprehensive, detailed compartments. Include file paths, function names, commit hashes, config keys, and values when they matter.
153028
+ - Do not list every changed file. Do not narrate tool calls. Do not preserve dead-end exploration beyond a brief clause when needed.
153029
+
153030
+ # Construction order (MANDATORY)
153031
+
153032
+ For each compartment, build in this exact order:
153033
+
153034
+ 1. Write the narrative summary first — what was done, why, and the outcome. This is 1–4 sentences covering the work unit completely.
153035
+ 2. Re-read your narrative. Ask: does the summary already convey all important decisions and constraints from this work unit?
153036
+ 3. If yes, the compartment is DONE with zero U: lines. Move on.
153037
+ 4. If no, identify the specific signal the narrative cannot capture. Add U: lines ONLY for those signals.
153038
+ 5. Before writing each U: line, run the CROSS-COMPARTMENT CHECK (see below).
153039
+
153040
+ Zero U: lines in a compartment is normal and expected. Most compartments should have 0–2 U: lines. Compartments with 3–5 are rare and must be justified by genuinely distinct durable signals.
153041
+
153042
+ # DROP rules (check these first — if any match, drop without exception)
153043
+
153044
+ - Questions in ANY form: "should I X?", "what about Y?", "do you think Z?", "isn't it better to A?", "why don't we B?", "any ideas?" — the resolved answer belongs in narrative only. If it feels important to keep the question, you are wrong: keep the answer in narrative.
153045
+ - Agreements and acknowledgments: "yes", "okay", "sure", "thanks", "go ahead", "looks good", "perfect", "I agree", "sounds good", "great".
153046
+ - Pure pacing and sequencing: "let's start", "continue", "let's do all", "now we can X", "let's commit", "first do A then B", "before that", "in the meantime".
153047
+ - Tactical observations: "I just noticed X", "we recently did Y", "I'm seeing Z right now", "this seems wrong".
153048
+ - Debugging status: "context is at 78%", "I'm restarting", "the last build failed".
153049
+ - Dogfooding/restart loops: "I restarted, can you check?", "okay we should have updated versions now", "let me try again".
153050
+ - Pasted error output or logs as U: line — capture the underlying problem in narrative, not the raw paste.
153051
+ - Examples and illustrations: "mine was when an agent wants to see X" — convert the underlying intent into a directive or drop.
153052
+ - Hype with embedded directive: ALL-CAPS pleas, "PLEASE PLEASE PLEASE just do X" — extract only the underlying directive into narrative; drop the hype.
153053
+ - Social signals, banter, emoji-only, enthusiasm.
153054
+ - Deferred ideas: "for later", "we can do X later", "another idea for the future".
153055
+ - Mid-process status: "running Y", "checking Z".
153056
+ - Superseded drafts once a later message gives the final decision.
153057
+ - Standing workflow rules ("always run lint before push") — these belong in WORKFLOW_RULES facts, not U: lines.
153058
+
153059
+ # Wording rule (default: verbatim)
153060
+
153061
+ By default, U: lines use the user's actual wording. The user's exact phrasing often carries negotiation context, emphasis, or technical specificity that paraphrase loses.
153062
+
153063
+ Paraphrase ONLY in these cases:
153064
+ - **Strip agreement prefixes**: "Yes X", "Okay X", "Sure X" → keep only the substantive part of X, in the user's original wording.
153065
+ - **Split compound directives**: If one message contains two distinct durable directives, split into two U: lines — each preserving the user's wording for its part.
153066
+ - **Drop conversational noise, keep core**: If a message wraps a directive in exploratory phrasing ("so I was thinking, maybe... but actually..."), drop the exploration and keep the core directive in the user's remaining words. Don't invent new phrasing.
153067
+
153068
+ NEVER:
153069
+ - Rewrite a clear user directive into a formal constraint statement. ("We need tool count at ~8" stays as-is; do NOT convert to "Tool count must be capped at 8.")
153070
+ - Synthesize a directive from multiple messages into one canonical statement. If the signal needs synthesis, it belongs in narrative, not a U: line.
153071
+ - Add technical specificity the user didn't state (file paths, function names, constant names). Canonical technical specificity belongs in narrative or facts, not in U: lines attributed to the user.
153072
+
153073
+ Good example:
153074
+ Original user message: "Yes let's do this. But we need to also make sure that we limit by message count as some sessions have quite a lot of messages."
153075
+ Correct U: line: "We need to also make sure that we limit by message count as some sessions have quite a lot of messages."
153076
+ (Stripped agreement prefix; kept the user's actual wording.)
153077
+
153078
+ Bad example (do not do this):
153079
+ Incorrect U: line: "Cap session history retrieval at a maximum message count to prevent memory issues on large sessions."
153080
+ (Rewrote the user directive into formal language and invented specificity.)
153081
+
153082
+ # KEEP rules (U: line survives only if ALL pass)
153083
+
153084
+ 1. DURABLE: The signal matters after the immediate turn.
153085
+ 2. SPECIFIC: Concrete goal, hard constraint, design decision, rejection, rationale, threshold, source-of-truth correction, or future-work directive.
153086
+ 3. OUTCOME-BACKED: This compartment's narrative clearly states what was done, decided, or changed because of the message.
153087
+ 4. NON-REDUNDANT: Not captured by another U: line (see CROSS-COMPARTMENT CHECK), by a fact, or by the narrative.
153088
+ 5. IRREPLACEABLE: The user's wording adds signal that narrative paraphrase cannot preserve. If the same information could appear as narrative without losing meaning, it should.
153089
+
153090
+ Categories of KEEP:
153091
+ - Hard gates, thresholds, config defaults, percentages, byte sizes with concrete values.
153092
+ - Accepted designs and explicit decisions.
153093
+ - Rejections and negative constraints: "X is wrong because Y", "we should NOT do Z".
153094
+ - Source-of-truth corrections: "follow the code, not the README".
153095
+ - Implementation pivots stated in future tense: "instead of X let's do Y", "switch to Z".
153096
+ - Durable rationale that explains WHY an approach was chosen.
153097
+ - Specific feature requirements stated as durable goals.
153098
+
153099
+ # PIVOT vs OBSERVATION test
153100
+
153101
+ A pivot is FUTURE-TENSE and changes the plan: "instead of X, let's do Y", "switch this to Z", "actually, let's not do A".
153102
+ An observation is PAST-TENSE or PRESENT-TENSE and reports state: "we recently did X", "I just noticed Y", "this is broken right now".
153103
+ Observations may frame narrative context but are NOT pivots and NOT durable. Drop them as U: lines.
153104
+
153105
+ # CROSS-COMPARTMENT CHECK (forward-looking)
153106
+
153107
+ Before writing ANY U: line in the current compartment:
153108
+ 1. Scan U: lines you have ALREADY written in previous compartments in this response.
153109
+ 2. If any prior U: line expresses the same intent, decision, constraint, or rationale — even in different words — do NOT write the new U: line.
153110
+ 3. Let the narrative in the current compartment carry the signal instead.
153111
+
153112
+ This is a forward operation: you only need to check what you already wrote, not revisit past compartments.
153113
+
153114
+ Examples of same-intent pairs to collapse:
153115
+ - "X shouldn't cause cache bust" + "X must not bust cache by itself" → keep only the first, in its original compartment.
153116
+ - "Let's use monorepo" + "Yes, monorepo is the right call" → keep only the first.
153117
+ - "Add logging" + "We need logs here too" → keep only the first.
153118
+
153119
+ Never keep two U: lines for the same underlying directive across compartments.
153120
+
153121
+ # Budget
153122
+
153123
+ - HARD LIMIT: 3–5 U: lines per compartment. 0–2 is typical.
153124
+ - If you have more than 5 candidate U: lines in one compartment, that is a signal to split into two compartments at a natural pivot, not to stuff more.
153125
+ - Every U: line must be immediately followed by 1–3 sentences describing the outcome, decision, or effect. Never stack two U: lines without intervening outcome text.
153126
+
153127
+ # Example: CORRECT preservation (narrative-first, verbatim U: line)
153128
+
153129
+ <compartment start="50" end="120" title="Built the auth layer">
153130
+ Implemented JWT auth with hard 60-minute exp claim and refresh-token rotation. Chose Bearer tokens over cookies after finding cookie-based auth broke the SPA flow. Added session_expiry config (read-only at runtime). Commits: a3f891, b22c4e.
153131
+ U: We need session expiry capped at 1 hour, no exceptions
153132
+ Hardcoded the 60-minute cap at the JWT-issuer layer so runtime overrides cannot extend it.
153133
+ </compartment>
153134
+
153135
+ Notice: only one U: line, kept verbatim from the user's actual message. The cookie-to-Bearer pivot is narrative because paraphrase captures the signal fully.
153136
+
153137
+ # Example: OVER-PRESERVATION (avoid)
153138
+
153139
+ <compartment start="200" end="350" title="Refactored data layer">
153140
+ U: Okay let's start on the data layer
153141
+ U: What about transactions?
153142
+ U: Yes that approach looks good
153143
+ U: Actually wait, maybe we need write-ahead logging
153144
+ U: I just noticed the previous commit broke a test
153145
+ U: Let's commit and ship it
153146
+ Refactored data layer with WAL mode and connection pooling.
153147
+ </compartment>
153148
+
153149
+ Problems: pacing, question, agreement, observation, pacing again. Only one message carries signal, and even that is narrative-capturable.
153150
+
153151
+ # CORRECT version of the above
153152
+
153153
+ <compartment start="200" end="350" title="Refactored data layer">
153154
+ Refactored data layer to use WAL mode plus connection pooling. Chose WAL over plain connections for concurrent read performance under sustained write load.
153155
+ </compartment>
153156
+
153157
+ Zero U: lines. The pivot to WAL is clear in narrative.
153158
+
153159
+ Fact rules:
153160
+ - Facts are editable state, not append-only notes. Rewrite, normalize, deduplicate, or drop existing facts whenever needed.
153161
+ - Before emitting any fact, check all existing facts in the same category for semantic duplicates. If two facts describe the same decision, constraint, or default with different wording, merge them into one canonical statement. Never emit two facts that could be answered by the same question.
153162
+ - When project memories are provided as read-only reference, drop any session fact that is already covered by a project memory. Project memories are the canonical cross-session source; session facts must not duplicate them.
153163
+ - Facts must be durable and actionable after the conversation ends.
153164
+ - A fact is either a stable invariant/default or a reusable operating rule. If it mainly explains what happened, it belongs in a compartment, not a fact.
153165
+ - Facts belong only in these categories when relevant: WORKFLOW_RULES, ARCHITECTURE_DECISIONS, CONSTRAINTS, CONFIG_DEFAULTS, KNOWN_ISSUES, ENVIRONMENT, NAMING, USER_PREFERENCES, USER_DIRECTIVES.
153166
+ - Keep only high-signal facts. Omit greetings, acknowledgements, temporary status, one-off sequencing, branch-local tactics, and task-local cleanup notes.
153167
+ - When a user message carries durable goals, constraints, preferences, or decision rationale, add a USER_DIRECTIVES fact when future agents should follow it after the session is compacted.
153168
+ - Do not turn task-local details into facts.
153169
+ - Do not keep stale facts. Rewrite or drop them even if the new input only implies they are obsolete.
153170
+ - Keep existing ARCHITECTURE_DECISIONS and CONSTRAINTS facts when they are still valid and uncontradicted; rewrite them into canonical form instead of dropping them.
153171
+ - Facts must be present tense and operational. Do not use chronology or provenance wording such as: initially, currently, remained, previously, later, then, was implemented, we changed, used to.
153172
+ - One fact bullet must contain exactly one rule/default/constraint/preference. If a candidate fact mixes history with guidance, keep the guidance and drop the history.
153173
+ - Durability test: a future agent should still act correctly on the fact next session, after merge/restart, without rereading the conversation.
153174
+ - Category guide:
153175
+ - WORKFLOW_RULES: standing repeatable process only. Prefer Do/When form: When <condition>, <action>. Do not store one-off branch strategy or task-specific sequencing unless it is standing policy.
153176
+ - ARCHITECTURE_DECISIONS: stable design choice. Use: <component> uses <choice> because <reason>.
153177
+ - CONSTRAINTS: hard must/must-not rule or invariant. Use: <thing> must/must not <action> because <reason>.
153178
+ - CONFIG_DEFAULTS: stable default only. Use: <key>=<value>.
153179
+ - KNOWN_ISSUES: unresolved recurring problem only. Do not store solved-issue stories.
153180
+ - ENVIRONMENT: stable setup fact that affects future work.
153181
+ - NAMING: canonical term choice. Use: Use <term>; avoid <term>.
153182
+ - USER_PREFERENCES: durable user preference. Prefer Do/When form.
153183
+ - USER_DIRECTIVES: durable user-stated goal, constraint, preference, or rationale. Keep the user's wording when it carries meaning, but narrow it to 1-3 sentences and remove filler.
153184
+ - Fact dedup examples:
153185
+ - These are DUPLICATES (merge into one): "Plugin config uses layered JSONC files" and "AFT plugin config uses layered JSONC files at ~/.config/opencode/aft.jsonc and <project>/.opencode/aft.jsonc, with project values deep-merging over user values." → keep the longer, more specific version only.
153186
+ - These are NOT duplicates (keep both): "AFT uses 1-based line numbers" and "AFT converts to LSP 0-based UTF-16 at the protocol boundary" → different aspects of the same system.
153187
+ - Fact rewrite examples:
153188
+ - Bad ARCHITECTURE_DECISIONS: The new tool-heavy \`ctx_reduce\` reminder was initially implemented as a hidden instruction appended to the latest user message in \`transform\`.
153189
+ - Good ARCHITECTURE_DECISIONS: \`ctx_reduce\` turn reminders are injected into the latest user message in \`transform\`.
153190
+ - Bad WORKFLOW_RULES: Current local workflow remained feat -> integrate -> build for code changes.
153191
+ - Good WORKFLOW_RULES (only if this is standing policy): For magic-context changes, commit on \`feat/magic-context\`, cherry-pick to \`integrate/athena-magic-context\`, run \`bun run build\` on integrate, then return to \`feat/magic-context\`.
153192
+
153193
+ Input notes:
153194
+ - [N] or [N-M] is a stable raw OpenCode message range.
153195
+ - U: means user.
153196
+ - A: means assistant.
153197
+ - TC: means tool call — a compact summary of what the agent did (e.g., "TC: Fix lint errors", "TC: read(src/index.ts)", "TC: grep(ctx_memory)"). TC lines appear when there is no text describing the action. Use them to understand what happened between text blocks, but do not copy them verbatim into compartments — incorporate their meaning into the narrative.
153198
+ - commits: ... on an assistant block lists commit hashes mentioned in that work unit; keep the relevant ones in the compartment summary when they matter.
153199
+
153200
+ Output valid XML only in this shape:
153201
+ <output>
153202
+ <compartments>
153203
+ <compartment start="FIRST" end="LAST" title="short title">
153204
+ U: Verbatim high-signal user message
153205
+ Summary text describing what was done and why.
153206
+ U: Another high-signal user message if applicable
153207
+ More summary text.
153208
+ </compartment>
153209
+ </compartments>
153210
+ <facts>
153211
+ <WORKFLOW_RULES>
153212
+ * Fact text
153213
+ </WORKFLOW_RULES>
153214
+ </facts>
153215
+ <meta>
153216
+ <messages_processed>FIRST-LAST</messages_processed>
153217
+ <unprocessed_from>INDEX</unprocessed_from>
153218
+ </meta>
153219
+ </output>
153220
+
153221
+ Omit empty fact categories. Compartments must be ordered, contiguous for the ranges they cover, and non-overlapping.`, HISTORIAN_EDITOR_SYSTEM_PROMPT = `You are an editor refining a historian draft. The draft was produced by a first-pass historian and may contain noise — low-signal U: lines, redundant quotes across compartments, and weak preservation decisions.
153222
+
153223
+ Your job is to clean the draft without changing its structure:
153224
+
153225
+ 1. DROP low-signal U: lines:
153226
+ - Questions in any form — resolved decision goes in narrative only.
153227
+ - Pacing/agreement: "let's go", "yes", "okay", "sounds good", "I agree".
153228
+ - Pasted error output, debugging status, mid-process observations.
153229
+ - Tactical micro-direction: "now look at X", "first check Y".
153230
+
153231
+ 2. DROP cross-compartment duplicates:
153232
+ - Scan U: lines across ALL compartments in the draft.
153233
+ - If two U: lines express the same intent/decision, keep only ONE — in the compartment where the outcome is actually described.
153234
+
153235
+ 3. STRIP agreement prefixes:
153236
+ - "Yes we should X" → keep only the directive content, or drop entirely if nothing substantive remains after "Yes".
153237
+
153238
+ 4. PREFER verbatim over paraphrase:
153239
+ - If the draft rephrased a user directive into formal constraint language, restore the user's wording if available.
153240
+ - Do not invent technical specificity (file paths, function names, constants) the user did not state.
153241
+
153242
+ 5. FOLD into narrative when possible:
153243
+ - If a U: line's signal is already captured in the surrounding narrative, drop the U: line.
153244
+ - Narrative should not need the U: line to be understood.
153245
+
153246
+ 6. KEEP as U: lines ONLY:
153247
+ - Hard constraints with concrete values (thresholds, byte sizes, timeouts).
153248
+ - Explicit rejections ("X is wrong because Y", "NOT Z").
153249
+ - Implementation pivots in future-tense ("instead of A, do B").
153250
+ - Source-of-truth corrections.
153251
+
153252
+ Do NOT change:
153253
+ - Compartment titles, ranges, or ordering.
153254
+ - Narrative summary text unless it directly references a U: line you dropped (in which case integrate the signal into the narrative).
153255
+ - Facts — leave the facts section untouched.
153256
+ - <meta> section — leave messages_processed and unprocessed_from exactly as the draft has them.
153257
+
153258
+ Output the cleaned version as valid XML matching the original structure. Preserve all XML tags, compartment ranges, meta, and facts.`, USER_OBSERVATIONS_APPENDIX = `
153259
+
153260
+ User observation rules (EXPERIMENTAL):
153261
+ - After outputting compartments and facts, also output a <user_observations> section.
153262
+ - User observations capture UNIVERSAL behavioral patterns about the human user — not project-specific or technical.
153263
+ - Good observations: communication preferences, review focus areas, expertise level, decision-making patterns, frustration triggers, working style.
153264
+ - Bad observations (DO NOT emit): project-specific preferences, framework choices, coding language preferences, one-off moods, task-local frustration.
153265
+ - Each observation must be a single concise sentence in present tense.
153266
+ - Only emit observations you have strong evidence for from the conversation. Do not speculate.
153267
+ - Emit 0-5 observations per run. Zero is fine if nothing stands out.
153268
+ - The output shape inside <output> gains an additional section:
153269
+ <user_observations>
153270
+ * User prefers terse communication and dislikes verbose explanations.
153271
+ * User is technically deep — understands cache invalidation, SQLite internals, and prompt engineering.
153272
+ </user_observations>
153273
+ If no observations, omit the <user_observations> section entirely.`;
153274
+ var init_compartment_prompt = __esm(() => {
153275
+ init_compartment_storage();
153276
+ });
153277
+
153278
+ // src/shared/opencode-config-dir.ts
153279
+ import { homedir as homedir4 } from "node:os";
153280
+ import { join as join8, resolve as resolve3 } from "node:path";
153281
+ function getCliConfigDir() {
153282
+ const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
153283
+ if (envConfigDir) {
153284
+ return resolve3(envConfigDir);
153285
+ }
153286
+ if (process.platform === "win32") {
153287
+ return join8(homedir4(), ".config", "opencode");
153288
+ }
153289
+ return join8(process.env.XDG_CONFIG_HOME || join8(homedir4(), ".config"), "opencode");
153290
+ }
153291
+ function getOpenCodeConfigDir(_options) {
153292
+ return getCliConfigDir();
153293
+ }
153294
+ function getOpenCodeConfigPaths(options) {
153295
+ const configDir = getOpenCodeConfigDir(options);
153296
+ return {
153297
+ configDir,
153298
+ configJson: join8(configDir, "opencode.json"),
153299
+ configJsonc: join8(configDir, "opencode.jsonc"),
153300
+ packageJson: join8(configDir, "package.json"),
153301
+ omoConfig: join8(configDir, "magic-context.jsonc")
153302
+ };
153303
+ }
153304
+ var init_opencode_config_dir = () => {};
153305
+
153306
+ // src/shared/conflict-detector.ts
153307
+ import { join as join9 } from "node:path";
153308
+ function detectConflicts(directory) {
153309
+ const conflicts = {
153310
+ compactionAuto: false,
153311
+ compactionPrune: false,
153312
+ dcpPlugin: false,
153313
+ omoPreemptiveCompaction: false,
153314
+ omoContextWindowMonitor: false,
153315
+ omoAnthropicRecovery: false
153316
+ };
153317
+ const reasons = [];
153318
+ const compactionResult = checkCompaction(directory);
153319
+ if (compactionResult.auto) {
153320
+ conflicts.compactionAuto = true;
153321
+ reasons.push("OpenCode auto-compaction is enabled (compaction.auto=true)");
153322
+ }
153323
+ if (compactionResult.prune) {
153324
+ conflicts.compactionPrune = true;
153325
+ reasons.push("OpenCode prune is enabled (compaction.prune=true)");
153326
+ }
153327
+ const dcpFound = checkDcpPlugin(directory);
153328
+ if (dcpFound) {
153329
+ conflicts.dcpPlugin = true;
153330
+ reasons.push("opencode-dcp plugin is installed — it conflicts with Magic Context's context management");
153331
+ }
153332
+ const omoResult = checkOmoHooks(directory);
153333
+ if (omoResult.preemptiveCompaction) {
153334
+ conflicts.omoPreemptiveCompaction = true;
153335
+ reasons.push("oh-my-opencode preemptive-compaction hook is active — it triggers compaction that conflicts with historian");
153336
+ }
153337
+ if (omoResult.contextWindowMonitor) {
153338
+ conflicts.omoContextWindowMonitor = true;
153339
+ reasons.push("oh-my-opencode context-window-monitor hook is active — it injects usage warnings that overlap with Magic Context nudges");
153340
+ }
153341
+ if (omoResult.anthropicRecovery) {
153342
+ conflicts.omoAnthropicRecovery = true;
153343
+ reasons.push("oh-my-opencode anthropic-context-window-limit-recovery hook is active — it triggers emergency compaction that bypasses historian");
153344
+ }
153345
+ return {
153346
+ hasConflict: reasons.length > 0,
153347
+ reasons,
153348
+ conflicts
153349
+ };
153350
+ }
153351
+ function checkCompaction(directory) {
153352
+ if (process.env.OPENCODE_DISABLE_AUTOCOMPACT) {
153353
+ return { auto: false, prune: false };
153354
+ }
153355
+ const projectResult = readProjectCompaction(directory);
153356
+ if (projectResult.resolved)
153357
+ return projectResult;
153358
+ const userResult = readUserCompaction();
153359
+ if (userResult.resolved)
153360
+ return userResult;
153361
+ return { auto: true, prune: false };
153362
+ }
153363
+ function readProjectCompaction(directory) {
153364
+ const dotOcJsonc = join9(directory, ".opencode", "opencode.jsonc");
153365
+ const dotOcJson = join9(directory, ".opencode", "opencode.json");
153366
+ const dotOcConfig = readJsoncFile(dotOcJsonc) ?? readJsoncFile(dotOcJson);
153367
+ if (dotOcConfig?.compaction) {
153368
+ const c = dotOcConfig.compaction;
153369
+ if (c.auto !== undefined || c.prune !== undefined) {
153370
+ return { auto: c.auto === true, prune: c.prune === true, resolved: true };
153371
+ }
153372
+ }
153373
+ const rootJsonc = join9(directory, "opencode.jsonc");
153374
+ const rootJson = join9(directory, "opencode.json");
153375
+ const rootConfig = readJsoncFile(rootJsonc) ?? readJsoncFile(rootJson);
153376
+ if (rootConfig?.compaction) {
153377
+ const c = rootConfig.compaction;
153378
+ if (c.auto !== undefined || c.prune !== undefined) {
153379
+ return { auto: c.auto === true, prune: c.prune === true, resolved: true };
153380
+ }
153381
+ }
153382
+ return { auto: false, prune: false, resolved: false };
153383
+ }
153384
+ function readUserCompaction() {
153385
+ try {
153386
+ const paths = getOpenCodeConfigPaths({ binary: "opencode" });
153387
+ const config2 = readJsoncFile(paths.configJsonc) ?? readJsoncFile(paths.configJson);
153388
+ if (config2?.compaction) {
153389
+ const c = config2.compaction;
153390
+ if (c.auto !== undefined || c.prune !== undefined) {
153391
+ return { auto: c.auto === true, prune: c.prune === true, resolved: true };
153392
+ }
153393
+ }
153394
+ } catch {}
153395
+ return { auto: false, prune: false, resolved: false };
153396
+ }
153397
+ function checkDcpPlugin(directory) {
153398
+ const plugins = collectPluginEntries(directory);
153399
+ return plugins.some((p) => matchesPackageName(p, DCP_PACKAGE_NAMES));
153400
+ }
153401
+ function matchesPackageName(entry, canonicalNames) {
153402
+ if (entry.startsWith("file:") || entry.startsWith("http:") || entry.startsWith("https:") || entry.startsWith("/") || entry.startsWith("./") || entry.startsWith("../")) {
153403
+ return false;
153404
+ }
153405
+ const lastAt = entry.lastIndexOf("@");
153406
+ const nameOnly = lastAt > 0 ? entry.slice(0, lastAt) : entry;
153407
+ return canonicalNames.has(nameOnly);
153408
+ }
153409
+ function extractPluginName(entry) {
153410
+ if (typeof entry === "string")
153411
+ return entry;
153412
+ if (Array.isArray(entry) && typeof entry[0] === "string")
153413
+ return entry[0];
153414
+ return null;
153415
+ }
153416
+ function collectPluginEntries(directory) {
153417
+ const plugins = [];
153418
+ const pushFrom = (entries) => {
153419
+ if (!entries)
153420
+ return;
153421
+ for (const entry of entries) {
153422
+ const name2 = extractPluginName(entry);
153423
+ if (name2)
153424
+ plugins.push(name2);
153425
+ }
153426
+ };
153427
+ for (const configPath of [
153428
+ join9(directory, ".opencode", "opencode.jsonc"),
153429
+ join9(directory, ".opencode", "opencode.json"),
153430
+ join9(directory, "opencode.jsonc"),
153431
+ join9(directory, "opencode.json")
153432
+ ]) {
153433
+ const config2 = readJsoncFile(configPath);
153434
+ pushFrom(config2?.plugin);
153435
+ }
153436
+ try {
153437
+ const paths = getOpenCodeConfigPaths({ binary: "opencode" });
153438
+ for (const configPath of [paths.configJsonc, paths.configJson]) {
153439
+ const config2 = readJsoncFile(configPath);
153440
+ pushFrom(config2?.plugin);
153441
+ }
153442
+ } catch {}
153443
+ return plugins;
153444
+ }
153445
+ function checkOmoHooks(directory) {
153446
+ const result = {
153447
+ preemptiveCompaction: false,
153448
+ contextWindowMonitor: false,
153449
+ anthropicRecovery: false
153450
+ };
153451
+ const plugins = collectPluginEntries(directory);
153452
+ const hasOmo = plugins.some((p) => matchesPackageName(p, OMO_PACKAGE_NAMES));
153453
+ if (!hasOmo)
153454
+ return result;
153455
+ const disabledHooks = readOmoDisabledHooks(directory);
153456
+ result.preemptiveCompaction = !disabledHooks.has("preemptive-compaction");
153457
+ result.contextWindowMonitor = !disabledHooks.has("context-window-monitor");
153458
+ result.anthropicRecovery = !disabledHooks.has("anthropic-context-window-limit-recovery");
153459
+ return result;
153460
+ }
153461
+ function readOmoDisabledHooks(directory) {
153462
+ const disabled = new Set;
153463
+ const configNames = [
153464
+ "oh-my-opencode.jsonc",
153465
+ "oh-my-opencode.json",
153466
+ "oh-my-openagent.jsonc",
153467
+ "oh-my-openagent.json"
153468
+ ];
153469
+ try {
153470
+ const paths = getOpenCodeConfigPaths({ binary: "opencode" });
153471
+ for (const name2 of configNames) {
153472
+ const configPath = join9(paths.configDir, name2);
153473
+ const config2 = readJsoncFile(configPath);
153474
+ if (config2?.disabled_hooks) {
153475
+ for (const hook of config2.disabled_hooks) {
153476
+ disabled.add(hook);
153477
+ }
153478
+ }
153479
+ }
153480
+ } catch {}
153481
+ for (const name2 of configNames) {
153482
+ const config2 = readJsoncFile(join9(directory, name2));
153483
+ if (config2?.disabled_hooks) {
153484
+ for (const hook of config2.disabled_hooks) {
153485
+ disabled.add(hook);
153486
+ }
153487
+ }
153488
+ }
153489
+ return disabled;
153490
+ }
153491
+ function formatConflictShort(result) {
153492
+ if (!result.hasConflict)
153493
+ return "";
153494
+ const lines = [
153495
+ "⚠️ Magic Context is disabled due to conflicting configuration:",
153496
+ "",
153497
+ ...result.reasons.map((r) => `• ${r}`),
153498
+ "",
153499
+ "Fix: run `npx @wolfx/opencode-magic-context@latest doctor`"
153500
+ ];
153501
+ return lines.join(`
153502
+ `);
153503
+ }
153504
+ var DCP_PACKAGE_NAMES, OMO_PACKAGE_NAMES;
153505
+ var init_conflict_detector = __esm(() => {
153506
+ init_jsonc_parser();
153507
+ init_opencode_config_dir();
153508
+ DCP_PACKAGE_NAMES = new Set(["@tarquinen/opencode-dcp"]);
153509
+ OMO_PACKAGE_NAMES = new Set(["oh-my-opencode", "oh-my-openagent"]);
153510
+ });
153511
+
153512
+ // src/plugin/conflict-warning-hook.ts
153513
+ var exports_conflict_warning_hook = {};
153514
+ __export(exports_conflict_warning_hook, {
153515
+ sendTuiSetupNotification: () => sendTuiSetupNotification,
153516
+ sendStartupAnnouncement: () => sendStartupAnnouncement,
153517
+ sendConflictWarning: () => sendConflictWarning,
153518
+ cleanupConflictWarnings: () => cleanupConflictWarnings
153519
+ });
153520
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
153521
+ import { homedir as homedir5, platform } from "node:os";
153522
+ import { join as join10 } from "node:path";
153523
+ function getDesktopStatePath() {
153524
+ const os2 = platform();
153525
+ const home = homedir5();
153526
+ if (os2 === "darwin") {
153527
+ return join10(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
153528
+ }
153529
+ if (os2 === "linux") {
153530
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join10(home, ".config");
153531
+ return join10(xdgConfig, "ai.opencode.desktop", "opencode.global.dat");
153532
+ }
153533
+ if (os2 === "win32") {
153534
+ const appData = process.env.APPDATA || join10(home, "AppData", "Roaming");
153535
+ return join10(appData, "ai.opencode.desktop", "opencode.global.dat");
153536
+ }
153537
+ return null;
153538
+ }
153539
+ function readDesktopState(directory) {
153540
+ const statePath = getDesktopStatePath();
153541
+ if (!statePath || !existsSync8(statePath)) {
153542
+ log(`[magic-context] conflict-warning: Desktop state file not found at ${statePath}`);
153543
+ return { sessionId: null, sidecarUrl: null };
153544
+ }
153545
+ try {
153546
+ const raw = readFileSync5(statePath, "utf-8");
153547
+ const state = JSON.parse(raw);
153548
+ let sidecarUrl = null;
153549
+ const serverStr = state.server;
153550
+ if (typeof serverStr === "string") {
153551
+ try {
153552
+ const serverState = JSON.parse(serverStr);
153553
+ if (typeof serverState.currentSidecarUrl === "string") {
153554
+ sidecarUrl = serverState.currentSidecarUrl;
153555
+ }
153556
+ } catch {}
153557
+ }
153558
+ let sessionId = null;
153559
+ const layoutPage = state["layout.page"];
153560
+ if (typeof layoutPage === "string") {
153561
+ const parsed = JSON.parse(layoutPage);
153562
+ const lastProjectSession = parsed.lastProjectSession;
153563
+ if (lastProjectSession) {
153564
+ const entry = lastProjectSession[directory];
153565
+ sessionId = entry?.id ?? null;
153566
+ }
153567
+ }
153568
+ return { sessionId, sidecarUrl };
153569
+ } catch (error51) {
153570
+ log(`[magic-context] conflict-warning: failed to read Desktop state: ${error51 instanceof Error ? error51.message : String(error51)}`);
153571
+ return { sessionId: null, sidecarUrl: null };
153572
+ }
153573
+ }
153574
+ function getDesktopState(directory) {
153575
+ let cached2 = cachedDesktopStateByDir.get(directory);
153576
+ if (!cached2) {
153577
+ cached2 = readDesktopState(directory);
153578
+ cachedDesktopStateByDir.set(directory, cached2);
153579
+ }
153580
+ return cached2;
153581
+ }
153582
+ async function deleteMessage(serverUrl, sessionId, messageId) {
153583
+ const auth = getServerAuth();
153584
+ const url2 = `${serverUrl}/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`;
153585
+ try {
153586
+ const response = await fetch(url2, {
153587
+ method: "DELETE",
153588
+ headers: auth ? { Authorization: auth } : {},
153589
+ signal: AbortSignal.timeout(1e4)
153590
+ });
153591
+ if (!response.ok) {
153592
+ log(`[magic-context] conflict-warning: DELETE failed status=${response.status} url=${url2}`);
153593
+ return false;
153594
+ }
153595
+ return true;
153596
+ } catch (error51) {
153597
+ log(`[magic-context] conflict-warning: DELETE error (url=${serverUrl}): ${error51 instanceof Error ? error51.message : String(error51)}`);
153598
+ return false;
153599
+ }
153600
+ }
153601
+ function getServerAuth() {
153602
+ const password = process.env.OPENCODE_SERVER_PASSWORD;
153603
+ if (!password)
153604
+ return;
153605
+ const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
153606
+ return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
153607
+ }
153608
+ async function getSessionMessages(client, sessionId) {
153609
+ try {
153610
+ const c = client;
153611
+ if (typeof c.session?.messages === "function") {
153612
+ const result = await c.session.messages({
153613
+ path: { id: sessionId },
153614
+ query: { limit: 50 }
153615
+ });
153616
+ return result?.data ?? [];
153617
+ }
153618
+ } catch (error51) {
153619
+ log(`[magic-context] conflict-warning: failed to read messages: ${error51 instanceof Error ? error51.message : String(error51)}`);
153620
+ }
153621
+ return [];
153622
+ }
153623
+ async function sendConflictWarning(client, directory, conflictResult) {
153624
+ const { sessionId } = getDesktopState(directory);
153625
+ if (!sessionId) {
153626
+ log("[magic-context] conflict-warning: could not find active session for Desktop warning");
153627
+ return;
153628
+ }
153629
+ const warningText = formatConflictShort(conflictResult);
153630
+ log(`[magic-context] sending conflict warning to session ${sessionId}: ${conflictResult.reasons.join(", ")}`);
153631
+ try {
153632
+ const c = client;
153633
+ const promptInput = {
153634
+ path: { id: sessionId },
153635
+ body: {
153636
+ noReply: true,
153637
+ parts: [
153638
+ {
153639
+ type: "text",
153640
+ text: warningText,
153641
+ ignored: true
153642
+ }
153643
+ ]
153644
+ }
153645
+ };
153646
+ if (typeof c.session?.prompt === "function") {
153647
+ await Promise.resolve(c.session.prompt(promptInput));
153648
+ } else if (typeof c.session?.promptAsync === "function") {
153649
+ await c.session.promptAsync(promptInput);
153650
+ } else {
153651
+ log("[magic-context] conflict-warning: session prompt API unavailable");
153652
+ }
153653
+ } catch (error51) {
153654
+ log(`[magic-context] conflict-warning: failed to send: ${error51 instanceof Error ? error51.message : String(error51)}`);
153655
+ }
153656
+ }
153657
+ async function cleanupConflictWarnings(client, directory, serverUrl) {
153658
+ const { sessionId } = getDesktopState(directory);
153659
+ if (!sessionId) {
153660
+ log("[magic-context] cleanup: no active Desktop session found");
153661
+ return;
153662
+ }
153663
+ const messages = await getSessionMessages(client, sessionId);
153664
+ if (messages.length === 0)
153665
+ return;
153666
+ const warningMessageIds = [];
153667
+ for (let i = messages.length - 1;i >= 0; i--) {
153668
+ const msg = messages[i];
153669
+ const msgId = msg.info?.id;
153670
+ const msgRole = msg.info?.role;
153671
+ if (!msgId || msgRole !== "user")
153672
+ break;
153673
+ const parts = msg.parts ?? [];
153674
+ const isWarning = parts.length > 0 && parts.every((p) => p.ignored === true && p.type === "text" && typeof p.text === "string" && p.text.startsWith(CONFLICT_WARNING_MARKER));
153675
+ if (isWarning) {
153676
+ warningMessageIds.push(msgId);
153677
+ } else {
153678
+ break;
153679
+ }
153680
+ }
153681
+ if (warningMessageIds.length === 0) {
153682
+ await cleanupEnabledMessages(messages, serverUrl, sessionId);
153683
+ return;
153684
+ }
153685
+ if (!serverUrl) {
153686
+ log("[magic-context] cleanup: no serverUrl provided, cannot delete messages");
153687
+ return;
153688
+ }
153689
+ log(`[magic-context] cleaning up ${warningMessageIds.length} conflict warning message(s) from session ${sessionId}`);
153690
+ for (const messageId of warningMessageIds) {
153691
+ const ok = await deleteMessage(serverUrl, sessionId, messageId);
153692
+ if (ok) {
153693
+ log(`[magic-context] deleted conflict warning message ${messageId}`);
153694
+ }
153695
+ }
153696
+ const enabledText = `${ENABLED_MARKER}. Enjoy! ✨`;
153697
+ try {
153698
+ const c = client;
153699
+ const promptInput = {
153700
+ path: { id: sessionId },
153701
+ body: {
153702
+ noReply: true,
153703
+ parts: [{ type: "text", text: enabledText, ignored: true }]
153704
+ }
153705
+ };
153706
+ if (typeof c.session?.prompt === "function") {
153707
+ await Promise.resolve(c.session.prompt(promptInput));
153708
+ } else if (typeof c.session?.promptAsync === "function") {
153709
+ await c.session.promptAsync(promptInput);
153710
+ }
153711
+ } catch {}
153712
+ setTimeout(async () => {
153713
+ try {
153714
+ const freshMessages = await getSessionMessages(client, sessionId);
153715
+ for (let i = freshMessages.length - 1;i >= 0; i--) {
153716
+ const msg = freshMessages[i];
153717
+ const msgId = msg.info?.id;
153718
+ const msgRole = msg.info?.role;
153719
+ if (!msgId || msgRole !== "user")
153720
+ break;
153721
+ const parts = msg.parts ?? [];
153722
+ const isEnabled = parts.length > 0 && parts.every((p) => p.ignored === true && p.type === "text" && typeof p.text === "string" && p.text.startsWith(ENABLED_MARKER));
153723
+ if (isEnabled) {
153724
+ await deleteMessage(serverUrl, sessionId, msgId);
153725
+ } else {
153726
+ break;
153727
+ }
153728
+ }
153729
+ } catch {}
153730
+ }, 1000);
153731
+ }
153732
+ async function cleanupEnabledMessages(messages, serverUrl, sessionId) {
153733
+ if (!serverUrl)
153734
+ return;
153735
+ for (let i = messages.length - 1;i >= 0; i--) {
153736
+ const msg = messages[i];
153737
+ const msgId = msg.info?.id;
153738
+ const msgRole = msg.info?.role;
153739
+ if (!msgId || msgRole !== "user")
153740
+ break;
153741
+ const parts = msg.parts ?? [];
153742
+ const isEnabled = parts.length > 0 && parts.every((p) => p.ignored === true && p.type === "text" && typeof p.text === "string" && p.text.startsWith(ENABLED_MARKER));
153743
+ if (isEnabled) {
153744
+ await deleteMessage(serverUrl, sessionId, msgId);
153745
+ } else {
153746
+ break;
153747
+ }
153748
+ }
153749
+ }
153750
+ async function sendTuiSetupNotification(client, directory, serverUrl) {
153751
+ const { sessionId } = getDesktopState(directory);
153752
+ if (!sessionId)
153753
+ return;
153754
+ const text = [
153755
+ `${TUI_SETUP_MARKER}`,
153756
+ "",
153757
+ "Magic Context added its TUI plugin to your tui.json.",
153758
+ "Restart OpenCode to see the sidebar with live context breakdown,",
153759
+ "token usage, historian status, memory counts, and more."
153760
+ ].join(`
153761
+ `);
153762
+ try {
153763
+ const c = client;
153764
+ const promptInput = {
153765
+ path: { id: sessionId },
153766
+ body: {
153767
+ noReply: true,
153768
+ parts: [{ type: "text", text, ignored: true }]
153769
+ }
153770
+ };
153771
+ if (typeof c.session?.prompt === "function") {
153772
+ await Promise.resolve(c.session.prompt(promptInput));
153773
+ } else if (typeof c.session?.promptAsync === "function") {
153774
+ await c.session.promptAsync(promptInput);
153775
+ }
153776
+ } catch {
153777
+ return;
153778
+ }
153779
+ if (!serverUrl)
153780
+ return;
153781
+ setTimeout(async () => {
153782
+ try {
153783
+ const msgs = await getSessionMessages(client, sessionId);
153784
+ for (let i = msgs.length - 1;i >= 0; i--) {
153785
+ const msg = msgs[i];
153786
+ const msgId = msg.info?.id;
153787
+ if (!msgId || msg.info?.role !== "user")
153788
+ break;
153789
+ const parts = msg.parts ?? [];
153790
+ const isTuiSetup = parts.length > 0 && parts.every((p) => p.ignored === true && p.type === "text" && typeof p.text === "string" && p.text.startsWith(TUI_SETUP_MARKER));
153791
+ if (isTuiSetup) {
153792
+ await deleteMessage(serverUrl, sessionId, msgId);
153793
+ } else {
153794
+ break;
153795
+ }
153796
+ }
153797
+ } catch {}
153798
+ }, 1000);
153799
+ }
153800
+ async function sendStartupAnnouncement(client, directory, version2, features, footer, markSeen) {
153801
+ if (!version2 || features.length === 0)
153802
+ return;
153803
+ const { sessionId } = getDesktopState(directory);
153804
+ if (!sessionId) {
153805
+ return;
153806
+ }
153807
+ const bullets = features.map((line) => ` • ${line}`).join(`
153808
+ `);
153809
+ const sections = [`${ANNOUNCEMENT_MARKER} v${version2}:`, "", bullets];
153810
+ if (footer && footer.trim().length > 0) {
153811
+ sections.push("", footer);
153812
+ }
153813
+ const text = sections.join(`
153814
+ `);
153815
+ log(`[magic-context] sending startup announcement for v${version2} to session ${sessionId}`);
153816
+ try {
153817
+ const c = client;
153818
+ const promptInput = {
153819
+ path: { id: sessionId },
153820
+ body: {
153821
+ noReply: true,
153822
+ parts: [{ type: "text", text, ignored: true }]
153823
+ }
153824
+ };
153825
+ if (typeof c.session?.prompt === "function") {
153826
+ await Promise.resolve(c.session.prompt(promptInput));
153827
+ } else if (typeof c.session?.promptAsync === "function") {
153828
+ await c.session.promptAsync(promptInput);
153829
+ } else {
153830
+ log("[magic-context] announcement: session prompt API unavailable");
153831
+ return;
153832
+ }
153833
+ } catch (error51) {
153834
+ log(`[magic-context] announcement: failed to send: ${error51 instanceof Error ? error51.message : String(error51)}`);
153835
+ return;
153836
+ }
153837
+ markSeen(version2);
153838
+ }
153839
+ var CONFLICT_WARNING_MARKER = "⚠️ Magic Context is disabled due to conflicting configuration:", ENABLED_MARKER = "✨ Magic Context is now enabled", TUI_SETUP_MARKER = "\uD83D\uDCCA Magic Context sidebar configured", ANNOUNCEMENT_MARKER = "✨ Magic Context — what's new in", cachedDesktopStateByDir;
153840
+ var init_conflict_warning_hook = __esm(() => {
153841
+ init_conflict_detector();
153842
+ init_logger();
153843
+ cachedDesktopStateByDir = new Map;
153844
+ });
153845
+
152942
153846
  // ../../node_modules/.bun/esprima@4.0.1/node_modules/esprima/dist/esprima.js
152943
153847
  var require_esprima = __commonJS((exports, module) => {
152944
153848
  (function webpackUniversalModuleDefinition(root, factory) {
@@ -160546,10 +161450,10 @@ var require_stringify = __commonJS((exports, module) => {
160546
161450
  replacer = null;
160547
161451
  indent = EMPTY;
160548
161452
  };
160549
- var join8 = (one, two, gap) => one ? two ? one + two.trim() + LF + gap : one.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(one, gap)), gap) : two ? two.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(two, gap)), gap) : EMPTY;
161453
+ var join11 = (one, two, gap) => one ? two ? one + two.trim() + LF + gap : one.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(one, gap)), gap) : two ? two.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(two, gap)), gap) : EMPTY;
160550
161454
  var join_content = (inside, value, gap) => {
160551
161455
  const comment = process_comments(value, PREFIX_BEFORE, gap + indent, true);
160552
- return join8(comment, inside, gap);
161456
+ return join11(comment, inside, gap);
160553
161457
  };
160554
161458
  var stringify_string = (holder, key, value) => {
160555
161459
  const raw = get_raw_string_literal(holder, key);
@@ -160571,13 +161475,13 @@ var require_stringify = __commonJS((exports, module) => {
160571
161475
  if (i !== 0) {
160572
161476
  inside += COMMA;
160573
161477
  }
160574
- const before = join8(after_comma, process_comments(value, BEFORE(i), deeper_gap), deeper_gap);
161478
+ const before = join11(after_comma, process_comments(value, BEFORE(i), deeper_gap), deeper_gap);
160575
161479
  inside += before || LF + deeper_gap;
160576
161480
  inside += stringify(i, value, deeper_gap) || STR_NULL;
160577
161481
  inside += process_comments(value, AFTER_VALUE(i), deeper_gap);
160578
161482
  after_comma = process_comments(value, AFTER(i), deeper_gap);
160579
161483
  }
160580
- inside += join8(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
161484
+ inside += join11(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
160581
161485
  return BRACKET_OPEN + join_content(inside, value, gap) + BRACKET_CLOSE;
160582
161486
  };
160583
161487
  var object_stringify = (value, gap) => {
@@ -160598,13 +161502,13 @@ var require_stringify = __commonJS((exports, module) => {
160598
161502
  inside += COMMA;
160599
161503
  }
160600
161504
  first = false;
160601
- const before = join8(after_comma, process_comments(value, BEFORE(key), deeper_gap), deeper_gap);
161505
+ const before = join11(after_comma, process_comments(value, BEFORE(key), deeper_gap), deeper_gap);
160602
161506
  inside += before || LF + deeper_gap;
160603
161507
  inside += quote(key) + process_comments(value, AFTER_PROP(key), deeper_gap) + COLON + process_comments(value, AFTER_COLON(key), deeper_gap) + SPACE + sv + process_comments(value, AFTER_VALUE(key), deeper_gap);
160604
161508
  after_comma = process_comments(value, AFTER(key), deeper_gap);
160605
161509
  };
160606
161510
  keys.forEach(iteratee);
160607
- inside += join8(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
161511
+ inside += join11(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
160608
161512
  return CURLY_BRACKET_OPEN + join_content(inside, value, gap) + CURLY_BRACKET_CLOSE;
160609
161513
  };
160610
161514
  function stringify(key, holder, gap) {
@@ -160696,907 +161600,6 @@ var require_src2 = __commonJS((exports, module) => {
160696
161600
  };
160697
161601
  });
160698
161602
 
160699
- // src/hooks/magic-context/compartment-prompt.ts
160700
- function buildHistorianEditorPrompt(draft) {
160701
- return [
160702
- "This is a historian draft. Clean it up following the rules in your system prompt.",
160703
- "",
160704
- "<draft>",
160705
- draft,
160706
- "</draft>",
160707
- "",
160708
- "Return the cleaned draft as valid XML matching the original structure."
160709
- ].join(`
160710
- `);
160711
- }
160712
- function buildCompressorPrompt(compartments, currentTokens, targetTokens, outputDepth, outputCount) {
160713
- const lines = [];
160714
- const densityLabel = outputDepth === 1 ? "MERGE ONLY" : outputDepth === 2 ? "LITE TIGHTEN" : outputDepth === 3 ? "FULL CONDENSE" : "ULTRA TELEGRAPH";
160715
- const resolvedOutputCount = outputCount ?? Math.max(1, Math.ceil(compartments.length / 2));
160716
- lines.push(`Density target: LEVEL ${outputDepth} (${densityLabel}). See system prompt for level rules.`);
160717
- lines.push(`Input: ${compartments.length} compartments, ~${currentTokens} tokens. Target output: exactly ${resolvedOutputCount} compartments, ~${targetTokens} tokens total.`);
160718
- lines.push("");
160719
- if (outputDepth === 1) {
160720
- lines.push("Merge only. Preserve narrative and all U: lines. Drop only genuine duplicate sentences spanning compartments.");
160721
- } else if (outputDepth === 2) {
160722
- lines.push("Merge + drop filler words and hedging. Keep grammar, keep U: lines verbatim.");
160723
- } else if (outputDepth === 3) {
160724
- lines.push("Merge into single-paragraph compartments. Drop articles and weak auxiliaries. Keep only IRREPLACEABLE U: lines.");
160725
- } else {
160726
- lines.push("Merge into telegraphic fragments with symbol connectives (→ + // |). U: lines only if truly irreplaceable.");
160727
- }
160728
- lines.push("");
160729
- lines.push("Preserved literally at all levels: commit hashes, file paths, URLs, code spans.");
160730
- lines.push("");
160731
- for (const c of compartments) {
160732
- lines.push(`<compartment start="${c.startMessage}" end="${c.endMessage}" title="${escapeXmlAttr(c.title)}">`);
160733
- lines.push(escapeXmlContent(c.content));
160734
- lines.push("</compartment>");
160735
- lines.push("");
160736
- }
160737
- lines.push("Return merged compartments as XML.");
160738
- return lines.join(`
160739
- `);
160740
- }
160741
- function buildCompartmentAgentPrompt(existingState, inputSource, options) {
160742
- const existingStateBlock = options?.stateFilePath ? `Read the existing session state from this file before proceeding:
160743
- ${options.stateFilePath}
160744
-
160745
- The file contains the full XML existing state (compartments, facts, memories). Read it first, then process the new messages below.` : existingState;
160746
- return [
160747
- "Existing state (read-only context for continuity and fact normalization — do NOT re-emit these compartments):",
160748
- existingStateBlock,
160749
- "",
160750
- "<new_messages>",
160751
- inputSource,
160752
- "</new_messages>",
160753
- "",
160754
- "Instructions:",
160755
- "- Return ONLY new compartments for the messages inside <new_messages>, plus the full normalized fact list.",
160756
- "- Use the exact absolute raw ordinals from the input ranges for every compartment start/end and for <unprocessed_from>.",
160757
- "- Rewrite every fact into terse, present-tense operational form. Merge semantic duplicates within each category.",
160758
- "- Drop any session fact already covered by a project memory in the existing state.",
160759
- "- Do not preserve prior narrative wording verbatim; if a fact is already canonical and still correct, keep or lightly normalize it.",
160760
- "- Drop obsolete or task-local facts.",
160761
- ...options?.userMemoriesEnabled ? [
160762
- "- Also emit <user_observations> with universal behavioral observations about the user (see system prompt for rules)."
160763
- ] : []
160764
- ].join(`
160765
- `);
160766
- }
160767
- var COMPARTMENT_AGENT_SYSTEM_PROMPT = `You condense long AI coding sessions into two outputs:
160768
-
160769
- 1. compartments: completed logical work units
160770
- 2. facts: persistent cross-cutting information for future work
160771
-
160772
- Compartment rules:
160773
- - A compartment is one contiguous completed work unit: investigation, fix, refactor, docs update, feature, or decision.
160774
- - Start a new compartment only when the work clearly pivots to a different objective.
160775
- - If one broad effort contains multiple completed sub-pivots with distinct outcomes, prefer multiple smaller compartments over one umbrella compartment with many U: lines.
160776
- - Do not create compartments for magic-context commands or tool-only noise.
160777
- - If the input ends mid-topic, leave it out and report its first message index in <unprocessed_from>.
160778
- - All compartment start/end ordinals and <unprocessed_from> must use the absolute raw message numbers shown in the input. Never renumber relative to this chunk.
160779
- - Every displayed raw message ordinal in the input MUST appear in exactly one compartment. Gaps between compartments are invalid. When a displayed block is pure tool noise (e.g. a long "TC: ..." run with no narrative text), do NOT skip it — extend the preceding compartment's \`end\` to absorb the range, or include it inside the current compartment if the block falls within an ongoing work unit. Never create a dedicated compartment just to cover a tool-only run.
160780
- - Only emit NEW compartments for the new messages. Do not re-emit existing compartments from the existing state.
160781
- - Write comprehensive, detailed compartments. Include file paths, function names, commit hashes, config keys, and values when they matter.
160782
- - Do not list every changed file. Do not narrate tool calls. Do not preserve dead-end exploration beyond a brief clause when needed.
160783
-
160784
- # Construction order (MANDATORY)
160785
-
160786
- For each compartment, build in this exact order:
160787
-
160788
- 1. Write the narrative summary first — what was done, why, and the outcome. This is 1–4 sentences covering the work unit completely.
160789
- 2. Re-read your narrative. Ask: does the summary already convey all important decisions and constraints from this work unit?
160790
- 3. If yes, the compartment is DONE with zero U: lines. Move on.
160791
- 4. If no, identify the specific signal the narrative cannot capture. Add U: lines ONLY for those signals.
160792
- 5. Before writing each U: line, run the CROSS-COMPARTMENT CHECK (see below).
160793
-
160794
- Zero U: lines in a compartment is normal and expected. Most compartments should have 0–2 U: lines. Compartments with 3–5 are rare and must be justified by genuinely distinct durable signals.
160795
-
160796
- # DROP rules (check these first — if any match, drop without exception)
160797
-
160798
- - Questions in ANY form: "should I X?", "what about Y?", "do you think Z?", "isn't it better to A?", "why don't we B?", "any ideas?" — the resolved answer belongs in narrative only. If it feels important to keep the question, you are wrong: keep the answer in narrative.
160799
- - Agreements and acknowledgments: "yes", "okay", "sure", "thanks", "go ahead", "looks good", "perfect", "I agree", "sounds good", "great".
160800
- - Pure pacing and sequencing: "let's start", "continue", "let's do all", "now we can X", "let's commit", "first do A then B", "before that", "in the meantime".
160801
- - Tactical observations: "I just noticed X", "we recently did Y", "I'm seeing Z right now", "this seems wrong".
160802
- - Debugging status: "context is at 78%", "I'm restarting", "the last build failed".
160803
- - Dogfooding/restart loops: "I restarted, can you check?", "okay we should have updated versions now", "let me try again".
160804
- - Pasted error output or logs as U: line — capture the underlying problem in narrative, not the raw paste.
160805
- - Examples and illustrations: "mine was when an agent wants to see X" — convert the underlying intent into a directive or drop.
160806
- - Hype with embedded directive: ALL-CAPS pleas, "PLEASE PLEASE PLEASE just do X" — extract only the underlying directive into narrative; drop the hype.
160807
- - Social signals, banter, emoji-only, enthusiasm.
160808
- - Deferred ideas: "for later", "we can do X later", "another idea for the future".
160809
- - Mid-process status: "running Y", "checking Z".
160810
- - Superseded drafts once a later message gives the final decision.
160811
- - Standing workflow rules ("always run lint before push") — these belong in WORKFLOW_RULES facts, not U: lines.
160812
-
160813
- # Wording rule (default: verbatim)
160814
-
160815
- By default, U: lines use the user's actual wording. The user's exact phrasing often carries negotiation context, emphasis, or technical specificity that paraphrase loses.
160816
-
160817
- Paraphrase ONLY in these cases:
160818
- - **Strip agreement prefixes**: "Yes X", "Okay X", "Sure X" → keep only the substantive part of X, in the user's original wording.
160819
- - **Split compound directives**: If one message contains two distinct durable directives, split into two U: lines — each preserving the user's wording for its part.
160820
- - **Drop conversational noise, keep core**: If a message wraps a directive in exploratory phrasing ("so I was thinking, maybe... but actually..."), drop the exploration and keep the core directive in the user's remaining words. Don't invent new phrasing.
160821
-
160822
- NEVER:
160823
- - Rewrite a clear user directive into a formal constraint statement. ("We need tool count at ~8" stays as-is; do NOT convert to "Tool count must be capped at 8.")
160824
- - Synthesize a directive from multiple messages into one canonical statement. If the signal needs synthesis, it belongs in narrative, not a U: line.
160825
- - Add technical specificity the user didn't state (file paths, function names, constant names). Canonical technical specificity belongs in narrative or facts, not in U: lines attributed to the user.
160826
-
160827
- Good example:
160828
- Original user message: "Yes let's do this. But we need to also make sure that we limit by message count as some sessions have quite a lot of messages."
160829
- Correct U: line: "We need to also make sure that we limit by message count as some sessions have quite a lot of messages."
160830
- (Stripped agreement prefix; kept the user's actual wording.)
160831
-
160832
- Bad example (do not do this):
160833
- Incorrect U: line: "Cap session history retrieval at a maximum message count to prevent memory issues on large sessions."
160834
- (Rewrote the user directive into formal language and invented specificity.)
160835
-
160836
- # KEEP rules (U: line survives only if ALL pass)
160837
-
160838
- 1. DURABLE: The signal matters after the immediate turn.
160839
- 2. SPECIFIC: Concrete goal, hard constraint, design decision, rejection, rationale, threshold, source-of-truth correction, or future-work directive.
160840
- 3. OUTCOME-BACKED: This compartment's narrative clearly states what was done, decided, or changed because of the message.
160841
- 4. NON-REDUNDANT: Not captured by another U: line (see CROSS-COMPARTMENT CHECK), by a fact, or by the narrative.
160842
- 5. IRREPLACEABLE: The user's wording adds signal that narrative paraphrase cannot preserve. If the same information could appear as narrative without losing meaning, it should.
160843
-
160844
- Categories of KEEP:
160845
- - Hard gates, thresholds, config defaults, percentages, byte sizes with concrete values.
160846
- - Accepted designs and explicit decisions.
160847
- - Rejections and negative constraints: "X is wrong because Y", "we should NOT do Z".
160848
- - Source-of-truth corrections: "follow the code, not the README".
160849
- - Implementation pivots stated in future tense: "instead of X let's do Y", "switch to Z".
160850
- - Durable rationale that explains WHY an approach was chosen.
160851
- - Specific feature requirements stated as durable goals.
160852
-
160853
- # PIVOT vs OBSERVATION test
160854
-
160855
- A pivot is FUTURE-TENSE and changes the plan: "instead of X, let's do Y", "switch this to Z", "actually, let's not do A".
160856
- An observation is PAST-TENSE or PRESENT-TENSE and reports state: "we recently did X", "I just noticed Y", "this is broken right now".
160857
- Observations may frame narrative context but are NOT pivots and NOT durable. Drop them as U: lines.
160858
-
160859
- # CROSS-COMPARTMENT CHECK (forward-looking)
160860
-
160861
- Before writing ANY U: line in the current compartment:
160862
- 1. Scan U: lines you have ALREADY written in previous compartments in this response.
160863
- 2. If any prior U: line expresses the same intent, decision, constraint, or rationale — even in different words — do NOT write the new U: line.
160864
- 3. Let the narrative in the current compartment carry the signal instead.
160865
-
160866
- This is a forward operation: you only need to check what you already wrote, not revisit past compartments.
160867
-
160868
- Examples of same-intent pairs to collapse:
160869
- - "X shouldn't cause cache bust" + "X must not bust cache by itself" → keep only the first, in its original compartment.
160870
- - "Let's use monorepo" + "Yes, monorepo is the right call" → keep only the first.
160871
- - "Add logging" + "We need logs here too" → keep only the first.
160872
-
160873
- Never keep two U: lines for the same underlying directive across compartments.
160874
-
160875
- # Budget
160876
-
160877
- - HARD LIMIT: 3–5 U: lines per compartment. 0–2 is typical.
160878
- - If you have more than 5 candidate U: lines in one compartment, that is a signal to split into two compartments at a natural pivot, not to stuff more.
160879
- - Every U: line must be immediately followed by 1–3 sentences describing the outcome, decision, or effect. Never stack two U: lines without intervening outcome text.
160880
-
160881
- # Example: CORRECT preservation (narrative-first, verbatim U: line)
160882
-
160883
- <compartment start="50" end="120" title="Built the auth layer">
160884
- Implemented JWT auth with hard 60-minute exp claim and refresh-token rotation. Chose Bearer tokens over cookies after finding cookie-based auth broke the SPA flow. Added session_expiry config (read-only at runtime). Commits: a3f891, b22c4e.
160885
- U: We need session expiry capped at 1 hour, no exceptions
160886
- Hardcoded the 60-minute cap at the JWT-issuer layer so runtime overrides cannot extend it.
160887
- </compartment>
160888
-
160889
- Notice: only one U: line, kept verbatim from the user's actual message. The cookie-to-Bearer pivot is narrative because paraphrase captures the signal fully.
160890
-
160891
- # Example: OVER-PRESERVATION (avoid)
160892
-
160893
- <compartment start="200" end="350" title="Refactored data layer">
160894
- U: Okay let's start on the data layer
160895
- U: What about transactions?
160896
- U: Yes that approach looks good
160897
- U: Actually wait, maybe we need write-ahead logging
160898
- U: I just noticed the previous commit broke a test
160899
- U: Let's commit and ship it
160900
- Refactored data layer with WAL mode and connection pooling.
160901
- </compartment>
160902
-
160903
- Problems: pacing, question, agreement, observation, pacing again. Only one message carries signal, and even that is narrative-capturable.
160904
-
160905
- # CORRECT version of the above
160906
-
160907
- <compartment start="200" end="350" title="Refactored data layer">
160908
- Refactored data layer to use WAL mode plus connection pooling. Chose WAL over plain connections for concurrent read performance under sustained write load.
160909
- </compartment>
160910
-
160911
- Zero U: lines. The pivot to WAL is clear in narrative.
160912
-
160913
- Fact rules:
160914
- - Facts are editable state, not append-only notes. Rewrite, normalize, deduplicate, or drop existing facts whenever needed.
160915
- - Before emitting any fact, check all existing facts in the same category for semantic duplicates. If two facts describe the same decision, constraint, or default with different wording, merge them into one canonical statement. Never emit two facts that could be answered by the same question.
160916
- - When project memories are provided as read-only reference, drop any session fact that is already covered by a project memory. Project memories are the canonical cross-session source; session facts must not duplicate them.
160917
- - Facts must be durable and actionable after the conversation ends.
160918
- - A fact is either a stable invariant/default or a reusable operating rule. If it mainly explains what happened, it belongs in a compartment, not a fact.
160919
- - Facts belong only in these categories when relevant: WORKFLOW_RULES, ARCHITECTURE_DECISIONS, CONSTRAINTS, CONFIG_DEFAULTS, KNOWN_ISSUES, ENVIRONMENT, NAMING, USER_PREFERENCES, USER_DIRECTIVES.
160920
- - Keep only high-signal facts. Omit greetings, acknowledgements, temporary status, one-off sequencing, branch-local tactics, and task-local cleanup notes.
160921
- - When a user message carries durable goals, constraints, preferences, or decision rationale, add a USER_DIRECTIVES fact when future agents should follow it after the session is compacted.
160922
- - Do not turn task-local details into facts.
160923
- - Do not keep stale facts. Rewrite or drop them even if the new input only implies they are obsolete.
160924
- - Keep existing ARCHITECTURE_DECISIONS and CONSTRAINTS facts when they are still valid and uncontradicted; rewrite them into canonical form instead of dropping them.
160925
- - Facts must be present tense and operational. Do not use chronology or provenance wording such as: initially, currently, remained, previously, later, then, was implemented, we changed, used to.
160926
- - One fact bullet must contain exactly one rule/default/constraint/preference. If a candidate fact mixes history with guidance, keep the guidance and drop the history.
160927
- - Durability test: a future agent should still act correctly on the fact next session, after merge/restart, without rereading the conversation.
160928
- - Category guide:
160929
- - WORKFLOW_RULES: standing repeatable process only. Prefer Do/When form: When <condition>, <action>. Do not store one-off branch strategy or task-specific sequencing unless it is standing policy.
160930
- - ARCHITECTURE_DECISIONS: stable design choice. Use: <component> uses <choice> because <reason>.
160931
- - CONSTRAINTS: hard must/must-not rule or invariant. Use: <thing> must/must not <action> because <reason>.
160932
- - CONFIG_DEFAULTS: stable default only. Use: <key>=<value>.
160933
- - KNOWN_ISSUES: unresolved recurring problem only. Do not store solved-issue stories.
160934
- - ENVIRONMENT: stable setup fact that affects future work.
160935
- - NAMING: canonical term choice. Use: Use <term>; avoid <term>.
160936
- - USER_PREFERENCES: durable user preference. Prefer Do/When form.
160937
- - USER_DIRECTIVES: durable user-stated goal, constraint, preference, or rationale. Keep the user's wording when it carries meaning, but narrow it to 1-3 sentences and remove filler.
160938
- - Fact dedup examples:
160939
- - These are DUPLICATES (merge into one): "Plugin config uses layered JSONC files" and "AFT plugin config uses layered JSONC files at ~/.config/opencode/aft.jsonc and <project>/.opencode/aft.jsonc, with project values deep-merging over user values." → keep the longer, more specific version only.
160940
- - These are NOT duplicates (keep both): "AFT uses 1-based line numbers" and "AFT converts to LSP 0-based UTF-16 at the protocol boundary" → different aspects of the same system.
160941
- - Fact rewrite examples:
160942
- - Bad ARCHITECTURE_DECISIONS: The new tool-heavy \`ctx_reduce\` reminder was initially implemented as a hidden instruction appended to the latest user message in \`transform\`.
160943
- - Good ARCHITECTURE_DECISIONS: \`ctx_reduce\` turn reminders are injected into the latest user message in \`transform\`.
160944
- - Bad WORKFLOW_RULES: Current local workflow remained feat -> integrate -> build for code changes.
160945
- - Good WORKFLOW_RULES (only if this is standing policy): For magic-context changes, commit on \`feat/magic-context\`, cherry-pick to \`integrate/athena-magic-context\`, run \`bun run build\` on integrate, then return to \`feat/magic-context\`.
160946
-
160947
- Input notes:
160948
- - [N] or [N-M] is a stable raw OpenCode message range.
160949
- - U: means user.
160950
- - A: means assistant.
160951
- - TC: means tool call — a compact summary of what the agent did (e.g., "TC: Fix lint errors", "TC: read(src/index.ts)", "TC: grep(ctx_memory)"). TC lines appear when there is no text describing the action. Use them to understand what happened between text blocks, but do not copy them verbatim into compartments — incorporate their meaning into the narrative.
160952
- - commits: ... on an assistant block lists commit hashes mentioned in that work unit; keep the relevant ones in the compartment summary when they matter.
160953
-
160954
- Output valid XML only in this shape:
160955
- <output>
160956
- <compartments>
160957
- <compartment start="FIRST" end="LAST" title="short title">
160958
- U: Verbatim high-signal user message
160959
- Summary text describing what was done and why.
160960
- U: Another high-signal user message if applicable
160961
- More summary text.
160962
- </compartment>
160963
- </compartments>
160964
- <facts>
160965
- <WORKFLOW_RULES>
160966
- * Fact text
160967
- </WORKFLOW_RULES>
160968
- </facts>
160969
- <meta>
160970
- <messages_processed>FIRST-LAST</messages_processed>
160971
- <unprocessed_from>INDEX</unprocessed_from>
160972
- </meta>
160973
- </output>
160974
-
160975
- Omit empty fact categories. Compartments must be ordered, contiguous for the ranges they cover, and non-overlapping.`, HISTORIAN_EDITOR_SYSTEM_PROMPT = `You are an editor refining a historian draft. The draft was produced by a first-pass historian and may contain noise — low-signal U: lines, redundant quotes across compartments, and weak preservation decisions.
160976
-
160977
- Your job is to clean the draft without changing its structure:
160978
-
160979
- 1. DROP low-signal U: lines:
160980
- - Questions in any form — resolved decision goes in narrative only.
160981
- - Pacing/agreement: "let's go", "yes", "okay", "sounds good", "I agree".
160982
- - Pasted error output, debugging status, mid-process observations.
160983
- - Tactical micro-direction: "now look at X", "first check Y".
160984
-
160985
- 2. DROP cross-compartment duplicates:
160986
- - Scan U: lines across ALL compartments in the draft.
160987
- - If two U: lines express the same intent/decision, keep only ONE — in the compartment where the outcome is actually described.
160988
-
160989
- 3. STRIP agreement prefixes:
160990
- - "Yes we should X" → keep only the directive content, or drop entirely if nothing substantive remains after "Yes".
160991
-
160992
- 4. PREFER verbatim over paraphrase:
160993
- - If the draft rephrased a user directive into formal constraint language, restore the user's wording if available.
160994
- - Do not invent technical specificity (file paths, function names, constants) the user did not state.
160995
-
160996
- 5. FOLD into narrative when possible:
160997
- - If a U: line's signal is already captured in the surrounding narrative, drop the U: line.
160998
- - Narrative should not need the U: line to be understood.
160999
-
161000
- 6. KEEP as U: lines ONLY:
161001
- - Hard constraints with concrete values (thresholds, byte sizes, timeouts).
161002
- - Explicit rejections ("X is wrong because Y", "NOT Z").
161003
- - Implementation pivots in future-tense ("instead of A, do B").
161004
- - Source-of-truth corrections.
161005
-
161006
- Do NOT change:
161007
- - Compartment titles, ranges, or ordering.
161008
- - Narrative summary text unless it directly references a U: line you dropped (in which case integrate the signal into the narrative).
161009
- - Facts — leave the facts section untouched.
161010
- - <meta> section — leave messages_processed and unprocessed_from exactly as the draft has them.
161011
-
161012
- Output the cleaned version as valid XML matching the original structure. Preserve all XML tags, compartment ranges, meta, and facts.`, USER_OBSERVATIONS_APPENDIX = `
161013
-
161014
- User observation rules (EXPERIMENTAL):
161015
- - After outputting compartments and facts, also output a <user_observations> section.
161016
- - User observations capture UNIVERSAL behavioral patterns about the human user — not project-specific or technical.
161017
- - Good observations: communication preferences, review focus areas, expertise level, decision-making patterns, frustration triggers, working style.
161018
- - Bad observations (DO NOT emit): project-specific preferences, framework choices, coding language preferences, one-off moods, task-local frustration.
161019
- - Each observation must be a single concise sentence in present tense.
161020
- - Only emit observations you have strong evidence for from the conversation. Do not speculate.
161021
- - Emit 0-5 observations per run. Zero is fine if nothing stands out.
161022
- - The output shape inside <output> gains an additional section:
161023
- <user_observations>
161024
- * User prefers terse communication and dislikes verbose explanations.
161025
- * User is technically deep — understands cache invalidation, SQLite internals, and prompt engineering.
161026
- </user_observations>
161027
- If no observations, omit the <user_observations> section entirely.`;
161028
- var init_compartment_prompt = __esm(() => {
161029
- init_compartment_storage();
161030
- });
161031
-
161032
- // src/shared/opencode-config-dir.ts
161033
- import { homedir as homedir6 } from "node:os";
161034
- import { join as join12, resolve as resolve4 } from "node:path";
161035
- function getCliConfigDir() {
161036
- const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
161037
- if (envConfigDir) {
161038
- return resolve4(envConfigDir);
161039
- }
161040
- if (process.platform === "win32") {
161041
- return join12(homedir6(), ".config", "opencode");
161042
- }
161043
- return join12(process.env.XDG_CONFIG_HOME || join12(homedir6(), ".config"), "opencode");
161044
- }
161045
- function getOpenCodeConfigDir(_options) {
161046
- return getCliConfigDir();
161047
- }
161048
- function getOpenCodeConfigPaths(options) {
161049
- const configDir = getOpenCodeConfigDir(options);
161050
- return {
161051
- configDir,
161052
- configJson: join12(configDir, "opencode.json"),
161053
- configJsonc: join12(configDir, "opencode.jsonc"),
161054
- packageJson: join12(configDir, "package.json"),
161055
- omoConfig: join12(configDir, "magic-context.jsonc")
161056
- };
161057
- }
161058
- var init_opencode_config_dir = () => {};
161059
-
161060
- // src/shared/conflict-detector.ts
161061
- import { join as join13 } from "node:path";
161062
- function detectConflicts(directory) {
161063
- const conflicts = {
161064
- compactionAuto: false,
161065
- compactionPrune: false,
161066
- dcpPlugin: false,
161067
- omoPreemptiveCompaction: false,
161068
- omoContextWindowMonitor: false,
161069
- omoAnthropicRecovery: false
161070
- };
161071
- const reasons = [];
161072
- const compactionResult = checkCompaction(directory);
161073
- if (compactionResult.auto) {
161074
- conflicts.compactionAuto = true;
161075
- reasons.push("OpenCode auto-compaction is enabled (compaction.auto=true)");
161076
- }
161077
- if (compactionResult.prune) {
161078
- conflicts.compactionPrune = true;
161079
- reasons.push("OpenCode prune is enabled (compaction.prune=true)");
161080
- }
161081
- const dcpFound = checkDcpPlugin(directory);
161082
- if (dcpFound) {
161083
- conflicts.dcpPlugin = true;
161084
- reasons.push("opencode-dcp plugin is installed — it conflicts with Magic Context's context management");
161085
- }
161086
- const omoResult = checkOmoHooks(directory);
161087
- if (omoResult.preemptiveCompaction) {
161088
- conflicts.omoPreemptiveCompaction = true;
161089
- reasons.push("oh-my-opencode preemptive-compaction hook is active — it triggers compaction that conflicts with historian");
161090
- }
161091
- if (omoResult.contextWindowMonitor) {
161092
- conflicts.omoContextWindowMonitor = true;
161093
- reasons.push("oh-my-opencode context-window-monitor hook is active — it injects usage warnings that overlap with Magic Context nudges");
161094
- }
161095
- if (omoResult.anthropicRecovery) {
161096
- conflicts.omoAnthropicRecovery = true;
161097
- reasons.push("oh-my-opencode anthropic-context-window-limit-recovery hook is active — it triggers emergency compaction that bypasses historian");
161098
- }
161099
- return {
161100
- hasConflict: reasons.length > 0,
161101
- reasons,
161102
- conflicts
161103
- };
161104
- }
161105
- function checkCompaction(directory) {
161106
- if (process.env.OPENCODE_DISABLE_AUTOCOMPACT) {
161107
- return { auto: false, prune: false };
161108
- }
161109
- const projectResult = readProjectCompaction(directory);
161110
- if (projectResult.resolved)
161111
- return projectResult;
161112
- const userResult = readUserCompaction();
161113
- if (userResult.resolved)
161114
- return userResult;
161115
- return { auto: true, prune: false };
161116
- }
161117
- function readProjectCompaction(directory) {
161118
- const dotOcJsonc = join13(directory, ".opencode", "opencode.jsonc");
161119
- const dotOcJson = join13(directory, ".opencode", "opencode.json");
161120
- const dotOcConfig = readJsoncFile(dotOcJsonc) ?? readJsoncFile(dotOcJson);
161121
- if (dotOcConfig?.compaction) {
161122
- const c = dotOcConfig.compaction;
161123
- if (c.auto !== undefined || c.prune !== undefined) {
161124
- return { auto: c.auto === true, prune: c.prune === true, resolved: true };
161125
- }
161126
- }
161127
- const rootJsonc = join13(directory, "opencode.jsonc");
161128
- const rootJson = join13(directory, "opencode.json");
161129
- const rootConfig = readJsoncFile(rootJsonc) ?? readJsoncFile(rootJson);
161130
- if (rootConfig?.compaction) {
161131
- const c = rootConfig.compaction;
161132
- if (c.auto !== undefined || c.prune !== undefined) {
161133
- return { auto: c.auto === true, prune: c.prune === true, resolved: true };
161134
- }
161135
- }
161136
- return { auto: false, prune: false, resolved: false };
161137
- }
161138
- function readUserCompaction() {
161139
- try {
161140
- const paths = getOpenCodeConfigPaths({ binary: "opencode" });
161141
- const config2 = readJsoncFile(paths.configJsonc) ?? readJsoncFile(paths.configJson);
161142
- if (config2?.compaction) {
161143
- const c = config2.compaction;
161144
- if (c.auto !== undefined || c.prune !== undefined) {
161145
- return { auto: c.auto === true, prune: c.prune === true, resolved: true };
161146
- }
161147
- }
161148
- } catch {}
161149
- return { auto: false, prune: false, resolved: false };
161150
- }
161151
- function checkDcpPlugin(directory) {
161152
- const plugins = collectPluginEntries(directory);
161153
- return plugins.some((p) => matchesPackageName(p, DCP_PACKAGE_NAMES));
161154
- }
161155
- function matchesPackageName(entry, canonicalNames) {
161156
- if (entry.startsWith("file:") || entry.startsWith("http:") || entry.startsWith("https:") || entry.startsWith("/") || entry.startsWith("./") || entry.startsWith("../")) {
161157
- return false;
161158
- }
161159
- const lastAt = entry.lastIndexOf("@");
161160
- const nameOnly = lastAt > 0 ? entry.slice(0, lastAt) : entry;
161161
- return canonicalNames.has(nameOnly);
161162
- }
161163
- function extractPluginName(entry) {
161164
- if (typeof entry === "string")
161165
- return entry;
161166
- if (Array.isArray(entry) && typeof entry[0] === "string")
161167
- return entry[0];
161168
- return null;
161169
- }
161170
- function collectPluginEntries(directory) {
161171
- const plugins = [];
161172
- const pushFrom = (entries) => {
161173
- if (!entries)
161174
- return;
161175
- for (const entry of entries) {
161176
- const name2 = extractPluginName(entry);
161177
- if (name2)
161178
- plugins.push(name2);
161179
- }
161180
- };
161181
- for (const configPath of [
161182
- join13(directory, ".opencode", "opencode.jsonc"),
161183
- join13(directory, ".opencode", "opencode.json"),
161184
- join13(directory, "opencode.jsonc"),
161185
- join13(directory, "opencode.json")
161186
- ]) {
161187
- const config2 = readJsoncFile(configPath);
161188
- pushFrom(config2?.plugin);
161189
- }
161190
- try {
161191
- const paths = getOpenCodeConfigPaths({ binary: "opencode" });
161192
- for (const configPath of [paths.configJsonc, paths.configJson]) {
161193
- const config2 = readJsoncFile(configPath);
161194
- pushFrom(config2?.plugin);
161195
- }
161196
- } catch {}
161197
- return plugins;
161198
- }
161199
- function checkOmoHooks(directory) {
161200
- const result = {
161201
- preemptiveCompaction: false,
161202
- contextWindowMonitor: false,
161203
- anthropicRecovery: false
161204
- };
161205
- const plugins = collectPluginEntries(directory);
161206
- const hasOmo = plugins.some((p) => matchesPackageName(p, OMO_PACKAGE_NAMES));
161207
- if (!hasOmo)
161208
- return result;
161209
- const disabledHooks = readOmoDisabledHooks(directory);
161210
- result.preemptiveCompaction = !disabledHooks.has("preemptive-compaction");
161211
- result.contextWindowMonitor = !disabledHooks.has("context-window-monitor");
161212
- result.anthropicRecovery = !disabledHooks.has("anthropic-context-window-limit-recovery");
161213
- return result;
161214
- }
161215
- function readOmoDisabledHooks(directory) {
161216
- const disabled = new Set;
161217
- const configNames = [
161218
- "oh-my-opencode.jsonc",
161219
- "oh-my-opencode.json",
161220
- "oh-my-openagent.jsonc",
161221
- "oh-my-openagent.json"
161222
- ];
161223
- try {
161224
- const paths = getOpenCodeConfigPaths({ binary: "opencode" });
161225
- for (const name2 of configNames) {
161226
- const configPath = join13(paths.configDir, name2);
161227
- const config2 = readJsoncFile(configPath);
161228
- if (config2?.disabled_hooks) {
161229
- for (const hook of config2.disabled_hooks) {
161230
- disabled.add(hook);
161231
- }
161232
- }
161233
- }
161234
- } catch {}
161235
- for (const name2 of configNames) {
161236
- const config2 = readJsoncFile(join13(directory, name2));
161237
- if (config2?.disabled_hooks) {
161238
- for (const hook of config2.disabled_hooks) {
161239
- disabled.add(hook);
161240
- }
161241
- }
161242
- }
161243
- return disabled;
161244
- }
161245
- function formatConflictShort(result) {
161246
- if (!result.hasConflict)
161247
- return "";
161248
- const lines = [
161249
- "⚠️ Magic Context is disabled due to conflicting configuration:",
161250
- "",
161251
- ...result.reasons.map((r) => `• ${r}`),
161252
- "",
161253
- "Fix: run `npx @wolfx/opencode-magic-context@latest doctor`"
161254
- ];
161255
- return lines.join(`
161256
- `);
161257
- }
161258
- var DCP_PACKAGE_NAMES, OMO_PACKAGE_NAMES;
161259
- var init_conflict_detector = __esm(() => {
161260
- init_jsonc_parser();
161261
- init_opencode_config_dir();
161262
- DCP_PACKAGE_NAMES = new Set(["@tarquinen/opencode-dcp"]);
161263
- OMO_PACKAGE_NAMES = new Set(["oh-my-opencode", "oh-my-openagent"]);
161264
- });
161265
-
161266
- // src/plugin/conflict-warning-hook.ts
161267
- var exports_conflict_warning_hook = {};
161268
- __export(exports_conflict_warning_hook, {
161269
- sendTuiSetupNotification: () => sendTuiSetupNotification,
161270
- sendStartupAnnouncement: () => sendStartupAnnouncement,
161271
- sendConflictWarning: () => sendConflictWarning,
161272
- cleanupConflictWarnings: () => cleanupConflictWarnings
161273
- });
161274
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
161275
- import { homedir as homedir7, platform as platform2 } from "node:os";
161276
- import { join as join14 } from "node:path";
161277
- function getDesktopStatePath() {
161278
- const os2 = platform2();
161279
- const home = homedir7();
161280
- if (os2 === "darwin") {
161281
- return join14(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
161282
- }
161283
- if (os2 === "linux") {
161284
- const xdgConfig = process.env.XDG_CONFIG_HOME || join14(home, ".config");
161285
- return join14(xdgConfig, "ai.opencode.desktop", "opencode.global.dat");
161286
- }
161287
- if (os2 === "win32") {
161288
- const appData = process.env.APPDATA || join14(home, "AppData", "Roaming");
161289
- return join14(appData, "ai.opencode.desktop", "opencode.global.dat");
161290
- }
161291
- return null;
161292
- }
161293
- function readDesktopState(directory) {
161294
- const statePath = getDesktopStatePath();
161295
- if (!statePath || !existsSync11(statePath)) {
161296
- log(`[magic-context] conflict-warning: Desktop state file not found at ${statePath}`);
161297
- return { sessionId: null, sidecarUrl: null };
161298
- }
161299
- try {
161300
- const raw = readFileSync8(statePath, "utf-8");
161301
- const state = JSON.parse(raw);
161302
- let sidecarUrl = null;
161303
- const serverStr = state.server;
161304
- if (typeof serverStr === "string") {
161305
- try {
161306
- const serverState = JSON.parse(serverStr);
161307
- if (typeof serverState.currentSidecarUrl === "string") {
161308
- sidecarUrl = serverState.currentSidecarUrl;
161309
- }
161310
- } catch {}
161311
- }
161312
- let sessionId = null;
161313
- const layoutPage = state["layout.page"];
161314
- if (typeof layoutPage === "string") {
161315
- const parsed = JSON.parse(layoutPage);
161316
- const lastProjectSession = parsed.lastProjectSession;
161317
- if (lastProjectSession) {
161318
- const entry = lastProjectSession[directory];
161319
- sessionId = entry?.id ?? null;
161320
- }
161321
- }
161322
- return { sessionId, sidecarUrl };
161323
- } catch (error51) {
161324
- log(`[magic-context] conflict-warning: failed to read Desktop state: ${error51 instanceof Error ? error51.message : String(error51)}`);
161325
- return { sessionId: null, sidecarUrl: null };
161326
- }
161327
- }
161328
- function getDesktopState(directory) {
161329
- let cached2 = cachedDesktopStateByDir.get(directory);
161330
- if (!cached2) {
161331
- cached2 = readDesktopState(directory);
161332
- cachedDesktopStateByDir.set(directory, cached2);
161333
- }
161334
- return cached2;
161335
- }
161336
- async function deleteMessage(serverUrl, sessionId, messageId) {
161337
- const auth = getServerAuth();
161338
- const url2 = `${serverUrl}/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`;
161339
- try {
161340
- const response = await fetch(url2, {
161341
- method: "DELETE",
161342
- headers: auth ? { Authorization: auth } : {},
161343
- signal: AbortSignal.timeout(1e4)
161344
- });
161345
- if (!response.ok) {
161346
- log(`[magic-context] conflict-warning: DELETE failed status=${response.status} url=${url2}`);
161347
- return false;
161348
- }
161349
- return true;
161350
- } catch (error51) {
161351
- log(`[magic-context] conflict-warning: DELETE error (url=${serverUrl}): ${error51 instanceof Error ? error51.message : String(error51)}`);
161352
- return false;
161353
- }
161354
- }
161355
- function getServerAuth() {
161356
- const password = process.env.OPENCODE_SERVER_PASSWORD;
161357
- if (!password)
161358
- return;
161359
- const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
161360
- return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
161361
- }
161362
- async function getSessionMessages(client, sessionId) {
161363
- try {
161364
- const c = client;
161365
- if (typeof c.session?.messages === "function") {
161366
- const result = await c.session.messages({
161367
- path: { id: sessionId },
161368
- query: { limit: 50 }
161369
- });
161370
- return result?.data ?? [];
161371
- }
161372
- } catch (error51) {
161373
- log(`[magic-context] conflict-warning: failed to read messages: ${error51 instanceof Error ? error51.message : String(error51)}`);
161374
- }
161375
- return [];
161376
- }
161377
- async function sendConflictWarning(client, directory, conflictResult) {
161378
- const { sessionId } = getDesktopState(directory);
161379
- if (!sessionId) {
161380
- log("[magic-context] conflict-warning: could not find active session for Desktop warning");
161381
- return;
161382
- }
161383
- const warningText = formatConflictShort(conflictResult);
161384
- log(`[magic-context] sending conflict warning to session ${sessionId}: ${conflictResult.reasons.join(", ")}`);
161385
- try {
161386
- const c = client;
161387
- const promptInput = {
161388
- path: { id: sessionId },
161389
- body: {
161390
- noReply: true,
161391
- parts: [
161392
- {
161393
- type: "text",
161394
- text: warningText,
161395
- ignored: true
161396
- }
161397
- ]
161398
- }
161399
- };
161400
- if (typeof c.session?.prompt === "function") {
161401
- await Promise.resolve(c.session.prompt(promptInput));
161402
- } else if (typeof c.session?.promptAsync === "function") {
161403
- await c.session.promptAsync(promptInput);
161404
- } else {
161405
- log("[magic-context] conflict-warning: session prompt API unavailable");
161406
- }
161407
- } catch (error51) {
161408
- log(`[magic-context] conflict-warning: failed to send: ${error51 instanceof Error ? error51.message : String(error51)}`);
161409
- }
161410
- }
161411
- async function cleanupConflictWarnings(client, directory, serverUrl) {
161412
- const { sessionId } = getDesktopState(directory);
161413
- if (!sessionId) {
161414
- log("[magic-context] cleanup: no active Desktop session found");
161415
- return;
161416
- }
161417
- const messages = await getSessionMessages(client, sessionId);
161418
- if (messages.length === 0)
161419
- return;
161420
- const warningMessageIds = [];
161421
- for (let i = messages.length - 1;i >= 0; i--) {
161422
- const msg = messages[i];
161423
- const msgId = msg.info?.id;
161424
- const msgRole = msg.info?.role;
161425
- if (!msgId || msgRole !== "user")
161426
- break;
161427
- const parts = msg.parts ?? [];
161428
- const isWarning = parts.length > 0 && parts.every((p) => p.ignored === true && p.type === "text" && typeof p.text === "string" && p.text.startsWith(CONFLICT_WARNING_MARKER));
161429
- if (isWarning) {
161430
- warningMessageIds.push(msgId);
161431
- } else {
161432
- break;
161433
- }
161434
- }
161435
- if (warningMessageIds.length === 0) {
161436
- await cleanupEnabledMessages(messages, serverUrl, sessionId);
161437
- return;
161438
- }
161439
- if (!serverUrl) {
161440
- log("[magic-context] cleanup: no serverUrl provided, cannot delete messages");
161441
- return;
161442
- }
161443
- log(`[magic-context] cleaning up ${warningMessageIds.length} conflict warning message(s) from session ${sessionId}`);
161444
- for (const messageId of warningMessageIds) {
161445
- const ok = await deleteMessage(serverUrl, sessionId, messageId);
161446
- if (ok) {
161447
- log(`[magic-context] deleted conflict warning message ${messageId}`);
161448
- }
161449
- }
161450
- const enabledText = `${ENABLED_MARKER}. Enjoy! ✨`;
161451
- try {
161452
- const c = client;
161453
- const promptInput = {
161454
- path: { id: sessionId },
161455
- body: {
161456
- noReply: true,
161457
- parts: [{ type: "text", text: enabledText, ignored: true }]
161458
- }
161459
- };
161460
- if (typeof c.session?.prompt === "function") {
161461
- await Promise.resolve(c.session.prompt(promptInput));
161462
- } else if (typeof c.session?.promptAsync === "function") {
161463
- await c.session.promptAsync(promptInput);
161464
- }
161465
- } catch {}
161466
- setTimeout(async () => {
161467
- try {
161468
- const freshMessages = await getSessionMessages(client, sessionId);
161469
- for (let i = freshMessages.length - 1;i >= 0; i--) {
161470
- const msg = freshMessages[i];
161471
- const msgId = msg.info?.id;
161472
- const msgRole = msg.info?.role;
161473
- if (!msgId || msgRole !== "user")
161474
- break;
161475
- const parts = msg.parts ?? [];
161476
- const isEnabled = parts.length > 0 && parts.every((p) => p.ignored === true && p.type === "text" && typeof p.text === "string" && p.text.startsWith(ENABLED_MARKER));
161477
- if (isEnabled) {
161478
- await deleteMessage(serverUrl, sessionId, msgId);
161479
- } else {
161480
- break;
161481
- }
161482
- }
161483
- } catch {}
161484
- }, 1000);
161485
- }
161486
- async function cleanupEnabledMessages(messages, serverUrl, sessionId) {
161487
- if (!serverUrl)
161488
- return;
161489
- for (let i = messages.length - 1;i >= 0; i--) {
161490
- const msg = messages[i];
161491
- const msgId = msg.info?.id;
161492
- const msgRole = msg.info?.role;
161493
- if (!msgId || msgRole !== "user")
161494
- break;
161495
- const parts = msg.parts ?? [];
161496
- const isEnabled = parts.length > 0 && parts.every((p) => p.ignored === true && p.type === "text" && typeof p.text === "string" && p.text.startsWith(ENABLED_MARKER));
161497
- if (isEnabled) {
161498
- await deleteMessage(serverUrl, sessionId, msgId);
161499
- } else {
161500
- break;
161501
- }
161502
- }
161503
- }
161504
- async function sendTuiSetupNotification(client, directory, serverUrl) {
161505
- const { sessionId } = getDesktopState(directory);
161506
- if (!sessionId)
161507
- return;
161508
- const text = [
161509
- `${TUI_SETUP_MARKER}`,
161510
- "",
161511
- "Magic Context added its TUI plugin to your tui.json.",
161512
- "Restart OpenCode to see the sidebar with live context breakdown,",
161513
- "token usage, historian status, memory counts, and more."
161514
- ].join(`
161515
- `);
161516
- try {
161517
- const c = client;
161518
- const promptInput = {
161519
- path: { id: sessionId },
161520
- body: {
161521
- noReply: true,
161522
- parts: [{ type: "text", text, ignored: true }]
161523
- }
161524
- };
161525
- if (typeof c.session?.prompt === "function") {
161526
- await Promise.resolve(c.session.prompt(promptInput));
161527
- } else if (typeof c.session?.promptAsync === "function") {
161528
- await c.session.promptAsync(promptInput);
161529
- }
161530
- } catch {
161531
- return;
161532
- }
161533
- if (!serverUrl)
161534
- return;
161535
- setTimeout(async () => {
161536
- try {
161537
- const msgs = await getSessionMessages(client, sessionId);
161538
- for (let i = msgs.length - 1;i >= 0; i--) {
161539
- const msg = msgs[i];
161540
- const msgId = msg.info?.id;
161541
- if (!msgId || msg.info?.role !== "user")
161542
- break;
161543
- const parts = msg.parts ?? [];
161544
- const isTuiSetup = parts.length > 0 && parts.every((p) => p.ignored === true && p.type === "text" && typeof p.text === "string" && p.text.startsWith(TUI_SETUP_MARKER));
161545
- if (isTuiSetup) {
161546
- await deleteMessage(serverUrl, sessionId, msgId);
161547
- } else {
161548
- break;
161549
- }
161550
- }
161551
- } catch {}
161552
- }, 1000);
161553
- }
161554
- async function sendStartupAnnouncement(client, directory, version2, features, footer, markSeen) {
161555
- if (!version2 || features.length === 0)
161556
- return;
161557
- const { sessionId } = getDesktopState(directory);
161558
- if (!sessionId) {
161559
- return;
161560
- }
161561
- const bullets = features.map((line) => ` • ${line}`).join(`
161562
- `);
161563
- const sections = [`${ANNOUNCEMENT_MARKER} v${version2}:`, "", bullets];
161564
- if (footer && footer.trim().length > 0) {
161565
- sections.push("", footer);
161566
- }
161567
- const text = sections.join(`
161568
- `);
161569
- log(`[magic-context] sending startup announcement for v${version2} to session ${sessionId}`);
161570
- try {
161571
- const c = client;
161572
- const promptInput = {
161573
- path: { id: sessionId },
161574
- body: {
161575
- noReply: true,
161576
- parts: [{ type: "text", text, ignored: true }]
161577
- }
161578
- };
161579
- if (typeof c.session?.prompt === "function") {
161580
- await Promise.resolve(c.session.prompt(promptInput));
161581
- } else if (typeof c.session?.promptAsync === "function") {
161582
- await c.session.promptAsync(promptInput);
161583
- } else {
161584
- log("[magic-context] announcement: session prompt API unavailable");
161585
- return;
161586
- }
161587
- } catch (error51) {
161588
- log(`[magic-context] announcement: failed to send: ${error51 instanceof Error ? error51.message : String(error51)}`);
161589
- return;
161590
- }
161591
- markSeen(version2);
161592
- }
161593
- var CONFLICT_WARNING_MARKER = "⚠️ Magic Context is disabled due to conflicting configuration:", ENABLED_MARKER = "✨ Magic Context is now enabled", TUI_SETUP_MARKER = "\uD83D\uDCCA Magic Context sidebar configured", ANNOUNCEMENT_MARKER = "✨ Magic Context — what's new in", cachedDesktopStateByDir;
161594
- var init_conflict_warning_hook = __esm(() => {
161595
- init_conflict_detector();
161596
- init_logger();
161597
- cachedDesktopStateByDir = new Map;
161598
- });
161599
-
161600
161603
  // src/features/magic-context/memory/storage-memory-embeddings.ts
161601
161604
  function isEmbeddingBlob(value) {
161602
161605
  return value instanceof Uint8Array || value instanceof ArrayBuffer;
@@ -162190,9 +162193,9 @@ var init_embedding_identity = __esm(() => {
162190
162193
  });
162191
162194
 
162192
162195
  // src/features/magic-context/memory/embedding-local.ts
162193
- import { mkdirSync as mkdirSync5 } from "node:fs";
162196
+ import { mkdirSync as mkdirSync4 } from "node:fs";
162194
162197
  import { open, stat, unlink, writeFile } from "node:fs/promises";
162195
- import { dirname as dirname7, join as join18 } from "node:path";
162198
+ import { dirname as dirname4, join as join14 } from "node:path";
162196
162199
  import { pathToFileURL } from "node:url";
162197
162200
  async function acquireModelLoadLock(lockPath) {
162198
162201
  const waitStart = Date.now();
@@ -162230,7 +162233,7 @@ async function acquireModelLoadLock(lockPath) {
162230
162233
  log("[magic-context] embedding-load lock wait exceeded, proceeding without lock");
162231
162234
  return async () => {};
162232
162235
  }
162233
- await new Promise((resolve6) => setTimeout(resolve6, LOCK_POLL_MS));
162236
+ await new Promise((resolve5) => setTimeout(resolve5, LOCK_POLL_MS));
162234
162237
  }
162235
162238
  }
162236
162239
  }
@@ -162254,7 +162257,7 @@ async function injectWasmOrtForElectron() {
162254
162257
  const { createRequire: createRequireFn } = await import("node:module");
162255
162258
  const requireFn = createRequireFn(import.meta.url);
162256
162259
  const pkgPath = requireFn.resolve("onnxruntime-web/package.json");
162257
- const distDir = join18(dirname7(pkgPath), "dist");
162260
+ const distDir = join14(dirname4(pkgPath), "dist");
162258
162261
  const wasmPathsPrefix = `${pathToFileURL(distDir).href}/`;
162259
162262
  if (ortWeb.env?.wasm) {
162260
162263
  ortWeb.env.wasm.wasmPaths = wasmPathsPrefix;
@@ -162365,15 +162368,15 @@ class LocalEmbeddingProvider {
162365
162368
  if (LogLevel && "ERROR" in LogLevel) {
162366
162369
  env.logLevel = LogLevel.ERROR;
162367
162370
  }
162368
- const modelCacheDir = join18(getMagicContextStorageDir(), "models");
162371
+ const modelCacheDir = join14(getMagicContextStorageDir(), "models");
162369
162372
  try {
162370
- mkdirSync5(modelCacheDir, { recursive: true });
162373
+ mkdirSync4(modelCacheDir, { recursive: true });
162371
162374
  env.cacheDir = modelCacheDir;
162372
162375
  } catch {
162373
162376
  log("[magic-context] could not create model cache dir, using library default");
162374
162377
  }
162375
162378
  const createPipeline = transformersModule.pipeline;
162376
- const lockPath = join18(modelCacheDir, ".load.lock");
162379
+ const lockPath = join14(modelCacheDir, ".load.lock");
162377
162380
  const releaseLock = await acquireModelLoadLock(lockPath);
162378
162381
  const stopHeartbeat = startLockHeartbeat(lockPath);
162379
162382
  try {
@@ -162399,7 +162402,7 @@ class LocalEmbeddingProvider {
162399
162402
  }
162400
162403
  const delayMs = 300 * attempt + Math.floor(Math.random() * 200);
162401
162404
  log(`[magic-context] embedding model load attempt ${attempt}/${MAX_ATTEMPTS} failed transiently, retrying in ${delayMs}ms`);
162402
- await new Promise((resolve6) => setTimeout(resolve6, delayMs));
162405
+ await new Promise((resolve5) => setTimeout(resolve5, delayMs));
162403
162406
  }
162404
162407
  }
162405
162408
  if (this.pipeline) {
@@ -162427,8 +162430,8 @@ class LocalEmbeddingProvider {
162427
162430
  if (this.inFlight === 0) {
162428
162431
  return Promise.resolve();
162429
162432
  }
162430
- return new Promise((resolve6) => {
162431
- this.inFlightWaiters.push(resolve6);
162433
+ return new Promise((resolve5) => {
162434
+ this.inFlightWaiters.push(resolve5);
162432
162435
  });
162433
162436
  }
162434
162437
  finishInFlight() {
@@ -163212,9 +163215,9 @@ var init_storage_memory_fts = __esm(() => {
163212
163215
 
163213
163216
  // src/shared/models-dev-cache.ts
163214
163217
  import { createHash as createHash7 } from "node:crypto";
163215
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
163216
- import { homedir as homedir9, platform as platform3 } from "node:os";
163217
- import { join as join19 } from "node:path";
163218
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
163219
+ import { homedir as homedir7, platform as platform2 } from "node:os";
163220
+ import { join as join15 } from "node:path";
163218
163221
  function hashFast(input) {
163219
163222
  return createHash7("sha1").update(input).digest("hex");
163220
163223
  }
@@ -163225,16 +163228,16 @@ function getModelsJsonPath() {
163225
163228
  const cacheBase = getCacheDir();
163226
163229
  const source = process.env.OPENCODE_MODELS_URL?.trim();
163227
163230
  const filename = source && source !== "https://models.dev" ? `models-${hashFast(source)}.json` : "models.json";
163228
- return join19(cacheBase, "opencode", filename);
163231
+ return join15(cacheBase, "opencode", filename);
163229
163232
  }
163230
163233
  function getOpencodeConfigPath() {
163231
163234
  const envDir = process.env.OPENCODE_CONFIG_DIR?.trim();
163232
- const configDir = envDir ? envDir : platform3() === "win32" ? join19(homedir9(), ".config", "opencode") : join19(process.env.XDG_CONFIG_HOME || join19(homedir9(), ".config"), "opencode");
163233
- const jsonc = join19(configDir, "opencode.jsonc");
163234
- if (existsSync14(jsonc))
163235
+ const configDir = envDir ? envDir : platform2() === "win32" ? join15(homedir7(), ".config", "opencode") : join15(process.env.XDG_CONFIG_HOME || join15(homedir7(), ".config"), "opencode");
163236
+ const jsonc = join15(configDir, "opencode.jsonc");
163237
+ if (existsSync11(jsonc))
163235
163238
  return jsonc;
163236
- const json2 = join19(configDir, "opencode.json");
163237
- if (existsSync14(json2))
163239
+ const json2 = join15(configDir, "opencode.json");
163240
+ if (existsSync11(json2))
163238
163241
  return json2;
163239
163242
  return null;
163240
163243
  }
@@ -163266,9 +163269,9 @@ function loadModelsDevMetadataFromFile() {
163266
163269
  const modelsJsonPath = getModelsJsonPath();
163267
163270
  let fileFound = false;
163268
163271
  try {
163269
- if (existsSync14(modelsJsonPath)) {
163272
+ if (existsSync11(modelsJsonPath)) {
163270
163273
  fileFound = true;
163271
- const raw = readFileSync11(modelsJsonPath, "utf-8");
163274
+ const raw = readFileSync8(modelsJsonPath, "utf-8");
163272
163275
  const data = JSON.parse(raw);
163273
163276
  for (const [providerId, provider2] of Object.entries(data)) {
163274
163277
  if (!provider2?.models || typeof provider2.models !== "object")
@@ -163283,8 +163286,8 @@ function loadModelsDevMetadataFromFile() {
163283
163286
  }
163284
163287
  try {
163285
163288
  const configPath = getOpencodeConfigPath();
163286
- if (configPath && existsSync14(configPath)) {
163287
- const config2 = parseJsonc(readFileSync11(configPath, "utf-8"));
163289
+ if (configPath && existsSync11(configPath)) {
163290
+ const config2 = parseJsonc(readFileSync8(configPath, "utf-8"));
163288
163291
  if (config2.provider && typeof config2.provider === "object") {
163289
163292
  for (const [providerId, provider2] of Object.entries(config2.provider)) {
163290
163293
  if (!provider2?.models || typeof provider2.models !== "object")
@@ -163382,7 +163385,7 @@ var init_rpc_notifications = __esm(() => {
163382
163385
  });
163383
163386
 
163384
163387
  // src/features/magic-context/compaction-marker.ts
163385
- import { join as join20 } from "node:path";
163388
+ import { join as join16 } from "node:path";
163386
163389
  function randomBase62(length) {
163387
163390
  const chars = [];
163388
163391
  for (let i = 0;i < length; i++) {
@@ -163402,7 +163405,7 @@ function generatePartId(timestampMs, counter = 0n) {
163402
163405
  return generateId("prt", timestampMs, counter);
163403
163406
  }
163404
163407
  function getOpenCodeDbPath3() {
163405
- return join20(getDataDir(), "opencode", "opencode.db");
163408
+ return join16(getDataDir(), "opencode", "opencode.db");
163406
163409
  }
163407
163410
  function isOpenCodeSchemaCompatible(db, dbPath) {
163408
163411
  if (cachedSchemaCompatible?.path === dbPath) {
@@ -163544,7 +163547,7 @@ var init_compaction_marker = __esm(async () => {
163544
163547
  });
163545
163548
 
163546
163549
  // src/hooks/magic-context/compaction-marker-manager.ts
163547
- import { join as join21 } from "node:path";
163550
+ import { join as join17 } from "node:path";
163548
163551
  function validatePendingTarget(db, sessionId, pending) {
163549
163552
  const ocMessage = getOpenCodeMessageById(sessionId, pending.endMessageId);
163550
163553
  if (!ocMessage) {
@@ -163651,7 +163654,7 @@ function removeCompactionMarkerForSession(db, sessionId) {
163651
163654
  }
163652
163655
  }
163653
163656
  function checkCompactionMarkerConsistency(db) {
163654
- const opencodeDbPath = join21(getDataDir(), "opencode", "opencode.db");
163657
+ const opencodeDbPath = join17(getDataDir(), "opencode", "opencode.db");
163655
163658
  let opencodeDb;
163656
163659
  try {
163657
163660
  opencodeDb = new Database(opencodeDbPath, { readonly: true });
@@ -163934,8 +163937,8 @@ var init_compartment_runner_validation = __esm(async () => {
163934
163937
  });
163935
163938
 
163936
163939
  // src/hooks/magic-context/compartment-runner-historian.ts
163937
- import { mkdirSync as mkdirSync6, unlinkSync, writeFileSync as writeFileSync5 } from "node:fs";
163938
- import { join as join22 } from "node:path";
163940
+ import { mkdirSync as mkdirSync5, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
163941
+ import { join as join18 } from "node:path";
163939
163942
  function historianResponseDumpDir(directory) {
163940
163943
  return getProjectMagicContextHistorianDir(directory);
163941
163944
  }
@@ -164198,8 +164201,8 @@ function isTransientHistorianPromptError(message) {
164198
164201
  ].some((token) => normalized.includes(token));
164199
164202
  }
164200
164203
  function sleep(ms) {
164201
- return new Promise((resolve6) => {
164202
- setTimeout(resolve6, ms);
164204
+ return new Promise((resolve5) => {
164205
+ setTimeout(resolve5, ms);
164203
164206
  });
164204
164207
  }
164205
164208
  function cleanupHistorianDump(sessionId, dumpPath) {
@@ -164217,11 +164220,11 @@ function cleanupHistorianDump(sessionId, dumpPath) {
164217
164220
  function dumpHistorianResponse(sessionId, directory, label, text) {
164218
164221
  try {
164219
164222
  const dumpDir = historianResponseDumpDir(directory);
164220
- mkdirSync6(dumpDir, { recursive: true });
164223
+ mkdirSync5(dumpDir, { recursive: true });
164221
164224
  const safeSessionId = sanitizeDumpName(sessionId);
164222
164225
  const safeLabel = sanitizeDumpName(label);
164223
- const dumpPath = join22(dumpDir, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
164224
- writeFileSync5(dumpPath, text, "utf8");
164226
+ const dumpPath = join18(dumpDir, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
164227
+ writeFileSync2(dumpPath, text, "utf8");
164225
164228
  sessionLog(sessionId, "compartment agent: historian response dumped", {
164226
164229
  label,
164227
164230
  dumpPath
@@ -165592,16 +165595,16 @@ var init_caveman = __esm(() => {
165592
165595
  });
165593
165596
 
165594
165597
  // src/hooks/magic-context/historian-state-file.ts
165595
- import { mkdirSync as mkdirSync7, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
165596
- import { join as join23 } from "node:path";
165598
+ import { mkdirSync as mkdirSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "node:fs";
165599
+ import { join as join19 } from "node:path";
165597
165600
  function maybeWriteHistorianStateFile(sessionId, existingState, directory) {
165598
165601
  if (existingState.length <= HISTORIAN_STATE_INLINE_THRESHOLD)
165599
165602
  return;
165600
165603
  try {
165601
165604
  const dir = getProjectMagicContextHistorianDir(directory);
165602
- mkdirSync7(dir, { recursive: true });
165603
- const path5 = join23(dir, `state-${sessionId}-${Date.now()}.xml`);
165604
- writeFileSync6(path5, existingState, "utf8");
165605
+ mkdirSync6(dir, { recursive: true });
165606
+ const path5 = join19(dir, `state-${sessionId}-${Date.now()}.xml`);
165607
+ writeFileSync3(path5, existingState, "utf8");
165605
165608
  return path5;
165606
165609
  } catch {
165607
165610
  return;
@@ -167181,9 +167184,6 @@ function deepMergeRawConfig(base, override) {
167181
167184
  }
167182
167185
  return result;
167183
167186
  }
167184
- function getProjectUserOnlyFields(config2) {
167185
- return "auto_update" in config2 ? ["auto_update"] : [];
167186
- }
167187
167187
  function redactConfigValue(value) {
167188
167188
  if (value === undefined)
167189
167189
  return "<missing>";
@@ -167330,15 +167330,7 @@ function loadPluginConfig(directory) {
167330
167330
  }
167331
167331
  if (projectLoaded) {
167332
167332
  allWarnings.push(...projectLoaded.warnings.map((w) => `[project config] ${w}`));
167333
- const projectRaw = { ...projectLoaded.config };
167334
- const strippedUserOnlyFields = getProjectUserOnlyFields(projectRaw);
167335
- if (strippedUserOnlyFields.length > 0) {
167336
- for (const key of strippedUserOnlyFields) {
167337
- delete projectRaw[key];
167338
- }
167339
- allWarnings.push(`[project config] Ignoring ${strippedUserOnlyFields.join(", ")} from project config (security: these settings only honor user-level config)`);
167340
- }
167341
- mergedRaw = deepMergeRawConfig(mergedRaw, projectRaw);
167333
+ mergedRaw = deepMergeRawConfig(mergedRaw, projectLoaded.config);
167342
167334
  }
167343
167335
  const config2 = parsePluginConfig(mergedRaw);
167344
167336
  if (config2.configWarnings?.length) {
@@ -167411,15 +167403,7 @@ function loadPluginConfigDetailed(directory) {
167411
167403
  }
167412
167404
  if (projectLoaded) {
167413
167405
  allWarnings.push(...projectLoaded.warnings.map((w) => `[project config] ${w}`));
167414
- const projectRaw = { ...projectLoaded.config };
167415
- const strippedUserOnlyFields = getProjectUserOnlyFields(projectRaw);
167416
- if (strippedUserOnlyFields.length > 0) {
167417
- for (const key of strippedUserOnlyFields) {
167418
- delete projectRaw[key];
167419
- }
167420
- allWarnings.push(`[project config] Ignoring ${strippedUserOnlyFields.join(", ")} from project config (security: these settings only honor user-level config)`);
167421
- }
167422
- mergedRaw = deepMergeRawConfig(mergedRaw, projectRaw);
167406
+ mergedRaw = deepMergeRawConfig(mergedRaw, projectLoaded.config);
167423
167407
  }
167424
167408
  const recoveredTopLevelKeys = [];
167425
167409
  const config2 = parsePluginConfig(mergedRaw, recoveredTopLevelKeys);
@@ -167957,587 +167941,6 @@ async function runSidekick(deps) {
167957
167941
 
167958
167942
  // src/index.ts
167959
167943
  init_tool_definition_tokens();
167960
-
167961
- // src/hooks/auto-update-checker/index.ts
167962
- init_logger();
167963
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync, writeFileSync as writeFileSync4 } from "node:fs";
167964
- import { dirname as dirname6, join as join11 } from "node:path";
167965
-
167966
- // src/hooks/auto-update-checker/cache.ts
167967
- init_logger();
167968
- var import_comment_json2 = __toESM(require_src2(), 1);
167969
- import { spawn } from "node:child_process";
167970
- import { existsSync as existsSync9, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
167971
- import { basename, dirname as dirname5, join as join10 } from "node:path";
167972
-
167973
- // src/hooks/auto-update-checker/checker.ts
167974
- init_logger();
167975
- var import_comment_json = __toESM(require_src2(), 1);
167976
- import { existsSync as existsSync8, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2 } from "node:fs";
167977
- import { homedir as homedir5 } from "node:os";
167978
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
167979
- import { fileURLToPath } from "node:url";
167980
-
167981
- // src/hooks/auto-update-checker/constants.ts
167982
- import { homedir as homedir4, platform } from "node:os";
167983
- import { join as join8 } from "node:path";
167984
- var PACKAGE_NAME = "@wolfx/opencode-magic-context";
167985
- var NPM_REGISTRY_URL = "https://registry.npmjs.org";
167986
- var NPM_FETCH_TIMEOUT = 1e4;
167987
- function getOpenCodeCacheRoot() {
167988
- if (platform() === "win32") {
167989
- return join8(process.env.LOCALAPPDATA ?? homedir4(), "opencode");
167990
- }
167991
- return join8(homedir4(), ".cache", "opencode");
167992
- }
167993
- function getOpenCodeConfigRoot() {
167994
- if (platform() === "win32") {
167995
- return join8(process.env.APPDATA ?? join8(homedir4(), "AppData", "Roaming"), "opencode");
167996
- }
167997
- return join8(process.env.XDG_CONFIG_HOME ?? join8(homedir4(), ".config"), "opencode");
167998
- }
167999
- var CACHE_DIR = join8(getOpenCodeCacheRoot(), "packages");
168000
- var USER_OPENCODE_CONFIG = join8(getOpenCodeConfigRoot(), "opencode.json");
168001
- var USER_OPENCODE_CONFIG_JSONC = join8(getOpenCodeConfigRoot(), "opencode.jsonc");
168002
-
168003
- // src/hooks/auto-update-checker/types.ts
168004
- init_zod();
168005
- var NpmPackageEnvelopeSchema = exports_external.object({
168006
- "dist-tags": exports_external.record(exports_external.string(), exports_external.string()).optional().default({})
168007
- });
168008
- var OpencodePluginTupleSchema = exports_external.tuple([exports_external.string(), exports_external.record(exports_external.string(), exports_external.unknown())]);
168009
- var OpencodeConfigSchema = exports_external.object({
168010
- plugin: exports_external.array(exports_external.union([exports_external.string(), OpencodePluginTupleSchema])).optional()
168011
- });
168012
- var PackageJsonSchema = exports_external.object({
168013
- name: exports_external.string().optional(),
168014
- version: exports_external.string().optional(),
168015
- dependencies: exports_external.record(exports_external.string(), exports_external.string()).optional()
168016
- }).passthrough();
168017
-
168018
- // src/hooks/auto-update-checker/checker.ts
168019
- function warn(message) {
168020
- log(`WARN: ${message}`);
168021
- }
168022
- function isString(value) {
168023
- return typeof value === "string";
168024
- }
168025
- function pluginSpecifier(entry) {
168026
- return typeof entry === "string" ? entry : entry[0];
168027
- }
168028
- function getPluginEntries(config2) {
168029
- const parsed = OpencodeConfigSchema.safeParse(config2);
168030
- if (!parsed.success)
168031
- return [];
168032
- return (parsed.data.plugin ?? []).map(pluginSpecifier).filter(isString);
168033
- }
168034
- function parseJsonConfig(content) {
168035
- try {
168036
- return import_comment_json.parse(content);
168037
- } catch (err) {
168038
- warn(`[auto-update-checker] Failed to parse OpenCode config: ${String(err)}`);
168039
- return null;
168040
- }
168041
- }
168042
- function isPrereleaseVersion(version2) {
168043
- return version2.includes("-");
168044
- }
168045
- function isDistTag(version2) {
168046
- return !/^\d/.test(version2);
168047
- }
168048
- function extractChannel(version2) {
168049
- if (!version2)
168050
- return "latest";
168051
- if (isDistTag(version2))
168052
- return version2;
168053
- if (isPrereleaseVersion(version2)) {
168054
- const prereleasePart = version2.split("-")[1];
168055
- const channelMatch = prereleasePart?.match(/^(alpha|beta|rc|canary|next)/);
168056
- if (channelMatch?.[1])
168057
- return channelMatch[1];
168058
- }
168059
- return "latest";
168060
- }
168061
- function getConfigPaths(directory) {
168062
- return [
168063
- join9(directory, ".opencode", "opencode.json"),
168064
- join9(directory, ".opencode", "opencode.jsonc"),
168065
- USER_OPENCODE_CONFIG,
168066
- USER_OPENCODE_CONFIG_JSONC
168067
- ];
168068
- }
168069
- function resolvePathPluginSpec(spec, configPath) {
168070
- if (spec.startsWith("file://")) {
168071
- try {
168072
- return fileURLToPath(spec);
168073
- } catch {
168074
- return spec.replace(/^file:\/\//, "");
168075
- }
168076
- }
168077
- if (isAbsolute2(spec) || /^[A-Za-z]:[\\/]/.test(spec))
168078
- return spec;
168079
- return resolve3(dirname4(configPath), spec);
168080
- }
168081
- function findPackageJsonUp(startPath) {
168082
- try {
168083
- const stat = statSync(startPath);
168084
- let dir = stat.isDirectory() ? startPath : dirname4(startPath);
168085
- for (let i = 0;i < 10; i++) {
168086
- const pkgPath = join9(dir, "package.json");
168087
- if (existsSync8(pkgPath)) {
168088
- try {
168089
- const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync5(pkgPath, "utf-8")));
168090
- if (pkg.success && pkg.data.name === PACKAGE_NAME)
168091
- return pkgPath;
168092
- } catch {}
168093
- }
168094
- const parent = dirname4(dir);
168095
- if (parent === dir)
168096
- break;
168097
- dir = parent;
168098
- }
168099
- } catch {}
168100
- return null;
168101
- }
168102
- function getLocalDevPath(directory) {
168103
- for (const configPath of getConfigPaths(directory)) {
168104
- try {
168105
- if (!existsSync8(configPath))
168106
- continue;
168107
- const rawConfig = parseJsonConfig(readFileSync5(configPath, "utf-8"));
168108
- const plugins = getPluginEntries(rawConfig);
168109
- for (const entry of plugins) {
168110
- if (entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`))
168111
- continue;
168112
- if (entry.startsWith("file://") || entry.startsWith(".") || isAbsolute2(entry)) {
168113
- const localPath = resolvePathPluginSpec(entry, configPath);
168114
- const pkgPath = findPackageJsonUp(localPath);
168115
- if (!pkgPath)
168116
- continue;
168117
- const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync5(pkgPath, "utf-8")));
168118
- if (pkg.success && pkg.data.name === PACKAGE_NAME)
168119
- return localPath;
168120
- }
168121
- }
168122
- } catch {}
168123
- }
168124
- return null;
168125
- }
168126
- function getLocalDevVersion(directory) {
168127
- const localPath = getLocalDevPath(directory);
168128
- if (!localPath)
168129
- return null;
168130
- try {
168131
- const pkgPath = findPackageJsonUp(localPath);
168132
- if (!pkgPath)
168133
- return null;
168134
- const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync5(pkgPath, "utf-8")));
168135
- return pkg.success ? pkg.data.version ?? null : null;
168136
- } catch {
168137
- return null;
168138
- }
168139
- }
168140
- function getCurrentRuntimePackageJsonPath(currentModuleUrl = import.meta.url) {
168141
- try {
168142
- return findPackageJsonUp(dirname4(fileURLToPath(currentModuleUrl)));
168143
- } catch (err) {
168144
- warn(`[auto-update-checker] Failed to resolve runtime package path: ${String(err)}`);
168145
- return null;
168146
- }
168147
- }
168148
- function findPluginEntry(directory) {
168149
- for (const configPath of getConfigPaths(directory)) {
168150
- try {
168151
- if (!existsSync8(configPath))
168152
- continue;
168153
- const rawConfig = parseJsonConfig(readFileSync5(configPath, "utf-8"));
168154
- const plugins = getPluginEntries(rawConfig);
168155
- for (const entry of plugins) {
168156
- if (entry === PACKAGE_NAME) {
168157
- return { entry, isPinned: false, pinnedVersion: null, configPath };
168158
- }
168159
- if (entry.startsWith(`${PACKAGE_NAME}@`)) {
168160
- const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1);
168161
- const isPinned = pinnedVersion !== "latest";
168162
- return {
168163
- entry,
168164
- isPinned,
168165
- pinnedVersion: isPinned ? pinnedVersion : null,
168166
- configPath
168167
- };
168168
- }
168169
- }
168170
- } catch {}
168171
- }
168172
- return null;
168173
- }
168174
- var cachedPackageVersion = null;
168175
- function getSpecCachePackageJsonPath(spec) {
168176
- return join9(CACHE_DIR, spec, "node_modules", PACKAGE_NAME, "package.json");
168177
- }
168178
- function getCachedVersion(spec) {
168179
- if (!spec && cachedPackageVersion)
168180
- return cachedPackageVersion;
168181
- const candidates = [
168182
- getCurrentRuntimePackageJsonPath(),
168183
- spec ? getSpecCachePackageJsonPath(spec) : null,
168184
- getSpecCachePackageJsonPath(`${PACKAGE_NAME}@latest`),
168185
- join9(homedir5(), ".cache", "opencode", "node_modules", PACKAGE_NAME, "package.json")
168186
- ].filter(isString);
168187
- for (const packageJsonPath of candidates) {
168188
- try {
168189
- if (!existsSync8(packageJsonPath))
168190
- continue;
168191
- const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync5(packageJsonPath, "utf-8")));
168192
- if (pkg.success && pkg.data.version) {
168193
- if (!spec)
168194
- cachedPackageVersion = pkg.data.version;
168195
- return pkg.data.version;
168196
- }
168197
- } catch {}
168198
- }
168199
- return null;
168200
- }
168201
- function buildRegistryUrl(registryUrl) {
168202
- return `${registryUrl.replace(/\/+$/, "")}/${encodeURIComponent(PACKAGE_NAME).replace("%2F", "/")}`;
168203
- }
168204
- async function getLatestVersion(channel = "latest", options = {}) {
168205
- const controller = new AbortController;
168206
- const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs ?? NPM_FETCH_TIMEOUT);
168207
- const abortHandler = () => controller.abort();
168208
- options.signal?.addEventListener("abort", abortHandler, { once: true });
168209
- try {
168210
- if (options.signal?.aborted)
168211
- return null;
168212
- const response = await fetch(buildRegistryUrl(options.registryUrl ?? NPM_REGISTRY_URL), {
168213
- signal: controller.signal,
168214
- headers: { Accept: "application/json" }
168215
- });
168216
- if (!response.ok)
168217
- return null;
168218
- const data = NpmPackageEnvelopeSchema.safeParse(await response.json());
168219
- if (!data.success)
168220
- return null;
168221
- return data.data["dist-tags"][channel] ?? data.data["dist-tags"].latest ?? null;
168222
- } catch {
168223
- return null;
168224
- } finally {
168225
- options.signal?.removeEventListener("abort", abortHandler);
168226
- clearTimeout(timeoutId);
168227
- }
168228
- }
168229
-
168230
- // src/hooks/auto-update-checker/cache.ts
168231
- function warn2(message) {
168232
- log(`WARN: ${message}`);
168233
- }
168234
- function stripPackageNameFromPath(pathValue, packageName) {
168235
- let current = pathValue;
168236
- for (const segment of [...packageName.split("/")].reverse()) {
168237
- if (basename(current) !== segment)
168238
- return null;
168239
- current = dirname5(current);
168240
- }
168241
- return current;
168242
- }
168243
- function removeFromPackageLock(installDir, packageName) {
168244
- const lockPath = join10(installDir, "package-lock.json");
168245
- if (!existsSync9(lockPath))
168246
- return false;
168247
- try {
168248
- const lock = import_comment_json2.parse(readFileSync6(lockPath, "utf-8"));
168249
- let modified = false;
168250
- if (lock.packages) {
168251
- const key = `node_modules/${packageName}`;
168252
- if (lock.packages[key] !== undefined) {
168253
- delete lock.packages[key];
168254
- modified = true;
168255
- }
168256
- }
168257
- if (lock.dependencies?.[packageName]) {
168258
- delete lock.dependencies[packageName];
168259
- modified = true;
168260
- }
168261
- if (modified) {
168262
- writeFileSync3(lockPath, JSON.stringify(lock, null, 2));
168263
- log(`[auto-update-checker] Removed from package-lock.json: ${packageName}`);
168264
- }
168265
- return modified;
168266
- } catch {
168267
- return false;
168268
- }
168269
- }
168270
- function ensureDependencyVersion(packageJsonPath, packageName, version2) {
168271
- if (!existsSync9(packageJsonPath))
168272
- return false;
168273
- try {
168274
- const raw = import_comment_json2.parse(readFileSync6(packageJsonPath, "utf-8"));
168275
- const pkgJson = PackageJsonSchema.safeParse(raw);
168276
- if (!pkgJson.success)
168277
- return false;
168278
- const nextPackageJson = { ...pkgJson.data };
168279
- const dependencies = { ...nextPackageJson.dependencies ?? {} };
168280
- if (dependencies[packageName] === version2)
168281
- return true;
168282
- dependencies[packageName] = version2;
168283
- nextPackageJson.dependencies = dependencies;
168284
- writeFileSync3(packageJsonPath, JSON.stringify(nextPackageJson, null, 2));
168285
- log(`[auto-update-checker] Updated dependency in package.json: ${packageName} → ${version2}`);
168286
- return true;
168287
- } catch (err) {
168288
- warn2(`[auto-update-checker] Failed to update package.json dependency: ${String(err)}`);
168289
- return false;
168290
- }
168291
- }
168292
- function removeInstalledPackage(installDir, packageName) {
168293
- const packageDir = join10(installDir, "node_modules", packageName);
168294
- if (!existsSync9(packageDir))
168295
- return false;
168296
- rmSync(packageDir, { recursive: true, force: true });
168297
- log(`[auto-update-checker] Package removed: ${packageDir}`);
168298
- return true;
168299
- }
168300
- function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
168301
- if (runtimePackageJsonPath) {
168302
- const packageDir = dirname5(runtimePackageJsonPath);
168303
- const nodeModulesDir = stripPackageNameFromPath(packageDir, PACKAGE_NAME);
168304
- if (nodeModulesDir && basename(nodeModulesDir) === "node_modules") {
168305
- const installDir = dirname5(nodeModulesDir);
168306
- const packageJsonPath = join10(installDir, "package.json");
168307
- if (!existsSync9(packageJsonPath)) {
168308
- try {
168309
- writeFileSync3(packageJsonPath, `${JSON.stringify({ private: true, dependencies: {} }, null, 2)}
168310
- `);
168311
- log(`[auto-update-checker] Seeded missing package.json at ${packageJsonPath} (issue #73)`);
168312
- } catch (err) {
168313
- warn2(`[auto-update-checker] Could not seed package.json at ${packageJsonPath}: ${String(err)}`);
168314
- return null;
168315
- }
168316
- }
168317
- return { installDir, packageJsonPath };
168318
- }
168319
- return null;
168320
- }
168321
- const legacyPackageJsonPath = join10(dirname5(CACHE_DIR), "package.json");
168322
- if (existsSync9(legacyPackageJsonPath)) {
168323
- return { installDir: dirname5(CACHE_DIR), packageJsonPath: legacyPackageJsonPath };
168324
- }
168325
- return null;
168326
- }
168327
- function preparePackageUpdate(version2, packageName = PACKAGE_NAME, runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
168328
- try {
168329
- const installContext = resolveInstallContext(runtimePackageJsonPath);
168330
- if (!installContext) {
168331
- warn2("[auto-update-checker] No install context found for auto-update");
168332
- return null;
168333
- }
168334
- if (!ensureDependencyVersion(installContext.packageJsonPath, packageName, version2))
168335
- return null;
168336
- const packageRemoved = removeInstalledPackage(installContext.installDir, packageName);
168337
- const lockRemoved = removeFromPackageLock(installContext.installDir, packageName);
168338
- if (!packageRemoved && !lockRemoved) {
168339
- log(`[auto-update-checker] No cached package artifacts removed for ${packageName}; continuing with updated dependency spec`);
168340
- }
168341
- return installContext.installDir;
168342
- } catch (err) {
168343
- warn2(`[auto-update-checker] Failed to prepare package update: ${String(err)}`);
168344
- return null;
168345
- }
168346
- }
168347
- async function runNpmInstallSafe(installDir, options = {}) {
168348
- let timeout = null;
168349
- try {
168350
- if (options.signal?.aborted)
168351
- return false;
168352
- const proc = spawn("npm", ["install", "--no-audit", "--no-fund", "--no-progress"], {
168353
- cwd: installDir,
168354
- stdio: "pipe"
168355
- });
168356
- const abortProcess = () => {
168357
- try {
168358
- proc.kill();
168359
- } catch {}
168360
- };
168361
- options.signal?.addEventListener("abort", abortProcess, { once: true });
168362
- const exitPromise = new Promise((resolveExit) => {
168363
- proc.on("error", () => resolveExit(false));
168364
- proc.on("exit", (code) => resolveExit(code === 0));
168365
- });
168366
- const timeoutPromise = new Promise((resolveTimeout) => {
168367
- timeout = setTimeout(() => resolveTimeout("timeout"), options.timeoutMs ?? 60000);
168368
- });
168369
- const result = await Promise.race([exitPromise, timeoutPromise]);
168370
- options.signal?.removeEventListener("abort", abortProcess);
168371
- if (result === "timeout" || options.signal?.aborted) {
168372
- abortProcess();
168373
- return false;
168374
- }
168375
- return result;
168376
- } catch (err) {
168377
- warn2(`[auto-update-checker] npm install error: ${String(err)}`);
168378
- return false;
168379
- } finally {
168380
- if (timeout)
168381
- clearTimeout(timeout);
168382
- }
168383
- }
168384
-
168385
- // src/hooks/auto-update-checker/index.ts
168386
- var DEFAULT_CHECK_INTERVAL_MS = 60 * 60 * 1000;
168387
- var DEFAULT_INIT_DELAY_MS = 5000;
168388
- var TIMESTAMP_FILENAME = "last-update-check.json";
168389
- function warn3(message) {
168390
- log(`WARN: ${message}`);
168391
- }
168392
- function createAutoUpdateCheckerHook(ctx, options = {}) {
168393
- const {
168394
- enabled = true,
168395
- showStartupToast = true,
168396
- autoUpdate = true,
168397
- npmRegistryUrl = NPM_REGISTRY_URL,
168398
- fetchTimeoutMs = NPM_FETCH_TIMEOUT,
168399
- signal = new AbortController().signal,
168400
- storageDir = null,
168401
- checkIntervalMs = DEFAULT_CHECK_INTERVAL_MS,
168402
- initDelayMs = DEFAULT_INIT_DELAY_MS
168403
- } = options;
168404
- if (!enabled) {
168405
- return async (_input) => {};
168406
- }
168407
- const initTimer = setTimeout(() => {
168408
- maybeRunCheck(ctx, {
168409
- showStartupToast,
168410
- autoUpdate,
168411
- npmRegistryUrl,
168412
- fetchTimeoutMs,
168413
- signal,
168414
- storageDir,
168415
- checkIntervalMs,
168416
- initDelayMs
168417
- }).catch((err) => {
168418
- warn3(`[auto-update-checker] Background update check failed: ${String(err)}`);
168419
- });
168420
- }, initDelayMs);
168421
- if (typeof initTimer === "object" && initTimer !== null && "unref" in initTimer) {
168422
- initTimer.unref();
168423
- }
168424
- signal.addEventListener("abort", () => {
168425
- clearTimeout(initTimer);
168426
- }, { once: true });
168427
- return async (_input) => {};
168428
- }
168429
- async function maybeRunCheck(ctx, options) {
168430
- if (options.signal.aborted)
168431
- return;
168432
- if (!claimCheckSlot(options.storageDir, options.checkIntervalMs)) {
168433
- log("[auto-update-checker] Skipping check (another instance ran one recently)");
168434
- return;
168435
- }
168436
- await runStartupCheck(ctx, options);
168437
- }
168438
- function claimCheckSlot(storageDir, intervalMs) {
168439
- if (!storageDir)
168440
- return true;
168441
- try {
168442
- const file2 = join11(storageDir, TIMESTAMP_FILENAME);
168443
- if (existsSync10(file2)) {
168444
- try {
168445
- const raw = JSON.parse(readFileSync7(file2, "utf-8"));
168446
- const last = typeof raw.lastCheckedMs === "number" ? raw.lastCheckedMs : 0;
168447
- if (Number.isFinite(last) && Date.now() - last < intervalMs) {
168448
- return false;
168449
- }
168450
- } catch {}
168451
- }
168452
- mkdirSync4(dirname6(file2), { recursive: true });
168453
- const tmp = `${file2}.tmp.${process.pid}`;
168454
- writeFileSync4(tmp, JSON.stringify({ lastCheckedMs: Date.now() }), "utf-8");
168455
- renameSync(tmp, file2);
168456
- return true;
168457
- } catch (err) {
168458
- warn3(`[auto-update-checker] Could not coordinate via timestamp file: ${String(err)}`);
168459
- return true;
168460
- }
168461
- }
168462
- async function runStartupCheck(ctx, options) {
168463
- if (options.signal.aborted)
168464
- return;
168465
- const cachedVersion = getCachedVersion();
168466
- const localDevVersion = getLocalDevVersion(ctx.directory);
168467
- const displayVersion = localDevVersion ?? cachedVersion;
168468
- if (localDevVersion) {
168469
- if (options.showStartupToast) {
168470
- showToast(ctx, `Magic Context ${displayVersion} (dev)`, "Running in local development mode.", "info");
168471
- }
168472
- log("[auto-update-checker] Local development mode");
168473
- return;
168474
- }
168475
- if (options.showStartupToast) {
168476
- showToast(ctx, `Magic Context ${displayVersion ?? "unknown"}`, "@wolfx/opencode-magic-context is active.", "info");
168477
- }
168478
- await runBackgroundUpdateCheck(ctx, options);
168479
- }
168480
- async function runBackgroundUpdateCheck(ctx, options) {
168481
- if (options.signal.aborted)
168482
- return;
168483
- const pluginInfo = findPluginEntry(ctx.directory);
168484
- if (!pluginInfo) {
168485
- log("[auto-update-checker] Plugin not found in config");
168486
- return;
168487
- }
168488
- const cachedVersion = getCachedVersion(pluginInfo.entry);
168489
- const currentVersion = cachedVersion ?? pluginInfo.pinnedVersion;
168490
- if (!currentVersion) {
168491
- log("[auto-update-checker] No version found (cached or pinned)");
168492
- return;
168493
- }
168494
- const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion);
168495
- const latestVersion = await getLatestVersion(channel, {
168496
- registryUrl: options.npmRegistryUrl,
168497
- timeoutMs: options.fetchTimeoutMs,
168498
- signal: options.signal
168499
- });
168500
- if (!latestVersion) {
168501
- warn3(`[auto-update-checker] Failed to fetch latest version for channel: ${channel}`);
168502
- showToast(ctx, "Magic Context update check failed", "Could not check npm for @wolfx/opencode-magic-context updates. Continuing with the cached version.", "warning", 8000);
168503
- return;
168504
- }
168505
- if (currentVersion === latestVersion) {
168506
- log(`[auto-update-checker] Already on latest version for channel: ${channel}`);
168507
- return;
168508
- }
168509
- log(`[auto-update-checker] Update available (${channel}): ${currentVersion} → ${latestVersion}`);
168510
- if (pluginInfo.isPinned) {
168511
- showToast(ctx, `Magic Context ${latestVersion}`, `v${latestVersion} available. Version is pinned; update your OpenCode plugin config to upgrade.`, "info", 8000);
168512
- log("[auto-update-checker] Version is pinned; skipping auto-update");
168513
- return;
168514
- }
168515
- if (!options.autoUpdate) {
168516
- showToast(ctx, `Magic Context ${latestVersion}`, `v${latestVersion} available. Auto-update is disabled.`, "info", 8000);
168517
- log("[auto-update-checker] Auto-update disabled, notification only");
168518
- return;
168519
- }
168520
- const installDir = preparePackageUpdate(latestVersion, PACKAGE_NAME);
168521
- if (!installDir) {
168522
- showToast(ctx, `Magic Context ${latestVersion}`, `v${latestVersion} available. Auto-update could not prepare the active install.`, "warning", 8000);
168523
- warn3("[auto-update-checker] Failed to prepare install root for auto-update");
168524
- return;
168525
- }
168526
- const installSuccess = await runNpmInstallSafe(installDir, { signal: options.signal });
168527
- if (installSuccess) {
168528
- showToast(ctx, "Magic Context Updated!", `v${currentVersion} → v${latestVersion}
168529
- Restart OpenCode to apply.`, "success", 8000);
168530
- log(`[auto-update-checker] Update installed: ${currentVersion} → ${latestVersion}`);
168531
- return;
168532
- }
168533
- showToast(ctx, `Magic Context ${latestVersion}`, `v${latestVersion} available, but auto-update failed to install it. Check logs or retry manually.`, "error", 8000);
168534
- warn3("[auto-update-checker] npm install failed; update not installed");
168535
- }
168536
- function showToast(ctx, title, message, variant = "info", duration3 = 3000) {
168537
- ctx.client.tui.showToast({ body: { title, message, variant, duration: duration3 } }).catch(() => {});
168538
- }
168539
-
168540
- // src/index.ts
168541
167944
  init_compartment_prompt();
168542
167945
 
168543
167946
  // src/hooks/magic-context/live-session-state.ts
@@ -168739,27 +168142,27 @@ init_assistant_message_extractor();
168739
168142
  init_data_path();
168740
168143
  init_logger();
168741
168144
  await init_sqlite();
168742
- import { existsSync as existsSync13 } from "node:fs";
168743
- import { join as join17 } from "node:path";
168145
+ import { existsSync as existsSync10 } from "node:fs";
168146
+ import { join as join13 } from "node:path";
168744
168147
 
168745
168148
  // src/features/magic-context/key-files/identify-key-files.ts
168746
168149
  init_read_session_formatting();
168747
168150
  init_shared();
168748
168151
  init_assistant_message_extractor();
168749
168152
  init_logger();
168750
- import { readFileSync as readFileSync10 } from "node:fs";
168751
- import { join as join16 } from "node:path";
168153
+ import { readFileSync as readFileSync7 } from "node:fs";
168154
+ import { join as join12 } from "node:path";
168752
168155
 
168753
168156
  // src/features/magic-context/key-files/aft-availability.ts
168754
- var import_comment_json3 = __toESM(require_src2(), 1);
168755
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
168756
- import { homedir as homedir8 } from "node:os";
168757
- import { join as join15 } from "node:path";
168157
+ var import_comment_json = __toESM(require_src2(), 1);
168158
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
168159
+ import { homedir as homedir6 } from "node:os";
168160
+ import { join as join11 } from "node:path";
168758
168161
  var overrideAvailability = null;
168759
168162
  function parseConfig(path5) {
168760
- if (!existsSync12(path5))
168163
+ if (!existsSync9(path5))
168761
168164
  return null;
168762
- return import_comment_json3.parse(readFileSync9(path5, "utf-8"));
168165
+ return import_comment_json.parse(readFileSync6(path5, "utf-8"));
168763
168166
  }
168764
168167
  function entryMatchesAft(entry) {
168765
168168
  const value = Array.isArray(entry) ? entry[0] : entry;
@@ -168779,12 +168182,12 @@ function hasAftAtKeys(value, keys) {
168779
168182
  return false;
168780
168183
  }
168781
168184
  function getAftAvailability() {
168782
- const home = process.env.HOME || homedir8();
168185
+ const home = process.env.HOME || homedir6();
168783
168186
  const opencodePaths = [
168784
- join15(home, ".config", "opencode", "opencode.jsonc"),
168785
- join15(home, ".config", "opencode", "opencode.json")
168187
+ join11(home, ".config", "opencode", "opencode.jsonc"),
168188
+ join11(home, ".config", "opencode", "opencode.json")
168786
168189
  ];
168787
- const piPaths = [join15(home, ".pi", "agent", "settings.json")];
168190
+ const piPaths = [join11(home, ".pi", "agent", "settings.json")];
168788
168191
  const checkedPaths = [...opencodePaths, ...piPaths];
168789
168192
  let opencode = false;
168790
168193
  for (const path5 of opencodePaths) {
@@ -168828,7 +168231,7 @@ init_project_key_files();
168828
168231
 
168829
168232
  // src/features/magic-context/key-files/read-history.ts
168830
168233
  import { realpathSync as realpathSync2 } from "node:fs";
168831
- import { relative, resolve as resolve5 } from "node:path";
168234
+ import { relative, resolve as resolve4 } from "node:path";
168832
168235
  function toMs(value) {
168833
168236
  if (typeof value === "number" && Number.isFinite(value))
168834
168237
  return value;
@@ -168865,7 +168268,7 @@ function coalesceRanges(ranges) {
168865
168268
  }
168866
168269
  function normalizeProjectRelativePath(projectPath, filePath) {
168867
168270
  const root = realpathSync2(projectPath);
168868
- const abs = filePath.startsWith("/") ? resolve5(filePath) : resolve5(root, filePath);
168271
+ const abs = filePath.startsWith("/") ? resolve4(filePath) : resolve4(root, filePath);
168869
168272
  let real = abs;
168870
168273
  try {
168871
168274
  real = realpathSync2(abs);
@@ -169251,7 +168654,7 @@ async function runKeyFilesTask(args) {
169251
168654
  if (row.staleReason !== null || row.generationConfigHash !== configHash)
169252
168655
  return false;
169253
168656
  try {
169254
- return sha256(readFileSync10(join16(projectPath, row.path))) === row.contentHash;
168657
+ return sha256(readFileSync7(join12(projectPath, row.path))) === row.contentHash;
169255
168658
  } catch {
169256
168659
  return false;
169257
168660
  }
@@ -169558,11 +168961,11 @@ function logWithStackHead(message, stackHead) {
169558
168961
  log(message, stackHead ? { stackHead } : undefined);
169559
168962
  }
169560
168963
  function getOpenCodeDbPath2() {
169561
- return join17(getDataDir(), "opencode", "opencode.db");
168964
+ return join13(getDataDir(), "opencode", "opencode.db");
169562
168965
  }
169563
168966
  function openOpenCodeDb() {
169564
168967
  const dbPath = getOpenCodeDbPath2();
169565
- if (!existsSync13(dbPath)) {
168968
+ if (!existsSync10(dbPath)) {
169566
168969
  log(`[key-files] OpenCode DB not found at ${dbPath} — skipping`);
169567
168970
  return null;
169568
168971
  }
@@ -169711,8 +169114,8 @@ async function runDream(args) {
169711
169114
  try {
169712
169115
  const docsDir = args.sessionDirectory ?? args.projectIdentity;
169713
169116
  const existingDocs = taskName === "maintain-docs" ? {
169714
- architecture: existsSync13(join17(docsDir, "ARCHITECTURE.md")),
169715
- structure: existsSync13(join17(docsDir, "STRUCTURE.md"))
169117
+ architecture: existsSync10(join13(docsDir, "ARCHITECTURE.md")),
169118
+ structure: existsSync10(join13(docsDir, "STRUCTURE.md"))
169716
169119
  } : undefined;
169717
169120
  const userMemories = taskName === "archive-stale" ? getActiveUserMemories(args.db).map((um) => ({
169718
169121
  id: um.id,
@@ -170968,7 +170371,6 @@ async function ensureProjectRegisteredFromOpenCodeDirectory(directory, db) {
170968
170371
  // src/plugin/event.ts
170969
170372
  function createEventHandler(args) {
170970
170373
  return async (input) => {
170971
- await args.autoUpdateChecker?.(input);
170972
170374
  await args.magicContext?.event?.(input);
170973
170375
  };
170974
170376
  }
@@ -173181,7 +172583,7 @@ async function runCompartmentPhase(args) {
173181
172583
  async function awaitCompartmentRun(activeRun, reason) {
173182
172584
  sessionLog(args.sessionId, reason);
173183
172585
  const timeoutMs = args.historianTimeoutMs ?? 120000;
173184
- const timeout = new Promise((resolve6) => setTimeout(() => resolve6("timeout"), timeoutMs));
172586
+ const timeout = new Promise((resolve5) => setTimeout(() => resolve5("timeout"), timeoutMs));
173185
172587
  const result = await Promise.race([activeRun.promise.then(() => "done"), timeout]);
173186
172588
  if (result === "timeout") {
173187
172589
  sessionLog(args.sessionId, `transform: compartment await timed out after ${timeoutMs}ms — proceeding without waiting`);
@@ -174632,10 +174034,10 @@ var AUTO_SEARCH_TIMEOUT_MS = 3000;
174632
174034
  async function unifiedSearchWithTimeout(db, sessionId, projectPath, prompt, options, timeoutMs) {
174633
174035
  const controller = new AbortController;
174634
174036
  let timer;
174635
- const timeoutPromise = new Promise((resolve6) => {
174037
+ const timeoutPromise = new Promise((resolve5) => {
174636
174038
  timer = setTimeout(() => {
174637
174039
  controller.abort();
174638
- resolve6(null);
174040
+ resolve5(null);
174639
174041
  }, timeoutMs);
174640
174042
  });
174641
174043
  try {
@@ -176697,11 +176099,24 @@ function estimateProjectedPercentage(db, sessionId, contextUsage, preloadedTags)
176697
176099
  await init_read_session_db();
176698
176100
 
176699
176101
  // src/hooks/magic-context/text-complete.ts
176700
- var LEADING_TAG_PREFIX_REGEX = /^(\u00a7\d+\u00a7\s*)+/;
176701
- var SECTION_CHAR_REGEX = /\u00a7/g;
176702
- function createTextCompleteHandler() {
176102
+ var DEFAULT_PATTERNS = [
176103
+ /^(\u00a7\d+\u00a7\s*)+/,
176104
+ /\u00a7/g
176105
+ ];
176106
+ function createTextCompleteHandler(stripConfig) {
176107
+ const isEnabled = stripConfig?.enabled ?? true;
176108
+ let patterns;
176109
+ if (!isEnabled) {
176110
+ patterns = [];
176111
+ } else if (stripConfig?.patterns && stripConfig.patterns.length > 0) {
176112
+ patterns = stripConfig.patterns.map((src) => new RegExp(src));
176113
+ } else {
176114
+ patterns = DEFAULT_PATTERNS;
176115
+ }
176703
176116
  return async (_input, output) => {
176704
- output.text = output.text.replace(LEADING_TAG_PREFIX_REGEX, "").replace(SECTION_CHAR_REGEX, "");
176117
+ for (const regex of patterns) {
176118
+ output.text = output.text.replace(regex, "");
176119
+ }
176705
176120
  };
176706
176121
  }
176707
176122
 
@@ -176924,8 +176339,8 @@ init_send_session_notification();
176924
176339
 
176925
176340
  // src/hooks/magic-context/system-prompt-hash.ts
176926
176341
  import { createHash as createHash9 } from "node:crypto";
176927
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
176928
- import { join as join25 } from "node:path";
176342
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
176343
+ import { join as join21 } from "node:path";
176929
176344
 
176930
176345
  // src/agents/magic-context-prompt.ts
176931
176346
  function getToolHistoryGuidance(dropToolStructure) {
@@ -177021,8 +176436,8 @@ await init_storage();
177021
176436
 
177022
176437
  // src/hooks/magic-context/key-files-block.ts
177023
176438
  init_compartment_storage();
177024
- import { readFileSync as readFileSync12, realpathSync as realpathSync3 } from "node:fs";
177025
- import { join as join24, sep as sep2 } from "node:path";
176439
+ import { readFileSync as readFileSync9, realpathSync as realpathSync3 } from "node:fs";
176440
+ import { join as join20, sep as sep2 } from "node:path";
177026
176441
  init_project_key_files();
177027
176442
  init_logger();
177028
176443
  var cachedKeyFilesBySession = new Map;
@@ -177076,13 +176491,13 @@ function buildKeyFilesBlock(db, projectPath, config2 = { enabled: true, tokenBud
177076
176491
  let nextStale = null;
177077
176492
  let observed = false;
177078
176493
  try {
177079
- const absPath = join24(projectPath, row.path);
176494
+ const absPath = join20(projectPath, row.path);
177080
176495
  const real = realpathSync3(absPath);
177081
176496
  if (!isUnderProject(projectPath, real)) {
177082
176497
  nextStale = "missing";
177083
176498
  observed = true;
177084
176499
  } else {
177085
- const diskHash = sha256(readFileSync12(real));
176500
+ const diskHash = sha256(readFileSync9(real));
177086
176501
  if (diskHash !== row.contentHash)
177087
176502
  nextStale = "content_drift";
177088
176503
  observed = true;
@@ -177166,10 +176581,10 @@ var DOC_FILES = ["ARCHITECTURE.md", "STRUCTURE.md"];
177166
176581
  function readProjectDocs(directory) {
177167
176582
  const sections = [];
177168
176583
  for (const filename of DOC_FILES) {
177169
- const filePath = join25(directory, filename);
176584
+ const filePath = join21(directory, filename);
177170
176585
  try {
177171
- if (existsSync15(filePath)) {
177172
- const content = readFileSync13(filePath, "utf-8").trim();
176586
+ if (existsSync12(filePath)) {
176587
+ const content = readFileSync10(filePath, "utf-8").trim();
177173
176588
  if (content.length > 0) {
177174
176589
  sections.push(`<${filename}>
177175
176590
  ${escapeXmlContent(content)}
@@ -177676,7 +177091,7 @@ function createMagicContextHook(deps) {
177676
177091
  return {
177677
177092
  "experimental.chat.messages.transform": transform2,
177678
177093
  "experimental.chat.system.transform": systemPromptHashHandler,
177679
- "experimental.text.complete": createTextCompleteHandler(),
177094
+ "experimental.text.complete": createTextCompleteHandler(deps.config.experimental?.text_complete_strip),
177680
177095
  "chat.message": createChatMessageHook({
177681
177096
  db,
177682
177097
  toolUsageSinceUserTurn,
@@ -179392,28 +178807,28 @@ init_models_dev_cache();
179392
178807
  // src/shared/rpc-server.ts
179393
178808
  init_logger();
179394
178809
  import {
179395
- mkdirSync as mkdirSync9,
178810
+ mkdirSync as mkdirSync8,
179396
178811
  readdirSync,
179397
- readFileSync as readFileSync15,
179398
- renameSync as renameSync2,
178812
+ readFileSync as readFileSync12,
178813
+ renameSync,
179399
178814
  unlinkSync as unlinkSync3,
179400
- writeFileSync as writeFileSync8
178815
+ writeFileSync as writeFileSync5
179401
178816
  } from "node:fs";
179402
178817
  import { createServer } from "node:http";
179403
- import { dirname as dirname8 } from "node:path";
178818
+ import { dirname as dirname5 } from "node:path";
179404
178819
 
179405
178820
  // src/shared/rpc-utils.ts
179406
178821
  import { createHash as createHash10 } from "node:crypto";
179407
- import { join as join27 } from "node:path";
178822
+ import { join as join23 } from "node:path";
179408
178823
  function projectHash(directory) {
179409
178824
  const normalized = directory.replace(/\/+$/, "");
179410
178825
  return createHash10("sha256").update(normalized).digest("hex").slice(0, 16);
179411
178826
  }
179412
178827
  function rpcPortDir(storageDir, directory) {
179413
- return join27(storageDir, "rpc", projectHash(directory));
178828
+ return join23(storageDir, "rpc", projectHash(directory));
179414
178829
  }
179415
178830
  function rpcPortFilePath(storageDir, directory, pid = process.pid) {
179416
- return join27(rpcPortDir(storageDir, directory), `port-${pid}.json`);
178831
+ return join23(rpcPortDir(storageDir, directory), `port-${pid}.json`);
179417
178832
  }
179418
178833
  function isPidAlive(pid) {
179419
178834
  if (!Number.isInteger(pid) || pid <= 0)
@@ -179471,7 +178886,7 @@ class MagicContextRpcServer {
179471
178886
  this.handlers.set(method, handler);
179472
178887
  }
179473
178888
  async start() {
179474
- return new Promise((resolve6, reject) => {
178889
+ return new Promise((resolve5, reject) => {
179475
178890
  const server = createServer((req, res) => this.dispatch(req, res));
179476
178891
  server.on("error", (err) => {
179477
178892
  log(`[rpc] server error: ${err.message}`);
@@ -179487,20 +178902,20 @@ class MagicContextRpcServer {
179487
178902
  this.server = server;
179488
178903
  try {
179489
178904
  this.warnIfOtherLiveInstance();
179490
- const dir = dirname8(this.portFilePath);
179491
- mkdirSync9(dir, { recursive: true });
178905
+ const dir = dirname5(this.portFilePath);
178906
+ mkdirSync8(dir, { recursive: true });
179492
178907
  const tmpPath = `${this.portFilePath}.tmp`;
179493
- writeFileSync8(tmpPath, JSON.stringify({
178908
+ writeFileSync5(tmpPath, JSON.stringify({
179494
178909
  port: this.port,
179495
178910
  pid: process.pid,
179496
178911
  started_at: this.startedAt
179497
178912
  }), "utf-8");
179498
- renameSync2(tmpPath, this.portFilePath);
178913
+ renameSync(tmpPath, this.portFilePath);
179499
178914
  log(`[rpc] server listening on 127.0.0.1:${this.port}`);
179500
178915
  } catch (err) {
179501
178916
  log(`[rpc] failed to write port file: ${err}`);
179502
178917
  }
179503
- resolve6(this.port);
178918
+ resolve5(this.port);
179504
178919
  });
179505
178920
  server.unref();
179506
178921
  });
@@ -179510,7 +178925,7 @@ class MagicContextRpcServer {
179510
178925
  for (const entry of readdirSync(this.portDir)) {
179511
178926
  if (!entry.startsWith("port-") || !entry.endsWith(".json"))
179512
178927
  continue;
179513
- const record2 = parseRpcPortFile(readFileSync15(`${this.portDir}/${entry}`, "utf-8"));
178928
+ const record2 = parseRpcPortFile(readFileSync12(`${this.portDir}/${entry}`, "utf-8"));
179514
178929
  if (!record2 || record2.pid === process.pid || !isPidAlive(record2.pid))
179515
178930
  continue;
179516
178931
  log(`[rpc] another Magic Context RPC server is active for this project (pid ${record2.pid}, port ${record2.port}); starting separate instance on a new port`);
@@ -179582,10 +178997,6 @@ class MagicContextRpcServer {
179582
178997
  // src/index.ts
179583
178998
  var plugin = async (ctx) => {
179584
178999
  const pluginConfig = loadPluginConfig(ctx.directory);
179585
- const autoUpdateAbort = new AbortController;
179586
- process.once("exit", () => {
179587
- autoUpdateAbort.abort();
179588
- });
179589
179000
  if (pluginConfig.configWarnings?.length) {
179590
179001
  for (const w of pluginConfig.configWarnings) {
179591
179002
  log(`[magic-context] config warning: ${w}`);
@@ -179699,12 +179110,7 @@ var plugin = async (ctx) => {
179699
179110
  return {
179700
179111
  tool: tools5,
179701
179112
  event: createEventHandler({
179702
- magicContext: hooks.magicContext,
179703
- autoUpdateChecker: createAutoUpdateCheckerHook(ctx, {
179704
- autoUpdate: pluginConfig.auto_update !== false,
179705
- signal: autoUpdateAbort.signal,
179706
- storageDir
179707
- })
179113
+ magicContext: hooks.magicContext
179708
179114
  }),
179709
179115
  "experimental.chat.messages.transform": createMessagesTransformHandler({
179710
179116
  magicContext: hooks.magicContext