@wolfx/opencode-magic-context 0.21.8 → 0.22.1-patch.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/agents/permissions.d.ts +29 -14
- package/dist/agents/permissions.d.ts.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/migrate-experimental.d.ts +30 -0
- package/dist/config/migrate-experimental.d.ts.map +1 -0
- package/dist/config/schema/agent-overrides.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +110 -109
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
- package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-events.d.ts +50 -0
- package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-storage.d.ts +22 -0
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
- package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
- package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
- package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
- package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
- package/dist/features/magic-context/memory/constants.d.ts +4 -0
- package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts +6 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-probe.d.ts +5 -0
- package/dist/features/magic-context/memory/embedding-probe.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/index.d.ts +1 -1
- package/dist/features/magic-context/memory/index.d.ts.map +1 -1
- package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
- package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
- package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts +15 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/memory/types.d.ts +3 -1
- package/dist/features/magic-context/memory/types.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +7 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
- package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
- package/dist/features/magic-context/project-identity.d.ts +2 -0
- package/dist/features/magic-context/project-identity.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +51 -7
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
- package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
- package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
- package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
- package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +44 -0
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -0
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-project-state.d.ts +19 -0
- package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
- package/dist/features/magic-context/storage-subagent-invocations.d.ts +9 -0
- package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +21 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
- package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
- package/dist/features/magic-context/storage.d.ts +12 -3
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
- package/dist/features/magic-context/tagger.d.ts +15 -1
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/types.d.ts +21 -0
- package/dist/features/magic-context/types.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
- package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
- package/dist/features/magic-context/work-metrics.d.ts +66 -0
- package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +13 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
- package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +6 -2
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +67 -4
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
- package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
- package/dist/hooks/magic-context/decay-render.d.ts +67 -0
- package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
- package/dist/hooks/magic-context/historian-state-file.d.ts +4 -4
- package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +12 -20
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
- package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
- package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
- package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
- package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
- package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
- package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
- package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
- package/dist/hooks/magic-context/send-session-notification.d.ts +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +5 -6
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/text-complete.d.ts +41 -1
- package/dist/hooks/magic-context/text-complete.d.ts.map +1 -1
- package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
- package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +9 -7
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
- package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10025 -5179
- package/dist/plugin/conflict-warning-hook.d.ts +13 -0
- package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/event.d.ts +10 -3
- package/dist/plugin/event.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +17 -1
- package/dist/shared/announcement.d.ts.map +1 -1
- package/dist/shared/data-path.d.ts +9 -11
- package/dist/shared/data-path.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +1 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +27 -5
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +1 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +30 -2
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +9 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-helpers.d.ts +7 -7
- package/dist/shared/sqlite.d.ts +23 -14
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts +10 -1
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/tools/ctx-expand/tools.d.ts +5 -1
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts +16 -1
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/shared/announcement.test.ts +23 -7
- package/src/shared/announcement.ts +30 -8
- package/src/shared/conflict-detector.test.ts +19 -6
- package/src/shared/conflict-detector.ts +1 -1
- package/src/shared/conflict-fixer.test.ts +7 -3
- package/src/shared/data-path.test.ts +9 -10
- package/src/shared/data-path.ts +10 -12
- package/src/shared/models-dev-cache.test.ts +72 -4
- package/src/shared/models-dev-cache.ts +47 -8
- package/src/shared/opencode-compaction-detector.test.ts +10 -2
- package/src/shared/rpc-client.test.ts +54 -3
- package/src/shared/rpc-client.ts +19 -9
- package/src/shared/rpc-notifications.test.ts +54 -1
- package/src/shared/rpc-notifications.ts +82 -13
- package/src/shared/rpc-server.ts +33 -4
- package/src/shared/rpc-types.ts +30 -2
- package/src/shared/rpc-utils.ts +10 -0
- package/src/shared/sqlite-helpers.ts +9 -9
- package/src/shared/sqlite.ts +99 -80
- package/src/shared/tag-transcript.test.ts +280 -0
- package/src/shared/tag-transcript.ts +162 -33
- package/src/shared/tui-config.ts +2 -2
- package/src/tui/data/context-db.ts +75 -11
- package/src/tui/index.tsx +227 -36
- package/src/tui/slots/sidebar-content.tsx +368 -36
- package/dist/hooks/auto-update-checker/cache.d.ts +0 -23
- package/dist/hooks/auto-update-checker/cache.d.ts.map +0 -1
- package/dist/hooks/auto-update-checker/checker.d.ts +0 -13
- package/dist/hooks/auto-update-checker/checker.d.ts.map +0 -1
- package/dist/hooks/auto-update-checker/constants.d.ts +0 -10
- package/dist/hooks/auto-update-checker/constants.d.ts.map +0 -1
- package/dist/hooks/auto-update-checker/index.d.ts +0 -40
- package/dist/hooks/auto-update-checker/index.d.ts.map +0 -1
- package/dist/hooks/auto-update-checker/types.d.ts +0 -50
- package/dist/hooks/auto-update-checker/types.d.ts.map +0 -1
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
- package/dist/shared/native-binding.d.ts +0 -87
- package/dist/shared/native-binding.d.ts.map +0 -1
- package/src/shared/native-binding.ts +0 -311
|
@@ -8,8 +8,11 @@ import * as path from "node:path";
|
|
|
8
8
|
* `getMagicContextStorageDir()`. The behavior we test:
|
|
9
9
|
* 1. `markAnnouncementSeen` then `readLastAnnouncedVersion` round-trips
|
|
10
10
|
* 2. `shouldShowAnnouncement` returns false after a matching mark
|
|
11
|
-
* 3. `shouldShowAnnouncement` returns true after a non-matching mark
|
|
12
|
-
* 4.
|
|
11
|
+
* 3. `shouldShowAnnouncement` returns true after a non-matching (older) mark
|
|
12
|
+
* 4. `shouldShowAnnouncement` seeds state + returns false on first run / wiped
|
|
13
|
+
* sandbox (no prior file), so fresh installs and ephemeral envs aren't
|
|
14
|
+
* spammed with a changelog (issue #99)
|
|
15
|
+
* 5. Empty-version inputs are no-ops (don't crash, don't write garbage)
|
|
13
16
|
*
|
|
14
17
|
* We isolate writes by pointing `XDG_DATA_HOME` at a temp dir before requiring
|
|
15
18
|
* the module fresh per test, since the module captures the storage path at
|
|
@@ -32,7 +35,8 @@ afterEach(() => {
|
|
|
32
35
|
process.env.XDG_DATA_HOME = originalXdg;
|
|
33
36
|
}
|
|
34
37
|
try {
|
|
35
|
-
|
|
38
|
+
// maxRetries/retryDelay ride out transient EBUSY/EPERM on Windows.
|
|
39
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
36
40
|
} catch {
|
|
37
41
|
// best-effort
|
|
38
42
|
}
|
|
@@ -110,9 +114,14 @@ describe("shouldShowAnnouncement gating", () => {
|
|
|
110
114
|
expect(shouldShowAnnouncement()).toBe(false);
|
|
111
115
|
});
|
|
112
116
|
|
|
113
|
-
test("returns
|
|
117
|
+
test("seeds state and returns false on first run / wiped sandbox (issue #99)", async () => {
|
|
114
118
|
const mod = await import(`./announcement?t=${Date.now()}-none`);
|
|
115
|
-
const {
|
|
119
|
+
const {
|
|
120
|
+
ANNOUNCEMENT_VERSION,
|
|
121
|
+
ANNOUNCEMENT_FEATURES,
|
|
122
|
+
shouldShowAnnouncement,
|
|
123
|
+
readLastAnnouncedVersion,
|
|
124
|
+
} = mod;
|
|
116
125
|
|
|
117
126
|
if (!ANNOUNCEMENT_VERSION || ANNOUNCEMENT_FEATURES.length === 0) {
|
|
118
127
|
// When empty, the gate is always false regardless of state
|
|
@@ -120,8 +129,15 @@ describe("shouldShowAnnouncement gating", () => {
|
|
|
120
129
|
return;
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
// No mark
|
|
124
|
-
|
|
132
|
+
// No mark exists yet (fresh install or ephemeral/wiped sandbox). The
|
|
133
|
+
// gate must NOT announce — it seeds the state to the current version and
|
|
134
|
+
// returns false, so first-run users and disposable containers are never
|
|
135
|
+
// spammed with a changelog they have no context for.
|
|
136
|
+
expect(readLastAnnouncedVersion()).toBe("");
|
|
137
|
+
expect(shouldShowAnnouncement()).toBe(false);
|
|
138
|
+
// The seed was written, so a subsequent check stays quiet too.
|
|
139
|
+
expect(readLastAnnouncedVersion()).toBe(ANNOUNCEMENT_VERSION);
|
|
140
|
+
expect(shouldShowAnnouncement()).toBe(false);
|
|
125
141
|
});
|
|
126
142
|
|
|
127
143
|
test("returns true when a different (older) version is marked", async () => {
|
|
@@ -23,19 +23,18 @@ import { getMagicContextStorageDir } from "./data-path";
|
|
|
23
23
|
* Bump only when there are user-visible changes worth a startup dialog.
|
|
24
24
|
* Does NOT need to match the published package version.
|
|
25
25
|
*/
|
|
26
|
-
export const ANNOUNCEMENT_VERSION = "0.
|
|
26
|
+
export const ANNOUNCEMENT_VERSION = "0.22.0";
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Short, user-facing bullet strings. Keep each line ~80 chars or shorter so the
|
|
30
30
|
* TUI dialog renders cleanly without horizontal scroll on a typical terminal.
|
|
31
31
|
*/
|
|
32
32
|
export const ANNOUNCEMENT_FEATURES: ReadonlyArray<string> = [
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"doctor --issue now caps GitHub issue bodies at ~60KB with a dedicated 'Recent errors' section so reports stay submittable.",
|
|
33
|
+
"NOW ON BY DEFAULT — Temporal awareness: the agent sees elapsed-time markers (e.g. +2h 15m) between messages and dated compartments, so it knows how long ago things happened. Opt out with temporal_awareness: false.",
|
|
34
|
+
"NOW ON BY DEFAULT — Auto-search hints: each turn a background ctx_search whispers a compact 'vague recall' when something relevant exists in your memories, past conversation, or git history. No full content injected. Opt out with memory.auto_search.enabled: false.",
|
|
35
|
+
"Experimental features graduated to stable config: temporal_awareness and caveman_text_compression are now top-level keys; auto_search and git_commit_indexing moved under memory.* . Run `doctor` to migrate old experimental.* settings (your opt-ins/opt-outs are preserved).",
|
|
36
|
+
"git_commit_indexing (make project history semantically searchable) stays opt-in — enable with memory.git_commit_indexing.enabled: true.",
|
|
37
|
+
"Audit hardening across both harnesses: memory config-bypass fix, supersede-delta cache-stability fixes, and dashboard correctness fixes.",
|
|
39
38
|
];
|
|
40
39
|
|
|
41
40
|
/**
|
|
@@ -90,8 +89,31 @@ export function markAnnouncementSeen(version: string): void {
|
|
|
90
89
|
* True when the configured `ANNOUNCEMENT_VERSION` has not yet been dismissed
|
|
91
90
|
* AND there is at least one feature to show. Used by both the TUI dialog path
|
|
92
91
|
* and the Desktop ignored-message fallback.
|
|
92
|
+
*
|
|
93
|
+
* First-run / sandbox handling: when NO state file exists yet, we seed it to the
|
|
94
|
+
* current `ANNOUNCEMENT_VERSION` and return false instead of announcing. This
|
|
95
|
+
* covers two cases that previously spammed the dialog (issue #99):
|
|
96
|
+
* - Fresh installs: a brand-new user shouldn't be shown a changelog of release
|
|
97
|
+
* bullets they have no context for — they need onboarding, not patch notes.
|
|
98
|
+
* - Ephemeral/sandbox environments (Docker, CI, disposable dev containers)
|
|
99
|
+
* where the storage dir is wiped between launches: without the seed, the
|
|
100
|
+
* missing file made the announcement re-show on every single startup.
|
|
101
|
+
* Real upgrades still announce exactly once: an existing user already has a
|
|
102
|
+
* state file at the prior version, so the version mismatch shows the dialog and
|
|
103
|
+
* dismissing it advances the file to the current version.
|
|
104
|
+
*
|
|
105
|
+
* The seed is a deliberate write side-effect on the "no file" branch — folding
|
|
106
|
+
* it here (rather than a separate startup call) makes every caller path (plugin
|
|
107
|
+
* startup, Pi startup, TUI rpc pull) consistent with no ordering dependency.
|
|
93
108
|
*/
|
|
94
109
|
export function shouldShowAnnouncement(): boolean {
|
|
95
110
|
if (!ANNOUNCEMENT_VERSION || ANNOUNCEMENT_FEATURES.length === 0) return false;
|
|
96
|
-
|
|
111
|
+
const lastVersion = readLastAnnouncedVersion();
|
|
112
|
+
if (!lastVersion) {
|
|
113
|
+
// No prior state: fresh install or wiped sandbox. Seed to current and
|
|
114
|
+
// skip the announcement so we never pester first-run / ephemeral envs.
|
|
115
|
+
markAnnouncementSeen(ANNOUNCEMENT_VERSION);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return lastVersion !== ANNOUNCEMENT_VERSION;
|
|
97
119
|
}
|
|
@@ -46,8 +46,21 @@ describe("detectConflicts", () => {
|
|
|
46
46
|
else process.env[k] = v;
|
|
47
47
|
}
|
|
48
48
|
// Test directories live under tmpdir(); cleanup is best-effort.
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
try {
|
|
50
|
+
rmSync(projectDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
51
|
+
} catch {
|
|
52
|
+
/* Ignore EBUSY on Windows */
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
rmSync(userConfigDir, {
|
|
56
|
+
recursive: true,
|
|
57
|
+
force: true,
|
|
58
|
+
maxRetries: 10,
|
|
59
|
+
retryDelay: 100,
|
|
60
|
+
});
|
|
61
|
+
} catch {
|
|
62
|
+
/* Ignore EBUSY on Windows */
|
|
63
|
+
}
|
|
51
64
|
});
|
|
52
65
|
|
|
53
66
|
function writeProjectConfig(plugins: Array<string | [string, unknown]>): void {
|
|
@@ -182,7 +195,7 @@ describe("detectConflicts", () => {
|
|
|
182
195
|
});
|
|
183
196
|
|
|
184
197
|
it("returns no conflicts for unrelated plugins", () => {
|
|
185
|
-
writeProjectConfig(["@
|
|
198
|
+
writeProjectConfig(["@wolfx/opencode-magic-context@latest", "some-other-plugin"]);
|
|
186
199
|
const result = detectConflicts(projectDir);
|
|
187
200
|
expect(result.hasConflict).toBe(false);
|
|
188
201
|
});
|
|
@@ -195,7 +208,7 @@ describe("detectConflicts", () => {
|
|
|
195
208
|
describe("tuple plugin entries (issue #49)", () => {
|
|
196
209
|
it("does not crash when a plugin is defined as a [name, options] tuple", () => {
|
|
197
210
|
writeProjectConfig([
|
|
198
|
-
"@
|
|
211
|
+
"@wolfx/opencode-magic-context@latest",
|
|
199
212
|
["@plannotator/opencode@latest", { workflow: "plan-agent" }],
|
|
200
213
|
]);
|
|
201
214
|
expect(() => detectConflicts(projectDir)).not.toThrow();
|
|
@@ -203,7 +216,7 @@ describe("detectConflicts", () => {
|
|
|
203
216
|
|
|
204
217
|
it("detects DCP conflict when DCP is expressed as a tuple", () => {
|
|
205
218
|
writeProjectConfig([
|
|
206
|
-
"@
|
|
219
|
+
"@wolfx/opencode-magic-context@latest",
|
|
207
220
|
["@tarquinen/opencode-dcp@latest", {}],
|
|
208
221
|
]);
|
|
209
222
|
const result = detectConflicts(projectDir);
|
|
@@ -223,7 +236,7 @@ describe("detectConflicts", () => {
|
|
|
223
236
|
"@plannotator/opencode@latest",
|
|
224
237
|
{ workflow: "plan-agent", planningAgents: ["plan"] },
|
|
225
238
|
],
|
|
226
|
-
"@
|
|
239
|
+
"@wolfx/opencode-magic-context@latest",
|
|
227
240
|
]);
|
|
228
241
|
const result = detectConflicts(projectDir);
|
|
229
242
|
expect(result.hasConflict).toBe(false);
|
|
@@ -365,7 +365,7 @@ export function formatConflictShort(result: ConflictResult): string {
|
|
|
365
365
|
"",
|
|
366
366
|
...result.reasons.map((r) => `• ${r}`),
|
|
367
367
|
"",
|
|
368
|
-
"Fix: run `npx @
|
|
368
|
+
"Fix: run `npx @wolfx/opencode-magic-context@latest doctor`",
|
|
369
369
|
];
|
|
370
370
|
return lines.join("\n");
|
|
371
371
|
}
|
|
@@ -38,7 +38,11 @@ describe("fixConflicts", () => {
|
|
|
38
38
|
if (value === undefined) delete process.env[key];
|
|
39
39
|
else process.env[key] = value;
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
try {
|
|
42
|
+
rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
43
|
+
} catch {
|
|
44
|
+
/* Ignore EBUSY on Windows */
|
|
45
|
+
}
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
it("preserves JSONC comments and tuple plugin entries while removing canonical DCP", () => {
|
|
@@ -50,7 +54,7 @@ describe("fixConflicts", () => {
|
|
|
50
54
|
"plugin": [
|
|
51
55
|
["@plannotator/opencode@latest", { "workflow": "plan-agent" }],
|
|
52
56
|
["@tarquinen/opencode-dcp@latest", { "enabled": true }],
|
|
53
|
-
"@
|
|
57
|
+
"@wolfx/opencode-magic-context@latest"
|
|
54
58
|
],
|
|
55
59
|
"compaction": {
|
|
56
60
|
// keep this compaction comment
|
|
@@ -76,7 +80,7 @@ describe("fixConflicts", () => {
|
|
|
76
80
|
expect(updated.compaction).toEqual({ auto: false, prune: false });
|
|
77
81
|
expect(updated.plugin).toEqual([
|
|
78
82
|
["@plannotator/opencode@latest", { workflow: "plan-agent" }],
|
|
79
|
-
"@
|
|
83
|
+
"@wolfx/opencode-magic-context@latest",
|
|
80
84
|
]);
|
|
81
85
|
});
|
|
82
86
|
|
|
@@ -121,20 +121,19 @@ describe("data-path", () => {
|
|
|
121
121
|
expect(shared).toContain("cortexkit");
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
test("getProjectMagicContextDir composes <project>/.
|
|
125
|
-
// Project-local artifacts
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// under os.tmpdir().
|
|
124
|
+
test("getProjectMagicContextDir composes <project>/.magic-context", () => {
|
|
125
|
+
// Project-local artifacts live inside the project so OpenCode's
|
|
126
|
+
// external_directory permission system treats them as project-internal.
|
|
127
|
+
// Without this, historian's Read tool would trigger a permission prompt
|
|
128
|
+
// on every run when artifacts lived under os.tmpdir().
|
|
130
129
|
expect(getProjectMagicContextDir("/Users/me/Work/proj")).toBe(
|
|
131
|
-
path.join("/Users/me/Work/proj", ".
|
|
130
|
+
path.join("/Users/me/Work/proj", ".magic-context"),
|
|
132
131
|
);
|
|
133
132
|
});
|
|
134
133
|
|
|
135
134
|
test("getProjectMagicContextHistorianDir appends historian/", () => {
|
|
136
135
|
expect(getProjectMagicContextHistorianDir("/Users/me/Work/proj")).toBe(
|
|
137
|
-
path.join("/Users/me/Work/proj", ".
|
|
136
|
+
path.join("/Users/me/Work/proj", ".magic-context", "historian"),
|
|
138
137
|
);
|
|
139
138
|
});
|
|
140
139
|
|
|
@@ -145,7 +144,7 @@ describe("data-path", () => {
|
|
|
145
144
|
// project-local historian dir.
|
|
146
145
|
process.env.XDG_DATA_HOME = "/tmp/custom-data";
|
|
147
146
|
expect(getProjectMagicContextDir("/some/project")).toBe(
|
|
148
|
-
path.join("/some/project", ".
|
|
147
|
+
path.join("/some/project", ".magic-context"),
|
|
149
148
|
);
|
|
150
149
|
});
|
|
151
150
|
|
|
@@ -153,7 +152,7 @@ describe("data-path", () => {
|
|
|
153
152
|
// path.join normalizes redundant separators so callers don't need to
|
|
154
153
|
// worry about how the project directory was constructed.
|
|
155
154
|
expect(getProjectMagicContextDir("/some/project/")).toBe(
|
|
156
|
-
path.join("/some/project/", ".
|
|
155
|
+
path.join("/some/project/", ".magic-context"),
|
|
157
156
|
);
|
|
158
157
|
});
|
|
159
158
|
});
|
package/src/shared/data-path.ts
CHANGED
|
@@ -62,34 +62,31 @@ export function getMagicContextHistorianDir(harness: HarnessId = getHarness()):
|
|
|
62
62
|
/**
|
|
63
63
|
* Project-local magic-context artifact directory.
|
|
64
64
|
*
|
|
65
|
-
* Layout: `<project-directory>/.
|
|
65
|
+
* Layout: `<project-directory>/.magic-context/`
|
|
66
66
|
*
|
|
67
67
|
* Used for artifacts that the historian/recomp pipeline writes during a run
|
|
68
68
|
* and that the model is asked to read via its native Read tool. OpenCode's
|
|
69
69
|
* `external_directory` permission system asks the user before reading any
|
|
70
70
|
* file outside the project directory or its worktree, which interrupts every
|
|
71
71
|
* historian run when artifacts live under `os.tmpdir()`. Writing under the
|
|
72
|
-
* project's own `.
|
|
73
|
-
* never triggers a permission prompt.
|
|
72
|
+
* project's own `.magic-context/` subtree falls inside the project boundary
|
|
73
|
+
* and never triggers a permission prompt.
|
|
74
74
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* magic-context artifacts under `.opencode/magic-context/` keeps them
|
|
78
|
-
* co-located with related OpenCode metadata and makes them easy for users to
|
|
79
|
-
* locate when debugging.
|
|
75
|
+
* Using a harness-agnostic name (`/.magic-context/` rather than
|
|
76
|
+
* `/.opencode/`) ensures the same path works for both OpenCode and Pi.
|
|
80
77
|
*
|
|
81
78
|
* Logger does NOT use this — log files stay in the per-harness tmp subtree
|
|
82
79
|
* because they are written by the plugin process itself (no model-side Read
|
|
83
80
|
* tool call, no permission prompt) and span sessions/projects.
|
|
84
81
|
*/
|
|
85
82
|
export function getProjectMagicContextDir(directory: string): string {
|
|
86
|
-
return path.join(directory, ".
|
|
83
|
+
return path.join(directory, ".magic-context");
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
/**
|
|
90
87
|
* Project-local historian artifact directory.
|
|
91
88
|
*
|
|
92
|
-
* Layout: `<project-directory>/.
|
|
89
|
+
* Layout: `<project-directory>/.magic-context/historian/`
|
|
93
90
|
*
|
|
94
91
|
* Used for:
|
|
95
92
|
* - existing-state offload XMLs that long historian/recomp passes write
|
|
@@ -97,8 +94,9 @@ export function getProjectMagicContextDir(directory: string): string {
|
|
|
97
94
|
* - validation-failure dump XMLs preserved for debugging
|
|
98
95
|
*
|
|
99
96
|
* Callers must `mkdirSync(dir, { recursive: true })` before writing — the
|
|
100
|
-
* `.
|
|
101
|
-
* here must degrade gracefully (e.g. historian falls back to inline
|
|
97
|
+
* `.magic-context/` parent may not exist on a fresh project, and write
|
|
98
|
+
* failures here must degrade gracefully (e.g. historian falls back to inline
|
|
99
|
+
* state).
|
|
102
100
|
*/
|
|
103
101
|
export function getProjectMagicContextHistorianDir(directory: string): string {
|
|
104
102
|
return path.join(getProjectMagicContextDir(directory), "historian");
|
|
@@ -39,7 +39,11 @@ describe("models-dev-cache", () => {
|
|
|
39
39
|
if (v === undefined) delete process.env[k];
|
|
40
40
|
else process.env[k] = v;
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
try {
|
|
43
|
+
rmSync(tempDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
44
|
+
} catch {
|
|
45
|
+
/* Ignore EBUSY on Windows */
|
|
46
|
+
}
|
|
43
47
|
clearModelsDevCache();
|
|
44
48
|
});
|
|
45
49
|
|
|
@@ -234,7 +238,7 @@ describe("models-dev-cache", () => {
|
|
|
234
238
|
expect(getModelsDevContextLimit("anthropic", "claude-4")).toBeUndefined();
|
|
235
239
|
});
|
|
236
240
|
|
|
237
|
-
test("
|
|
241
|
+
test("takes the larger limit when both layers know the model (API larger)", async () => {
|
|
238
242
|
// Seed file layer with one value.
|
|
239
243
|
const opencodeDir = join(tempDir, "opencode");
|
|
240
244
|
mkdirSync(opencodeDir, { recursive: true });
|
|
@@ -248,7 +252,7 @@ describe("models-dev-cache", () => {
|
|
|
248
252
|
// Sanity: file layer returns 100000 before API refresh.
|
|
249
253
|
expect(getModelsDevContextLimit("anthropic", "claude-4")).toBe(100000);
|
|
250
254
|
|
|
251
|
-
// Mock client providing
|
|
255
|
+
// Mock client providing a LARGER value via API.
|
|
252
256
|
const mockClient = {
|
|
253
257
|
config: {
|
|
254
258
|
providers: async () => ({
|
|
@@ -267,7 +271,7 @@ describe("models-dev-cache", () => {
|
|
|
267
271
|
};
|
|
268
272
|
await refreshModelLimitsFromApi(mockClient);
|
|
269
273
|
|
|
270
|
-
// API value wins.
|
|
274
|
+
// Larger (API) value wins.
|
|
271
275
|
expect(getModelsDevContextLimit("anthropic", "claude-4")).toBe(1000000);
|
|
272
276
|
|
|
273
277
|
const state = getModelsDevCacheState();
|
|
@@ -275,6 +279,70 @@ describe("models-dev-cache", () => {
|
|
|
275
279
|
expect(state.apiCount).toBe(1);
|
|
276
280
|
});
|
|
277
281
|
|
|
282
|
+
test("file value wins when the live API reports a smaller (wrong) limit (issue #117)", async () => {
|
|
283
|
+
// The ollama-cloud scenario: models.dev has the correct large window, but
|
|
284
|
+
// ollama reports its tiny default num_ctx via the live /config/providers
|
|
285
|
+
// API. The larger, correct file value must win so pressure isn't bogus.
|
|
286
|
+
const opencodeDir = join(tempDir, "opencode");
|
|
287
|
+
mkdirSync(opencodeDir, { recursive: true });
|
|
288
|
+
writeFileSync(
|
|
289
|
+
join(opencodeDir, "models.json"),
|
|
290
|
+
JSON.stringify({
|
|
291
|
+
"ollama-cloud": {
|
|
292
|
+
models: { "deepseek-v4-pro": { limit: { context: 1048576 } } },
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const mockClient = {
|
|
298
|
+
config: {
|
|
299
|
+
providers: async () => ({
|
|
300
|
+
data: {
|
|
301
|
+
providers: [
|
|
302
|
+
{
|
|
303
|
+
id: "ollama-cloud",
|
|
304
|
+
models: {
|
|
305
|
+
// Bogus tiny default num_ctx from ollama.
|
|
306
|
+
"deepseek-v4-pro": { limit: { context: 8192 } },
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
await refreshModelLimitsFromApi(mockClient);
|
|
315
|
+
|
|
316
|
+
// Larger (file/models.dev) value wins, not the tiny live-API value.
|
|
317
|
+
expect(getModelsDevContextLimit("ollama-cloud", "deepseek-v4-pro")).toBe(1048576);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("matches a tagged ollama model against its tag-less models.dev entry (issue #117)", () => {
|
|
321
|
+
// ollama invokes cloud models with a tag (deepseek-v4-pro:cloud) while
|
|
322
|
+
// models.dev stores them tag-less (deepseek-v4-pro).
|
|
323
|
+
const opencodeDir = join(tempDir, "opencode");
|
|
324
|
+
mkdirSync(opencodeDir, { recursive: true });
|
|
325
|
+
writeFileSync(
|
|
326
|
+
join(opencodeDir, "models.json"),
|
|
327
|
+
JSON.stringify({
|
|
328
|
+
"ollama-cloud": {
|
|
329
|
+
models: {
|
|
330
|
+
"deepseek-v4-pro": { limit: { context: 1048576 } },
|
|
331
|
+
// A legitimately-tagged model must still match exactly.
|
|
332
|
+
"gemma3:27b": { limit: { context: 131072 } },
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Tagged invocation falls back to the tag-less entry.
|
|
339
|
+
expect(getModelsDevContextLimit("ollama-cloud", "deepseek-v4-pro:cloud")).toBe(1048576);
|
|
340
|
+
// Exact tagged match still wins (no wrongful collapse).
|
|
341
|
+
expect(getModelsDevContextLimit("ollama-cloud", "gemma3:27b")).toBe(131072);
|
|
342
|
+
// Unknown tagged model with no tag-less base stays undefined.
|
|
343
|
+
expect(getModelsDevContextLimit("ollama-cloud", "nonexistent:cloud")).toBeUndefined();
|
|
344
|
+
});
|
|
345
|
+
|
|
278
346
|
test("refreshModelLimitsFromApi tolerates empty/malformed responses", async () => {
|
|
279
347
|
// Undefined data.
|
|
280
348
|
await refreshModelLimitsFromApi({
|
|
@@ -298,19 +298,58 @@ export async function refreshModelLimitsFromApi(client: OpencodeClientLike): Pro
|
|
|
298
298
|
* Returns `undefined` if neither layer knows the model.
|
|
299
299
|
*/
|
|
300
300
|
export function getModelsDevContextLimit(providerID: string, modelID: string): number | undefined {
|
|
301
|
-
const key = `${providerID}/${modelID}`;
|
|
302
|
-
|
|
303
|
-
if (apiCache) {
|
|
304
|
-
const fromApi = apiCache.get(key)?.limit;
|
|
305
|
-
if (typeof fromApi === "number") return fromApi;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
301
|
const now = Date.now();
|
|
309
302
|
if (!fileCache || now - fileLastAttempt > RELOAD_INTERVAL_MS) {
|
|
310
303
|
fileLastAttempt = now;
|
|
311
304
|
fileCache = loadModelsDevMetadataFromFile();
|
|
312
305
|
}
|
|
313
|
-
|
|
306
|
+
|
|
307
|
+
const fromApi = lookupLimitWithTagFallback(apiCache, providerID, modelID);
|
|
308
|
+
const fromFile = lookupLimitWithTagFallback(fileCache, providerID, modelID);
|
|
309
|
+
|
|
310
|
+
// When BOTH layers know the model, take the LARGER limit. Providers never
|
|
311
|
+
// under-report their real window, so a suspiciously small value — e.g.
|
|
312
|
+
// ollama reporting its default `num_ctx` (4k/8k) for a cloud model via the
|
|
313
|
+
// live `/config/providers` API — must not override the correct, larger
|
|
314
|
+
// models.dev value. A genuinely smaller real limit (provider actually
|
|
315
|
+
// rejects at N) is captured separately via the overflow-detection path
|
|
316
|
+
// (detectedContextLimit), not here. (issue #117)
|
|
317
|
+
if (typeof fromApi === "number" && typeof fromFile === "number") {
|
|
318
|
+
return Math.max(fromApi, fromFile);
|
|
319
|
+
}
|
|
320
|
+
return fromApi ?? fromFile;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Look up a model's limit in one cache layer, with an ollama-style tag-suffix
|
|
325
|
+
* fallback.
|
|
326
|
+
*
|
|
327
|
+
* models.dev stores some models WITH a colon tag (e.g. `gemma3:27b`,
|
|
328
|
+
* `deepseek-v3.1:671b`) and ollama-cloud base models WITHOUT one
|
|
329
|
+
* (`deepseek-v4-pro`). But ollama invokes cloud models with a tag at runtime
|
|
330
|
+
* (`deepseek-v4-pro:cloud`), so OpenCode reports the tagged id. An exact-only
|
|
331
|
+
* match therefore misses → falls back to the 128k default → wrong pressure
|
|
332
|
+
* denominator (issue #117).
|
|
333
|
+
*
|
|
334
|
+
* Strategy: exact match first (never collapses a legitimately-tagged model),
|
|
335
|
+
* then retry once with the last `:tag` segment stripped.
|
|
336
|
+
*/
|
|
337
|
+
function lookupLimitWithTagFallback(
|
|
338
|
+
cache: Map<string, CachedModelMetadata> | null,
|
|
339
|
+
providerID: string,
|
|
340
|
+
modelID: string,
|
|
341
|
+
): number | undefined {
|
|
342
|
+
if (!cache) return undefined;
|
|
343
|
+
const exact = cache.get(`${providerID}/${modelID}`)?.limit;
|
|
344
|
+
if (typeof exact === "number") return exact;
|
|
345
|
+
|
|
346
|
+
const colonIdx = modelID.lastIndexOf(":");
|
|
347
|
+
if (colonIdx > 0) {
|
|
348
|
+
const baseModel = modelID.slice(0, colonIdx);
|
|
349
|
+
const fallback = cache.get(`${providerID}/${baseModel}`)?.limit;
|
|
350
|
+
if (typeof fallback === "number") return fallback;
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
314
353
|
}
|
|
315
354
|
|
|
316
355
|
/** Clear in-memory caches (for testing). */
|
|
@@ -18,7 +18,11 @@ describe("opencode-compaction-detector", () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
afterEach(() => {
|
|
21
|
-
|
|
21
|
+
try {
|
|
22
|
+
rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
23
|
+
} catch {
|
|
24
|
+
/* Ignore EBUSY on Windows */
|
|
25
|
+
}
|
|
22
26
|
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
23
27
|
});
|
|
24
28
|
|
|
@@ -30,7 +34,11 @@ describe("opencode-compaction-detector", () => {
|
|
|
30
34
|
const result = isOpenCodeAutoCompactionEnabled(emptyDir);
|
|
31
35
|
|
|
32
36
|
expect(result).toBe(true);
|
|
33
|
-
|
|
37
|
+
try {
|
|
38
|
+
rmSync(emptyDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
39
|
+
} catch {
|
|
40
|
+
/* Ignore EBUSY on Windows */
|
|
41
|
+
}
|
|
34
42
|
});
|
|
35
43
|
});
|
|
36
44
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { MagicContextRpcClient } from "./rpc-client";
|
|
7
|
-
import {
|
|
7
|
+
import { MagicContextRpcServer } from "./rpc-server";
|
|
8
|
+
import { parseRpcPortFile, rpcPortFilePath } from "./rpc-utils";
|
|
8
9
|
|
|
9
10
|
interface TestServer {
|
|
10
11
|
port: number;
|
|
@@ -19,7 +20,11 @@ afterEach(async () => {
|
|
|
19
20
|
await server.close();
|
|
20
21
|
}
|
|
21
22
|
for (const dir of tempDirs.splice(0)) {
|
|
22
|
-
|
|
23
|
+
try {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
25
|
+
} catch {
|
|
26
|
+
/* Ignore EBUSY on Windows */
|
|
27
|
+
}
|
|
23
28
|
}
|
|
24
29
|
});
|
|
25
30
|
|
|
@@ -116,6 +121,52 @@ describe("MagicContextRpcClient", () => {
|
|
|
116
121
|
expect(await client.call<{ value: string }>("value")).toEqual({ value: "second" });
|
|
117
122
|
});
|
|
118
123
|
|
|
124
|
+
test("authenticates against a real server with the published token", async () => {
|
|
125
|
+
const storageDir = makeTempDir();
|
|
126
|
+
const directory = "/repo-auth";
|
|
127
|
+
const server = new MagicContextRpcServer(storageDir, directory);
|
|
128
|
+
server.handle("ping", async () => ({ pong: true }));
|
|
129
|
+
await server.start();
|
|
130
|
+
try {
|
|
131
|
+
const client = new MagicContextRpcClient(storageDir, directory);
|
|
132
|
+
// Real round-trip: client must read the token from the port file and
|
|
133
|
+
// send it as Bearer auth, or the server returns 401.
|
|
134
|
+
expect(await client.call<{ pong: boolean }>("ping")).toEqual({ pong: true });
|
|
135
|
+
} finally {
|
|
136
|
+
server.stop();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("a request without the token is rejected 401 by the server", async () => {
|
|
141
|
+
const storageDir = makeTempDir();
|
|
142
|
+
const directory = "/repo-noauth";
|
|
143
|
+
const server = new MagicContextRpcServer(storageDir, directory);
|
|
144
|
+
server.handle("ping", async () => ({ pong: true }));
|
|
145
|
+
const port = await server.start();
|
|
146
|
+
try {
|
|
147
|
+
// Sanity: the port file carries a non-empty token.
|
|
148
|
+
const record = parseRpcPortFile(
|
|
149
|
+
readFileSync(rpcPortFilePath(storageDir, directory), "utf-8"),
|
|
150
|
+
);
|
|
151
|
+
expect(typeof record?.token).toBe("string");
|
|
152
|
+
expect((record?.token ?? "").length).toBeGreaterThan(0);
|
|
153
|
+
|
|
154
|
+
// A raw fetch with no Authorization header must be rejected.
|
|
155
|
+
const res = await fetch(`http://127.0.0.1:${port}/rpc/ping`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "Content-Type": "application/json" },
|
|
158
|
+
body: "{}",
|
|
159
|
+
});
|
|
160
|
+
expect(res.status).toBe(401);
|
|
161
|
+
|
|
162
|
+
// Health stays open (no token required) for discovery.
|
|
163
|
+
const health = await fetch(`http://127.0.0.1:${port}/health`);
|
|
164
|
+
expect(health.status).toBe(200);
|
|
165
|
+
} finally {
|
|
166
|
+
server.stop();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
119
170
|
test("gives up when the port file points at a dead server", async () => {
|
|
120
171
|
const storageDir = makeTempDir();
|
|
121
172
|
const directory = "/repo";
|