@cortexkit/opencode-magic-context 0.38.1 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -4
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/profiles.d.ts +22 -0
- package/dist/config/profiles.d.ts.map +1 -0
- package/dist/config/project-security.d.ts +14 -6
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/raw-loader.d.ts +42 -0
- package/dist/config/raw-loader.d.ts.map +1 -0
- package/dist/config/schema/magic-context.d.ts +966 -531
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts +3 -2
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts +9 -2
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +4 -2
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +21 -5
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +3 -2
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts +2 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts +0 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -2
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts +6 -3
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +11 -1
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-failure.d.ts +11 -0
- package/dist/features/magic-context/memory/embedding-failure.d.ts.map +1 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts +5 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-provider.d.ts +3 -0
- package/dist/features/magic-context/memory/embedding-provider.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/mural/compress-cues.d.ts +3 -2
- package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +2 -0
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts +3 -2
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/condition-compiler.d.ts +2 -0
- package/dist/features/magic-context/smart-notes/condition-compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -2
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +7 -2
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +1 -0
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/tool-definition-tokens.d.ts +10 -0
- package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +11 -0
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts +3 -2
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts +3 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +4 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +4 -1
- package/dist/hooks/magic-context/compartment-runner-historian.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.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +4 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +14 -2
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts +7 -0
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts.map +1 -1
- package/dist/hooks/magic-context/format-embed-failure.d.ts +3 -0
- package/dist/hooks/magic-context/format-embed-failure.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +13 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-persist.d.ts +14 -0
- package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts +13 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts +1 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts +12 -0
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/raw-fallback-context-limit.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +4 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +6 -0
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +12 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +5 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +9 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
- package/dist/{index-2y0ybv5x.js → index-3dkzzh0t.js} +15 -17
- package/dist/{index-5znjkjws.js → index-axpym70t.js} +879 -170
- package/dist/index-ehpzdy0v.js +126 -0
- package/dist/{index-22mw8km4.js → index-rw0qrkvq.js} +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18466 -16177
- package/dist/model-resolution-r9jbcz86.js +15 -0
- package/dist/plugin/dream-timer.d.ts +3 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +2 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +14 -6
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-p030kxtc.js → recomp-orchestrator-dmyvqekw.js} +2 -3
- package/dist/shared/jsonc-edit.d.ts +6 -1
- package/dist/shared/jsonc-edit.d.ts.map +1 -1
- package/dist/shared/model-resolution.d.ts +53 -0
- package/dist/shared/model-resolution.d.ts.map +1 -0
- package/dist/shared/model-suggestion-retry.d.ts +4 -1
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/resolve-fallbacks.d.ts +5 -1
- package/dist/shared/resolve-fallbacks.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +2 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +12 -1
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/status-detail-text.d.ts +4 -0
- package/dist/shared/status-detail-text.d.ts.map +1 -0
- package/dist/shared/subagent-runner.d.ts +2 -1
- package/dist/shared/subagent-runner.d.ts.map +1 -1
- package/dist/{storage-meta-session-2306t3j7.js → storage-meta-session-yvtbaw7c.js} +1 -1
- package/dist/{task-config-at3adc59.js → task-config-a1qmj9bx.js} +2 -2
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/config/index.test.ts +434 -19
- package/src/config/index.ts +67 -33
- package/src/config/profiles.ts +106 -0
- package/src/config/project-security.hostile-matrix.test.ts +480 -0
- package/src/config/project-security.test.ts +43 -5
- package/src/config/project-security.ts +151 -22
- package/src/config/raw-loader.test.ts +424 -0
- package/src/config/raw-loader.ts +490 -0
- package/src/config/schema/magic-context.test.ts +272 -2
- package/src/config/schema/magic-context.ts +342 -49
- package/src/shared/jsonc-edit.ts +74 -1
- package/src/shared/model-resolution.test.ts +170 -0
- package/src/shared/model-resolution.ts +227 -0
- package/src/shared/model-suggestion-retry.test.ts +27 -0
- package/src/shared/model-suggestion-retry.ts +19 -10
- package/src/shared/resolve-fallbacks.test.ts +13 -1
- package/src/shared/resolve-fallbacks.ts +19 -4
- package/src/shared/rpc-types.ts +2 -0
- package/src/shared/rpc-utils.test.ts +32 -0
- package/src/shared/rpc-utils.ts +38 -13
- package/src/shared/status-detail-text.test.ts +82 -0
- package/src/shared/status-detail-text.ts +57 -0
- package/src/shared/subagent-runner.ts +3 -1
- package/src/tui/data/context-db.ts +1 -0
- package/src/tui-compiled/data/context-db.ts +1 -0
- package/dist/index-td9depx5.js +0 -46
- package/dist/resolve-fallbacks-qf0ekmz9.js +0 -11
package/src/config/index.test.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
|
|
5
|
+
import { resolveHistorianModel } from "../shared/model-resolution";
|
|
6
6
|
import { loadPluginConfig, loadPluginConfigDetailed } from "./index";
|
|
7
|
+
import { resolveConfigProfile } from "./profiles";
|
|
7
8
|
import { RUST_COMPACTION_OFF_WARNING } from "./transform-mode";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -342,6 +343,40 @@ describe("loadPluginConfig — secret redaction", () => {
|
|
|
342
343
|
expect(w).toBeDefined();
|
|
343
344
|
});
|
|
344
345
|
|
|
346
|
+
it("prunes only cross-harness qualifier leaves and names their full paths", () => {
|
|
347
|
+
const result = loadWithUserConfig(
|
|
348
|
+
JSON.stringify({
|
|
349
|
+
historian: {
|
|
350
|
+
two_pass: true,
|
|
351
|
+
opencode: {
|
|
352
|
+
model: "anthropic/claude-sonnet",
|
|
353
|
+
variant: "high",
|
|
354
|
+
thinking_level: "minimal",
|
|
355
|
+
},
|
|
356
|
+
pi: {
|
|
357
|
+
model: "github-copilot/gpt-5",
|
|
358
|
+
thinking_level: "medium",
|
|
359
|
+
variant: "fast",
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
expect(result.historian?.two_pass).toBe(true);
|
|
366
|
+
expect(result.historian?.opencode).toEqual({
|
|
367
|
+
model: "anthropic/claude-sonnet",
|
|
368
|
+
variant: "high",
|
|
369
|
+
});
|
|
370
|
+
expect(result.historian?.pi).toEqual({
|
|
371
|
+
model: "github-copilot/gpt-5",
|
|
372
|
+
thinking_level: "medium",
|
|
373
|
+
});
|
|
374
|
+
const warnings = result.configWarnings?.join("\n") ?? "";
|
|
375
|
+
expect(warnings).toContain("historian.opencode.thinking_level");
|
|
376
|
+
expect(warnings).toContain("historian.pi.variant");
|
|
377
|
+
expect(warnings).not.toContain("invalid agent configuration, ignoring");
|
|
378
|
+
});
|
|
379
|
+
|
|
345
380
|
it("still shows numeric and boolean invalid values (not secrets by nature)", () => {
|
|
346
381
|
// Numbers/booleans in config fields are never secrets — they're
|
|
347
382
|
// plain validation mistakes — so we surface them fully to help
|
|
@@ -740,25 +775,43 @@ describe("loadPluginConfig — user-only settings", () => {
|
|
|
740
775
|
const result = loadWithUserAndProjectConfig(
|
|
741
776
|
JSON.stringify({
|
|
742
777
|
historian: {
|
|
743
|
-
|
|
744
|
-
|
|
778
|
+
opencode: {
|
|
779
|
+
model: "anthropic/user-historian",
|
|
780
|
+
fallback_models: ["anthropic/user-fallback"],
|
|
781
|
+
},
|
|
782
|
+
pi: {
|
|
783
|
+
model: "github-copilot/user-historian",
|
|
784
|
+
fallback_models: ["github-copilot/user-fallback"],
|
|
785
|
+
},
|
|
745
786
|
},
|
|
746
787
|
}),
|
|
747
788
|
JSON.stringify({
|
|
748
789
|
historian: {
|
|
749
|
-
|
|
750
|
-
|
|
790
|
+
opencode: {
|
|
791
|
+
model: "anthropic/project-historian",
|
|
792
|
+
fallback_models: ["anthropic/project-fallback"],
|
|
793
|
+
},
|
|
794
|
+
pi: {
|
|
795
|
+
model: "github-copilot/project-historian",
|
|
796
|
+
fallback_models: ["github-copilot/project-fallback"],
|
|
797
|
+
},
|
|
751
798
|
temperature: 0.2,
|
|
752
799
|
},
|
|
753
800
|
}),
|
|
754
801
|
);
|
|
755
802
|
|
|
756
|
-
expect(result.historian?.
|
|
757
|
-
|
|
803
|
+
expect(result.historian?.opencode).toEqual({
|
|
804
|
+
model: "anthropic/user-historian",
|
|
805
|
+
fallback_models: ["anthropic/user-fallback"],
|
|
806
|
+
});
|
|
807
|
+
expect(result.historian?.pi).toEqual({
|
|
808
|
+
model: "github-copilot/user-historian",
|
|
809
|
+
fallback_models: ["github-copilot/user-fallback"],
|
|
810
|
+
});
|
|
758
811
|
expect(result.historian?.temperature).toBe(0.2);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
);
|
|
812
|
+
const warnings = result.configWarnings?.join("\n") ?? "";
|
|
813
|
+
expect(warnings).toContain("historian.opencode.model");
|
|
814
|
+
expect(warnings).toContain("historian.pi.model");
|
|
762
815
|
});
|
|
763
816
|
});
|
|
764
817
|
|
|
@@ -877,21 +930,25 @@ describe("loadPluginConfig — raw merge preserves user fields not set in projec
|
|
|
877
930
|
JSON.stringify({ language: "tr" }),
|
|
878
931
|
JSON.stringify({
|
|
879
932
|
dreamer: {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
933
|
+
tasks: { verify: { schedule: "0 3 * * *" } },
|
|
934
|
+
opencode: {
|
|
935
|
+
model: "anthropic/project-dreamer",
|
|
936
|
+
tasks: { verify: { model: "anthropic/project-verify" } },
|
|
937
|
+
},
|
|
938
|
+
pi: {
|
|
939
|
+
model: "github-copilot/project-dreamer",
|
|
940
|
+
tasks: { verify: { model: "github-copilot/project-verify" } },
|
|
886
941
|
},
|
|
887
942
|
},
|
|
888
943
|
}),
|
|
889
944
|
);
|
|
890
945
|
|
|
891
946
|
expect(result.language).toBe("tr");
|
|
892
|
-
expect(result.dreamer?.model).toBe("anthropic/project-dreamer");
|
|
947
|
+
expect(result.dreamer?.opencode?.model).toBe("anthropic/project-dreamer");
|
|
948
|
+
expect(result.dreamer?.pi?.model).toBe("github-copilot/project-dreamer");
|
|
893
949
|
expect(result.dreamer?.tasks.verify.schedule).toBe("0 3 * * *");
|
|
894
|
-
expect(result.dreamer?.tasks
|
|
950
|
+
expect(result.dreamer?.opencode?.tasks?.verify?.model).toBe("anthropic/project-verify");
|
|
951
|
+
expect(result.dreamer?.pi?.tasks?.verify?.model).toBe("github-copilot/project-verify");
|
|
895
952
|
});
|
|
896
953
|
|
|
897
954
|
it("project boolean override beats user default", () => {
|
|
@@ -958,6 +1015,364 @@ describe("transform_mode resolution", () => {
|
|
|
958
1015
|
});
|
|
959
1016
|
});
|
|
960
1017
|
|
|
1018
|
+
describe("loadPluginConfig — user-owned model profiles", () => {
|
|
1019
|
+
it("merges user base, selected profile, then project config without losing profile fallback qualifiers", () => {
|
|
1020
|
+
const result = loadWithUserAndProjectConfig(
|
|
1021
|
+
JSON.stringify({
|
|
1022
|
+
profile: "personal",
|
|
1023
|
+
historian: {
|
|
1024
|
+
two_pass: true,
|
|
1025
|
+
opencode: {
|
|
1026
|
+
model: "anthropic/base-historian",
|
|
1027
|
+
fallback_models: ["anthropic/base-fallback"],
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
dreamer: {
|
|
1031
|
+
opencode: { model: "anthropic/base-dreamer" },
|
|
1032
|
+
},
|
|
1033
|
+
profiles: {
|
|
1034
|
+
work: {
|
|
1035
|
+
historian: {
|
|
1036
|
+
opencode: {
|
|
1037
|
+
model: { model: "anthropic/work-historian", variant: "high" },
|
|
1038
|
+
fallback_models: [
|
|
1039
|
+
{ model: "openai/work-fallback", variant: "low" },
|
|
1040
|
+
],
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
dreamer: {
|
|
1044
|
+
opencode: {
|
|
1045
|
+
model: "anthropic/work-dreamer",
|
|
1046
|
+
fallback_models: [
|
|
1047
|
+
{ model: "openai/work-dreamer-fallback", variant: "medium" },
|
|
1048
|
+
],
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
sidekick: { model: "anthropic/work-sidekick" },
|
|
1052
|
+
},
|
|
1053
|
+
personal: {
|
|
1054
|
+
historian: { opencode: { model: "anthropic/personal-historian" } },
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
}),
|
|
1058
|
+
JSON.stringify({
|
|
1059
|
+
profile: "work",
|
|
1060
|
+
memory: { enabled: false },
|
|
1061
|
+
}),
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
expect(result.profile).toBe("work");
|
|
1065
|
+
expect(result.memory.enabled).toBe(false);
|
|
1066
|
+
expect(result.historian?.two_pass).toBe(true);
|
|
1067
|
+
expect(result.historian?.opencode).toEqual({
|
|
1068
|
+
model: { model: "anthropic/work-historian", variant: "high" },
|
|
1069
|
+
fallback_models: [{ model: "openai/work-fallback", variant: "low" }],
|
|
1070
|
+
});
|
|
1071
|
+
expect(result.dreamer?.opencode?.model).toBe("anthropic/work-dreamer");
|
|
1072
|
+
expect(result.dreamer?.opencode?.fallback_models).toEqual([
|
|
1073
|
+
{ model: "openai/work-dreamer-fallback", variant: "medium" },
|
|
1074
|
+
]);
|
|
1075
|
+
expect(result.sidekick?.model).toBe("anthropic/work-sidekick");
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it("uses project selection over user selection and falls back to the base on an unknown name", () => {
|
|
1079
|
+
const userConfig = JSON.stringify({
|
|
1080
|
+
profile: "personal",
|
|
1081
|
+
historian: { opencode: { model: "anthropic/base" } },
|
|
1082
|
+
profiles: {
|
|
1083
|
+
personal: { historian: { opencode: { model: "anthropic/personal" } } },
|
|
1084
|
+
work: { historian: { opencode: { model: "anthropic/work" } } },
|
|
1085
|
+
},
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
const userDefault = loadWithUserAndProjectConfig(userConfig, "{}");
|
|
1089
|
+
const projectOverride = loadWithUserAndProjectConfig(userConfig, '{"profile":"work"}');
|
|
1090
|
+
const unknownProject = loadWithUserAndProjectConfig(userConfig, '{"profile":"missing"}');
|
|
1091
|
+
|
|
1092
|
+
expect(userDefault.profile).toBe("personal");
|
|
1093
|
+
expect(userDefault.historian?.opencode?.model).toBe("anthropic/personal");
|
|
1094
|
+
expect(projectOverride.profile).toBe("work");
|
|
1095
|
+
expect(projectOverride.historian?.opencode?.model).toBe("anthropic/work");
|
|
1096
|
+
expect(unknownProject.profile).toBeUndefined();
|
|
1097
|
+
expect(unknownProject.enabled).toBe(true);
|
|
1098
|
+
expect(unknownProject.historian?.opencode?.model).toBe("anthropic/base");
|
|
1099
|
+
expect(unknownProject.configWarnings?.join("\n")).toContain(
|
|
1100
|
+
'Unknown profile "missing" selected by project config',
|
|
1101
|
+
);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("rejects invalid profile definitions with a warning instead of applying their fields", () => {
|
|
1105
|
+
const result = loadWithUserConfig(
|
|
1106
|
+
JSON.stringify({
|
|
1107
|
+
profile: "unsafe",
|
|
1108
|
+
historian: { opencode: { model: "anthropic/base" } },
|
|
1109
|
+
profiles: {
|
|
1110
|
+
unsafe: {
|
|
1111
|
+
embedding: { provider: "off" },
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
}),
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
expect(result.profile).toBeUndefined();
|
|
1118
|
+
expect(result.embedding.provider).toBe("local");
|
|
1119
|
+
expect(result.historian?.opencode?.model).toBe("anthropic/base");
|
|
1120
|
+
expect(result.configWarnings?.join("\n")).toContain("Ignoring profiles from user config");
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it("strips hostile project profile definitions while allowing only user-owned definitions", () => {
|
|
1124
|
+
const result = loadWithUserAndProjectConfig(
|
|
1125
|
+
JSON.stringify({
|
|
1126
|
+
historian: { opencode: { model: "anthropic/user-base" } },
|
|
1127
|
+
}),
|
|
1128
|
+
JSON.stringify({
|
|
1129
|
+
profile: "work",
|
|
1130
|
+
profiles: {
|
|
1131
|
+
work: {
|
|
1132
|
+
historian: { opencode: { model: "attacker/project-profile" } },
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
}),
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
expect(result.profile).toBeUndefined();
|
|
1139
|
+
expect(result.historian?.opencode?.model).toBe("anthropic/user-base");
|
|
1140
|
+
expect(result.configWarnings?.join("\n")).toContain(
|
|
1141
|
+
"Ignoring profiles from project config",
|
|
1142
|
+
);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
it("falls back loudly for inherited profile names without changing the resolved base config", () => {
|
|
1146
|
+
const userConfig = JSON.stringify({
|
|
1147
|
+
historian: { opencode: { model: "anthropic/base", fallback_models: ["openai/base"] } },
|
|
1148
|
+
profiles: { known: { historian: { opencode: { model: "anthropic/known" } } } },
|
|
1149
|
+
});
|
|
1150
|
+
const base = loadWithUserConfig(userConfig);
|
|
1151
|
+
const { configWarnings: _baseWarnings, ...baseWithoutWarnings } = base;
|
|
1152
|
+
const hostileNames = [
|
|
1153
|
+
"__proto__",
|
|
1154
|
+
"constructor",
|
|
1155
|
+
"prototype",
|
|
1156
|
+
"toString",
|
|
1157
|
+
"hasOwnProperty",
|
|
1158
|
+
"valueOf",
|
|
1159
|
+
];
|
|
1160
|
+
|
|
1161
|
+
for (const name of hostileNames) {
|
|
1162
|
+
const resolved = loadWithUserAndProjectConfig(
|
|
1163
|
+
userConfig,
|
|
1164
|
+
JSON.stringify({ profile: name }),
|
|
1165
|
+
);
|
|
1166
|
+
const { configWarnings: _warnings, ...resolvedWithoutWarnings } = resolved;
|
|
1167
|
+
|
|
1168
|
+
expect(resolved.profile).toBeUndefined();
|
|
1169
|
+
expect(resolved.configWarnings?.join("\n")).toContain(
|
|
1170
|
+
`Unknown profile "${name}" selected by project config`,
|
|
1171
|
+
);
|
|
1172
|
+
expect(resolvedWithoutWarnings).toEqual(baseWithoutWarnings);
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it("ignores a profile inherited from a polluted Object prototype", () => {
|
|
1177
|
+
const profileName = "profile_overlay_pollution_canary";
|
|
1178
|
+
const previousDescriptor = Object.getOwnPropertyDescriptor(Object.prototype, profileName);
|
|
1179
|
+
const userBase = { historian: { opencode: { model: "anthropic/base" } } };
|
|
1180
|
+
// Null-prototype input isolates lookup behavior from strict schema validation of inherited data.
|
|
1181
|
+
const profileOpenCode = Object.assign(Object.create(null), {
|
|
1182
|
+
model: "anthropic/known",
|
|
1183
|
+
});
|
|
1184
|
+
const profileHistorian = Object.assign(Object.create(null), {
|
|
1185
|
+
opencode: profileOpenCode,
|
|
1186
|
+
});
|
|
1187
|
+
const knownProfile = Object.assign(Object.create(null), {
|
|
1188
|
+
historian: profileHistorian,
|
|
1189
|
+
});
|
|
1190
|
+
const profiles = Object.assign(Object.create(null), {
|
|
1191
|
+
known: knownProfile,
|
|
1192
|
+
}) as Record<string, unknown>;
|
|
1193
|
+
Object.defineProperty(Object.prototype, profileName, {
|
|
1194
|
+
value: { historian: { opencode: { model: "attacker/inherited-overlay" } } },
|
|
1195
|
+
enumerable: true,
|
|
1196
|
+
configurable: true,
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
try {
|
|
1200
|
+
const resolution = resolveConfigProfile({
|
|
1201
|
+
userRaw: {
|
|
1202
|
+
...userBase,
|
|
1203
|
+
profiles,
|
|
1204
|
+
},
|
|
1205
|
+
projectRaw: { profile: profileName },
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
expect(resolution.activeProfile).toBeUndefined();
|
|
1209
|
+
expect(resolution.overlay).toEqual({});
|
|
1210
|
+
expect(resolution.userBase).toEqual(userBase);
|
|
1211
|
+
expect(resolution.projectBase).toEqual({});
|
|
1212
|
+
expect(resolution.warnings).toEqual([
|
|
1213
|
+
`Unknown profile "${profileName}" selected by project config; using base config without a profile.`,
|
|
1214
|
+
]);
|
|
1215
|
+
} finally {
|
|
1216
|
+
if (previousDescriptor) {
|
|
1217
|
+
Object.defineProperty(Object.prototype, profileName, previousDescriptor);
|
|
1218
|
+
} else {
|
|
1219
|
+
delete (Object.prototype as Record<string, unknown>)[profileName];
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it("treats empty, null, and non-string project selectors as no selection", () => {
|
|
1225
|
+
const userConfig = JSON.stringify({
|
|
1226
|
+
profile: "personal",
|
|
1227
|
+
historian: { opencode: { model: "anthropic/base" } },
|
|
1228
|
+
profiles: {
|
|
1229
|
+
personal: { historian: { opencode: { model: "anthropic/personal" } } },
|
|
1230
|
+
},
|
|
1231
|
+
});
|
|
1232
|
+
const invalidProjectSelectors = ["", null, { name: "work" }];
|
|
1233
|
+
|
|
1234
|
+
for (const profile of invalidProjectSelectors) {
|
|
1235
|
+
const result = loadWithUserAndProjectConfig(userConfig, JSON.stringify({ profile }));
|
|
1236
|
+
|
|
1237
|
+
expect(result.profile).toBe("personal");
|
|
1238
|
+
expect(result.historian?.opencode?.model).toBe("anthropic/personal");
|
|
1239
|
+
expect(result.configWarnings?.join("\n")).toContain(
|
|
1240
|
+
"Ignoring invalid profile selection from project config",
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
it("preserves an untouched historian Pi block when a profile overlays OpenCode", () => {
|
|
1246
|
+
const piBase = {
|
|
1247
|
+
model: { model: "github-copilot/base", thinking_level: "high" },
|
|
1248
|
+
fallback_models: [
|
|
1249
|
+
{ model: "openai/base-fallback", thinking_level: "minimal" },
|
|
1250
|
+
{ model: "github-copilot/second-base-fallback", thinking_level: "medium" },
|
|
1251
|
+
],
|
|
1252
|
+
thinking_level: "high",
|
|
1253
|
+
};
|
|
1254
|
+
const result = loadWithUserConfig(
|
|
1255
|
+
JSON.stringify({
|
|
1256
|
+
profile: "work",
|
|
1257
|
+
historian: {
|
|
1258
|
+
opencode: { model: "anthropic/base-opencode" },
|
|
1259
|
+
pi: piBase,
|
|
1260
|
+
},
|
|
1261
|
+
profiles: {
|
|
1262
|
+
work: {
|
|
1263
|
+
historian: { opencode: { model: "anthropic/work-opencode" } },
|
|
1264
|
+
},
|
|
1265
|
+
},
|
|
1266
|
+
}),
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
expect(result.historian?.opencode).toEqual({ model: "anthropic/work-opencode" });
|
|
1270
|
+
expect(result.historian?.pi).toEqual(piBase);
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
it("replaces a base fallback_models array wholesale when a profile provides one", () => {
|
|
1274
|
+
const result = loadWithUserConfig(
|
|
1275
|
+
JSON.stringify({
|
|
1276
|
+
profile: "work",
|
|
1277
|
+
historian: {
|
|
1278
|
+
opencode: {
|
|
1279
|
+
model: "anthropic/base",
|
|
1280
|
+
fallback_models: [
|
|
1281
|
+
{ model: "openai/base-first", variant: "low" },
|
|
1282
|
+
{ model: "anthropic/base-second", variant: "high" },
|
|
1283
|
+
],
|
|
1284
|
+
},
|
|
1285
|
+
},
|
|
1286
|
+
profiles: {
|
|
1287
|
+
work: {
|
|
1288
|
+
historian: {
|
|
1289
|
+
opencode: {
|
|
1290
|
+
fallback_models: [{ model: "google/work-only", variant: "medium" }],
|
|
1291
|
+
},
|
|
1292
|
+
},
|
|
1293
|
+
},
|
|
1294
|
+
},
|
|
1295
|
+
}),
|
|
1296
|
+
);
|
|
1297
|
+
|
|
1298
|
+
expect(result.historian?.opencode?.fallback_models).toEqual([
|
|
1299
|
+
{ model: "google/work-only", variant: "medium" },
|
|
1300
|
+
]);
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
it("resolves profiles independently for two projects in one process", () => {
|
|
1304
|
+
const xdg = mkdtempSync(join(tmpdir(), "mc-profile-isolation-user-"));
|
|
1305
|
+
const projectA = mkdtempSync(join(tmpdir(), "mc-profile-isolation-a-"));
|
|
1306
|
+
const projectB = mkdtempSync(join(tmpdir(), "mc-profile-isolation-b-"));
|
|
1307
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
1308
|
+
mkdirSync(join(xdg, "cortexkit"), { recursive: true });
|
|
1309
|
+
mkdirSync(join(projectA, ".cortexkit"), { recursive: true });
|
|
1310
|
+
mkdirSync(join(projectB, ".cortexkit"), { recursive: true });
|
|
1311
|
+
writeFileSync(
|
|
1312
|
+
join(xdg, "cortexkit", "magic-context.jsonc"),
|
|
1313
|
+
JSON.stringify({
|
|
1314
|
+
profiles: {
|
|
1315
|
+
work: { historian: { opencode: { model: "anthropic/work" } } },
|
|
1316
|
+
personal: {
|
|
1317
|
+
historian: { opencode: { model: "anthropic/personal" } },
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1320
|
+
}),
|
|
1321
|
+
);
|
|
1322
|
+
writeFileSync(join(projectA, ".cortexkit", "magic-context.jsonc"), '{"profile":"work"}');
|
|
1323
|
+
writeFileSync(
|
|
1324
|
+
join(projectB, ".cortexkit", "magic-context.jsonc"),
|
|
1325
|
+
'{"profile":"personal"}',
|
|
1326
|
+
);
|
|
1327
|
+
process.env.XDG_CONFIG_HOME = xdg;
|
|
1328
|
+
try {
|
|
1329
|
+
const work = loadPluginConfig(projectA);
|
|
1330
|
+
const personal = loadPluginConfig(projectB);
|
|
1331
|
+
const workAgain = loadPluginConfig(projectA);
|
|
1332
|
+
expect(work.profile).toBe("work");
|
|
1333
|
+
expect(personal.profile).toBe("personal");
|
|
1334
|
+
expect(workAgain.profile).toBe("work");
|
|
1335
|
+
expect(work.historian?.opencode?.model).toBe("anthropic/work");
|
|
1336
|
+
expect(personal.historian?.opencode?.model).toBe("anthropic/personal");
|
|
1337
|
+
expect(workAgain.historian?.opencode?.model).toBe("anthropic/work");
|
|
1338
|
+
} finally {
|
|
1339
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
1340
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
1341
|
+
rmSync(xdg, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
1342
|
+
rmSync(projectA, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
1343
|
+
rmSync(projectB, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
it("feeds the selected profile through the existing historian model resolver", () => {
|
|
1348
|
+
const config = loadWithUserAndProjectConfig(
|
|
1349
|
+
JSON.stringify({
|
|
1350
|
+
profiles: {
|
|
1351
|
+
work: {
|
|
1352
|
+
historian: {
|
|
1353
|
+
opencode: {
|
|
1354
|
+
model: { model: "anthropic/work", variant: "high" },
|
|
1355
|
+
fallback_models: [
|
|
1356
|
+
{ model: "openai/work-fallback", variant: "low" },
|
|
1357
|
+
],
|
|
1358
|
+
},
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
}),
|
|
1363
|
+
'{"profile":"work"}',
|
|
1364
|
+
);
|
|
1365
|
+
|
|
1366
|
+
// Profile selection changes only the normal resolved model input. The
|
|
1367
|
+
// existing model-switch identity path still sees canonical model IDs and
|
|
1368
|
+
// qualifier-distinct fallback attempts; profiles add no separate cache-bust class.
|
|
1369
|
+
expect(resolveHistorianModel(config, "opencode")).toEqual({
|
|
1370
|
+
primary: { model: "anthropic/work", qualifier: "high" },
|
|
1371
|
+
fallbacks: [{ model: "openai/work-fallback", qualifier: "low" }],
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
|
|
961
1376
|
describe("loadPluginConfigDetailed — prompt-surface registration owner", () => {
|
|
962
1377
|
it("captures the user default before project guidance routing is merged", () => {
|
|
963
1378
|
const xdg = mkdtempSync(join(tmpdir(), "mc-config-prompt-surface-"));
|