@cortexkit/opencode-magic-context 0.33.0 → 0.34.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/dist/config/agent-disable.d.ts +22 -0
- package/dist/config/agent-disable.d.ts.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +7 -0
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +144 -24
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config/transform-mode.d.ts +2 -0
- package/dist/config/transform-mode.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-bwa99a04.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
- package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
- package/dist/features/builtin-commands/commands.d.ts +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts +106 -0
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +0 -2
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts +0 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/index.d.ts +2 -2
- package/dist/features/magic-context/git-commits/index.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts +0 -2
- package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commits.d.ts +0 -13
- package/dist/features/magic-context/git-commits/storage-git-commits.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 +7 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts +1 -2
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/memory-migration.d.ts +0 -6
- package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +0 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts +1 -6
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/memory/verification-paths.d.ts +0 -1
- package/dist/features/magic-context/memory/verification-paths.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.map +1 -1
- package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
- package/dist/features/magic-context/mural/storage-mural.d.ts +0 -1
- package/dist/features/magic-context/mural/storage-mural.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +8 -1
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +16 -2
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
- package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
- package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/capabilities.d.ts +0 -2
- package/dist/features/magic-context/smart-notes/capabilities.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts +0 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/types.d.ts +0 -16
- package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +35 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -5
- package/dist/features/magic-context/storage-meta-persisted.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.d.ts +1 -1
- package/dist/features/magic-context/storage.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 +0 -2
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/types.d.ts +0 -3
- package/dist/hooks/auto-update-checker/types.d.ts.map +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
- package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
- package/dist/hooks/magic-context/command-handler.d.ts +2 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts +49 -0
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
- package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts +12 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
- package/dist/hooks/magic-context/compartment-runner-historian.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 +0 -12
- 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.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
- package/dist/hooks/magic-context/event-handler.d.ts +7 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +11 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +18 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +39 -0
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +19 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +30 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +27 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-kxz4tn6m.js → index-56yjaeb2.js} +56 -4
- package/dist/{index-bmkea2dq.js → index-b563sga5.js} +40 -287
- package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
- package/dist/{index-k5y60ymm.js → index-dtas91s5.js} +27 -6
- package/dist/index-fcyvznch.js +279 -0
- package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
- package/dist/{index-h86twgsp.js → index-s0mdn690.js} +2113 -1067
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1356 -627
- package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +19 -8
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +10 -0
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +3 -7
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts +8 -0
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-yh7pepvx.js → recomp-orchestrator-8e82vmvb.js} +6 -5
- package/dist/send-session-notification-6njqewzk.js +20 -0
- package/dist/shared/conflict-detector.d.ts +38 -1
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/conflict-fixer.d.ts +18 -1
- package/dist/shared/conflict-fixer.d.ts.map +1 -1
- package/dist/shared/context-limit-provenance.d.ts +3 -0
- package/dist/shared/context-limit-provenance.d.ts.map +1 -0
- package/dist/shared/escalation-bands.d.ts +9 -0
- package/dist/shared/escalation-bands.d.ts.map +1 -0
- package/dist/shared/format-threshold.d.ts +1 -1
- package/dist/shared/models-dev-cache.d.ts +29 -3
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +46 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +9 -4
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/storage-permissions.d.ts +5 -0
- package/dist/shared/storage-permissions.d.ts.map +1 -0
- package/dist/{storage-meta-session-1phy0b1z.js → storage-meta-session-xt41zbw4.js} +2 -1
- package/dist/{task-config-dn9k9szr.js → task-config-5nx3qfz8.js} +1 -2
- package/dist/tui/compaction-off.d.ts +13 -0
- package/dist/tui/compaction-off.d.ts.map +1 -0
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui-compiled/compaction-off.d.ts +13 -0
- package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
- package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
- package/package.json +8 -3
- package/src/agents/dreamer.ts +68 -0
- package/src/agents/hidden-agent-registrations.ts +331 -0
- package/src/agents/historian.ts +3 -0
- package/src/agents/language-directive.test.ts +138 -0
- package/src/agents/language-directive.ts +121 -0
- package/src/agents/magic-context-prompt.language.test.ts +35 -0
- package/src/agents/magic-context-prompt.test.ts +292 -0
- package/src/agents/magic-context-prompt.ts +182 -0
- package/src/agents/permissions.test.ts +252 -0
- package/src/agents/permissions.ts +186 -0
- package/src/agents/sidekick.ts +1 -0
- package/src/agents/smart-note-compiler.ts +1 -0
- package/src/config/agent-disable.ts +106 -0
- package/src/config/compaction-accessor-guard.test.ts +82 -0
- package/src/config/compaction-backcompat.test.ts +52 -0
- package/src/config/index.test.ts +904 -0
- package/src/config/index.ts +601 -0
- package/src/config/migrate-config-location.test.ts +332 -0
- package/src/config/migrate-config-location.ts +635 -0
- package/src/config/migrate-dreamer-v2.test.ts +287 -0
- package/src/config/migrate-dreamer-v2.ts +336 -0
- package/src/config/migrate-experimental.ts +159 -0
- package/src/config/project-security.test.ts +366 -0
- package/src/config/project-security.ts +588 -0
- package/src/config/prune-config-leaf.test.ts +78 -0
- package/src/config/prune-config-leaf.ts +68 -0
- package/src/config/removed-config-keys.test.ts +116 -0
- package/src/config/schema/agent-overrides.ts +45 -0
- package/src/config/schema/magic-context.test.ts +358 -0
- package/src/config/schema/magic-context.ts +1002 -0
- package/src/config/transform-mode.test.ts +62 -0
- package/src/config/transform-mode.ts +34 -0
- package/src/config/variable.test.ts +323 -0
- package/src/config/variable.ts +192 -0
- package/src/features/magic-context/defaults.ts +1 -0
- package/src/features/magic-context/dreamer/cron.ts +243 -0
- package/src/features/magic-context/dreamer/task-registry.ts +104 -0
- package/src/shared/conflict-detector.test.ts +251 -1
- package/src/shared/conflict-detector.ts +84 -5
- package/src/shared/conflict-fixer.test.ts +309 -0
- package/src/shared/conflict-fixer.ts +82 -5
- package/src/shared/context-limit-provenance.ts +6 -0
- package/src/shared/escalation-bands.test.ts +23 -0
- package/src/shared/escalation-bands.ts +18 -0
- package/src/shared/format-threshold.ts +1 -1
- package/src/shared/models-dev-cache.test.ts +171 -0
- package/src/shared/models-dev-cache.ts +195 -38
- package/src/shared/rpc-server.ts +27 -18
- package/src/shared/rpc-types.ts +46 -1
- package/src/shared/sqlite.ts +13 -33
- package/src/shared/storage-permissions.ts +20 -0
- package/src/tui/compaction-off.test.ts +56 -0
- package/src/tui/compaction-off.ts +29 -0
- package/src/tui/data/context-db.ts +6 -0
- package/src/tui/index.tsx +113 -65
- package/src/tui/slots/sidebar-content.tsx +139 -83
- package/src/tui-compiled/compaction-off.ts +29 -0
- package/src/tui-compiled/data/context-db.ts +6 -0
- package/src/tui-compiled/index.tsx +558 -455
- package/src/tui-compiled/slots/sidebar-content.tsx +237 -161
- package/dist/config/schema.d.ts +0 -3
- package/dist/config/schema.d.ts.map +0 -1
- package/dist/features/magic-context/plugin-messages.d.ts +0 -75
- package/dist/features/magic-context/plugin-messages.d.ts.map +0 -1
- package/dist/hooks/magic-context/compartment-runner-state-xml.d.ts +0 -21
- package/dist/hooks/magic-context/compartment-runner-state-xml.d.ts.map +0 -1
- package/dist/index-anhrfygk.js +0 -57
- package/dist/send-session-notification-tghq9gnh.js +0 -11
- package/dist/shared/opencode-compaction-detector.d.ts +0 -2
- package/dist/shared/opencode-compaction-detector.d.ts.map +0 -1
- package/dist/shared/transcript-opencode.d.ts +0 -71
- package/dist/shared/transcript-opencode.d.ts.map +0 -1
- package/dist/tools/index.d.ts +0 -6
- package/dist/tools/index.d.ts.map +0 -1
- package/src/shared/opencode-compaction-detector.test.ts +0 -239
- package/src/shared/opencode-compaction-detector.ts +0 -81
- package/src/shared/transcript-opencode.ts +0 -302
- /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { RUST_COMPACTION_OFF_WARNING, resolveTransformMode } from "./transform-mode";
|
|
4
|
+
|
|
5
|
+
describe("resolveTransformMode", () => {
|
|
6
|
+
it("falls back to ts and warns when rust lacks user-level subc", () => {
|
|
7
|
+
expect(
|
|
8
|
+
resolveTransformMode({
|
|
9
|
+
configured: "rust",
|
|
10
|
+
userTierHasSubc: false,
|
|
11
|
+
compactionEnabled: true,
|
|
12
|
+
}),
|
|
13
|
+
).toEqual({
|
|
14
|
+
mode: "ts",
|
|
15
|
+
warnings: ["rust mode requires user-level subc configuration; running ts."],
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("keeps rust when trusted user-level subc is present", () => {
|
|
20
|
+
expect(
|
|
21
|
+
resolveTransformMode({
|
|
22
|
+
configured: "rust",
|
|
23
|
+
userTierHasSubc: true,
|
|
24
|
+
compactionEnabled: true,
|
|
25
|
+
}),
|
|
26
|
+
).toEqual({ mode: "rust", warnings: [] });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("keeps ts without warnings when ts is configured", () => {
|
|
30
|
+
expect(
|
|
31
|
+
resolveTransformMode({
|
|
32
|
+
configured: "ts",
|
|
33
|
+
userTierHasSubc: false,
|
|
34
|
+
compactionEnabled: true,
|
|
35
|
+
}),
|
|
36
|
+
).toEqual({ mode: "ts", warnings: [] });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps rust in the default compaction-on mode without a warning", () => {
|
|
40
|
+
const result = resolveTransformMode({
|
|
41
|
+
configured: "rust",
|
|
42
|
+
userTierHasSubc: true,
|
|
43
|
+
compactionEnabled: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result.mode).toBe("rust");
|
|
47
|
+
expect(result.warnings).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("downgrades rust to ts with one warning when compaction is off", () => {
|
|
51
|
+
expect(
|
|
52
|
+
resolveTransformMode({
|
|
53
|
+
configured: "rust",
|
|
54
|
+
userTierHasSubc: true,
|
|
55
|
+
compactionEnabled: false,
|
|
56
|
+
}),
|
|
57
|
+
).toEqual({
|
|
58
|
+
mode: "ts",
|
|
59
|
+
warnings: [RUST_COMPACTION_OFF_WARNING],
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ResolvedTransformMode = "ts" | "rust";
|
|
2
|
+
|
|
3
|
+
export interface ResolveTransformModeArgs {
|
|
4
|
+
configured: ResolvedTransformMode;
|
|
5
|
+
userTierHasSubc: boolean;
|
|
6
|
+
compactionEnabled: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const RUST_COMPACTION_OFF_WARNING =
|
|
10
|
+
"compaction-off mode does not support rust transform mode; using the TypeScript transform.";
|
|
11
|
+
|
|
12
|
+
const RUST_REQUIRES_USER_SUBC_WARNING =
|
|
13
|
+
"rust mode requires user-level subc configuration; running ts.";
|
|
14
|
+
|
|
15
|
+
export function resolveTransformMode(args: ResolveTransformModeArgs): {
|
|
16
|
+
mode: ResolvedTransformMode;
|
|
17
|
+
warnings: string[];
|
|
18
|
+
} {
|
|
19
|
+
if (args.configured === "rust" && !args.compactionEnabled) {
|
|
20
|
+
return {
|
|
21
|
+
mode: "ts",
|
|
22
|
+
warnings: [RUST_COMPACTION_OFF_WARNING],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (args.configured === "rust" && !args.userTierHasSubc) {
|
|
27
|
+
return {
|
|
28
|
+
mode: "ts",
|
|
29
|
+
warnings: [RUST_REQUIRES_USER_SUBC_WARNING],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { mode: args.configured, warnings: [] };
|
|
34
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { join, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { substituteConfigVariables } from "./variable";
|
|
7
|
+
|
|
8
|
+
describe("substituteConfigVariables", () => {
|
|
9
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-variable-test-"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
try {
|
|
18
|
+
rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
19
|
+
} catch {
|
|
20
|
+
/* Ignore EBUSY on Windows */
|
|
21
|
+
}
|
|
22
|
+
process.env = { ...ORIGINAL_ENV };
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("env substitution", () => {
|
|
26
|
+
it("replaces {env:VAR} with process.env value", () => {
|
|
27
|
+
process.env.MC_TEST_KEY = "sk-real-value";
|
|
28
|
+
const input = `{ "api_key": "{env:MC_TEST_KEY}" }`;
|
|
29
|
+
|
|
30
|
+
const result = substituteConfigVariables({ text: input });
|
|
31
|
+
|
|
32
|
+
expect(result.text).toBe(`{ "api_key": "sk-real-value" }`);
|
|
33
|
+
expect(result.warnings).toHaveLength(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("trims whitespace inside env token", () => {
|
|
37
|
+
process.env.MC_TEST_KEY = "trimmed-value";
|
|
38
|
+
const input = `{ "api_key": "{env: MC_TEST_KEY }" }`;
|
|
39
|
+
|
|
40
|
+
const result = substituteConfigVariables({ text: input });
|
|
41
|
+
|
|
42
|
+
expect(result.text).toBe(`{ "api_key": "trimmed-value" }`);
|
|
43
|
+
expect(result.warnings).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("JSON-escapes quotes in env values so JSONC parsing survives", () => {
|
|
47
|
+
process.env.MC_QUOTED = 'sk-"quoted"-value';
|
|
48
|
+
const input = `{ "api_key": "{env:MC_QUOTED}" }`;
|
|
49
|
+
|
|
50
|
+
const result = substituteConfigVariables({ text: input });
|
|
51
|
+
|
|
52
|
+
expect(result.text).toBe(`{ "api_key": "sk-\\"quoted\\"-value" }`);
|
|
53
|
+
expect(JSON.parse(result.text).api_key).toBe('sk-"quoted"-value');
|
|
54
|
+
expect(result.warnings).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("JSON-escapes newlines in env values so JSONC parsing survives", () => {
|
|
58
|
+
process.env.MC_MULTILINE = "line1\nline2";
|
|
59
|
+
const input = `{ "api_key": "{env:MC_MULTILINE}" }`;
|
|
60
|
+
|
|
61
|
+
const result = substituteConfigVariables({ text: input });
|
|
62
|
+
|
|
63
|
+
expect(result.text).toBe(`{ "api_key": "line1\\nline2" }`);
|
|
64
|
+
expect(JSON.parse(result.text).api_key).toBe("line1\nline2");
|
|
65
|
+
expect(result.warnings).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("prevents env values from injecting sibling JSON keys", () => {
|
|
69
|
+
process.env.MC_INJECT = 'abc", "provider": "off';
|
|
70
|
+
const input = `{ "api_key": "{env:MC_INJECT}" }`;
|
|
71
|
+
|
|
72
|
+
const result = substituteConfigVariables({ text: input });
|
|
73
|
+
const parsed = JSON.parse(result.text);
|
|
74
|
+
|
|
75
|
+
expect(parsed.api_key).toBe('abc", "provider": "off');
|
|
76
|
+
expect(parsed.provider).toBeUndefined();
|
|
77
|
+
expect(result.warnings).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("emits warning and empty string for missing env var", () => {
|
|
81
|
+
delete process.env.MC_MISSING_VAR;
|
|
82
|
+
const input = `{ "api_key": "{env:MC_MISSING_VAR}" }`;
|
|
83
|
+
|
|
84
|
+
const result = substituteConfigVariables({ text: input });
|
|
85
|
+
|
|
86
|
+
expect(result.text).toBe(`{ "api_key": "" }`);
|
|
87
|
+
expect(result.warnings).toHaveLength(1);
|
|
88
|
+
expect(result.warnings[0]).toContain("MC_MISSING_VAR");
|
|
89
|
+
expect(result.warnings[0]).toContain("not set");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("emits warning for empty-string env var", () => {
|
|
93
|
+
process.env.MC_EMPTY = "";
|
|
94
|
+
const input = `{ "api_key": "{env:MC_EMPTY}" }`;
|
|
95
|
+
|
|
96
|
+
const result = substituteConfigVariables({ text: input });
|
|
97
|
+
|
|
98
|
+
expect(result.text).toBe(`{ "api_key": "" }`);
|
|
99
|
+
expect(result.warnings).toHaveLength(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("passes {env:} literally through (matches OpenCode regex: at least one char required)", () => {
|
|
103
|
+
const input = `{ "api_key": "{env:}" }`;
|
|
104
|
+
|
|
105
|
+
const result = substituteConfigVariables({ text: input });
|
|
106
|
+
|
|
107
|
+
// The regex `{env:([^}]+)}` requires ≥1 char between the colon and
|
|
108
|
+
// brace. An empty `{env:}` is not a valid token and passes through
|
|
109
|
+
// to be parsed as literal JSONC text.
|
|
110
|
+
expect(result.text).toBe(input);
|
|
111
|
+
expect(result.warnings).toHaveLength(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("handles multiple env tokens in one text", () => {
|
|
115
|
+
process.env.MC_A = "alpha";
|
|
116
|
+
process.env.MC_B = "beta";
|
|
117
|
+
const input = `{ "a": "{env:MC_A}", "b": "{env:MC_B}", "c": "{env:MC_MISSING}" }`;
|
|
118
|
+
|
|
119
|
+
const result = substituteConfigVariables({ text: input });
|
|
120
|
+
|
|
121
|
+
expect(result.text).toBe(`{ "a": "alpha", "b": "beta", "c": "" }`);
|
|
122
|
+
expect(result.warnings).toHaveLength(1);
|
|
123
|
+
expect(result.warnings[0]).toContain("MC_MISSING");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("file substitution", () => {
|
|
128
|
+
it("inlines file contents for absolute path", () => {
|
|
129
|
+
const keyFile = join(tmpDir, "key.txt");
|
|
130
|
+
writeFileSync(keyFile, "sk-from-file\n");
|
|
131
|
+
const input = `{ "api_key": "{file:${keyFile}}" }`;
|
|
132
|
+
|
|
133
|
+
const result = substituteConfigVariables({ text: input });
|
|
134
|
+
|
|
135
|
+
expect(result.text).toBe(`{ "api_key": "sk-from-file" }`);
|
|
136
|
+
expect(result.warnings).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("resolves relative path against configPath directory", () => {
|
|
140
|
+
const keyFile = join(tmpDir, "key.txt");
|
|
141
|
+
writeFileSync(keyFile, "relative-value");
|
|
142
|
+
const configPath = join(tmpDir, "magic-context.jsonc");
|
|
143
|
+
|
|
144
|
+
const input = `{ "api_key": "{file:./key.txt}" }`;
|
|
145
|
+
const result = substituteConfigVariables({ text: input, configPath });
|
|
146
|
+
|
|
147
|
+
expect(result.text).toBe(`{ "api_key": "relative-value" }`);
|
|
148
|
+
expect(result.warnings).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("resolves relative path without leading ./", () => {
|
|
152
|
+
const keyFile = join(tmpDir, "key.txt");
|
|
153
|
+
writeFileSync(keyFile, "no-dot-slash");
|
|
154
|
+
const configPath = join(tmpDir, "magic-context.jsonc");
|
|
155
|
+
|
|
156
|
+
const input = `{ "api_key": "{file:key.txt}" }`;
|
|
157
|
+
const result = substituteConfigVariables({ text: input, configPath });
|
|
158
|
+
|
|
159
|
+
expect(result.text).toBe(`{ "api_key": "no-dot-slash" }`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("expands ~/ to home directory", () => {
|
|
163
|
+
const input = `{ "marker": "{file:~/__mc-never-exists-${Date.now()}}" }`;
|
|
164
|
+
|
|
165
|
+
const result = substituteConfigVariables({ text: input });
|
|
166
|
+
|
|
167
|
+
// File doesn't exist, so warning fires — but warning must show it
|
|
168
|
+
// resolved under homedir, proving the ~ expansion happened.
|
|
169
|
+
expect(result.warnings).toHaveLength(1);
|
|
170
|
+
expect(result.warnings[0]).toContain(homedir() + sep);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("JSON-escapes quotes and newlines in file contents", () => {
|
|
174
|
+
const keyFile = join(tmpDir, "multiline.txt");
|
|
175
|
+
writeFileSync(keyFile, 'line1 with "quote"\nline2');
|
|
176
|
+
const input = `{ "value": "{file:${keyFile}}" }`;
|
|
177
|
+
|
|
178
|
+
const result = substituteConfigVariables({ text: input });
|
|
179
|
+
|
|
180
|
+
// Escaped so the outer JSONC string still parses cleanly.
|
|
181
|
+
expect(result.text).toBe(`{ "value": "line1 with \\"quote\\"\\nline2" }`);
|
|
182
|
+
const parsed = JSON.parse(result.text);
|
|
183
|
+
expect(parsed.value).toBe('line1 with "quote"\nline2');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("emits warning and empty string for missing file", () => {
|
|
187
|
+
const missing = join(tmpDir, "never-exists.txt");
|
|
188
|
+
const input = `{ "api_key": "{file:${missing}}" }`;
|
|
189
|
+
|
|
190
|
+
const result = substituteConfigVariables({ text: input });
|
|
191
|
+
|
|
192
|
+
expect(result.text).toBe(`{ "api_key": "" }`);
|
|
193
|
+
expect(result.warnings).toHaveLength(1);
|
|
194
|
+
expect(result.warnings[0]).toContain("not found");
|
|
195
|
+
expect(result.warnings[0]).toContain(missing);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("passes {file:} literally through (matches OpenCode regex: at least one char required)", () => {
|
|
199
|
+
const input = `{ "api_key": "{file:}" }`;
|
|
200
|
+
|
|
201
|
+
const result = substituteConfigVariables({ text: input });
|
|
202
|
+
|
|
203
|
+
// Same reasoning as {env:} — empty token pattern doesn't match.
|
|
204
|
+
expect(result.text).toBe(input);
|
|
205
|
+
expect(result.warnings).toHaveLength(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("suppresses {file:} expansion inside // line comments", () => {
|
|
209
|
+
const keyFile = join(tmpDir, "key.txt");
|
|
210
|
+
writeFileSync(keyFile, "should-not-appear");
|
|
211
|
+
const input = [
|
|
212
|
+
`{`,
|
|
213
|
+
` // see docs: {file:${keyFile}}`,
|
|
214
|
+
` "other": "value"`,
|
|
215
|
+
`}`,
|
|
216
|
+
].join("\n");
|
|
217
|
+
|
|
218
|
+
const result = substituteConfigVariables({ text: input });
|
|
219
|
+
|
|
220
|
+
expect(result.text).not.toContain(`{file:${keyFile}}`);
|
|
221
|
+
expect(result.text).not.toContain("should-not-appear");
|
|
222
|
+
expect(result.warnings).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("suppresses {file:} expansion inside block comments", () => {
|
|
226
|
+
const keyFile = join(tmpDir, "key.txt");
|
|
227
|
+
writeFileSync(keyFile, "should-not-appear");
|
|
228
|
+
const input = [
|
|
229
|
+
`{`,
|
|
230
|
+
` /* see docs: {file:${keyFile}} */`,
|
|
231
|
+
` "other": "value"`,
|
|
232
|
+
`}`,
|
|
233
|
+
].join("\n");
|
|
234
|
+
|
|
235
|
+
const result = substituteConfigVariables({ text: input });
|
|
236
|
+
|
|
237
|
+
expect(result.text).not.toContain(`{file:${keyFile}}`);
|
|
238
|
+
expect(result.text).not.toContain("should-not-appear");
|
|
239
|
+
expect(result.warnings).toHaveLength(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("still expands {file:} tokens inside strings that contain URL comment markers", () => {
|
|
243
|
+
const keyFile = join(tmpDir, "key.txt");
|
|
244
|
+
writeFileSync(keyFile, "token-value");
|
|
245
|
+
const input = `{ "endpoint": "https://example.test/*literal*/{file:${keyFile}}" }`;
|
|
246
|
+
|
|
247
|
+
const result = substituteConfigVariables({ text: input });
|
|
248
|
+
|
|
249
|
+
expect(result.text).toBe(
|
|
250
|
+
`{ "endpoint": "https://example.test/*literal*/token-value" }`,
|
|
251
|
+
);
|
|
252
|
+
expect(JSON.parse(result.text).endpoint).toBe(
|
|
253
|
+
"https://example.test/*literal*/token-value",
|
|
254
|
+
);
|
|
255
|
+
expect(result.warnings).toHaveLength(0);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("combined substitution", () => {
|
|
260
|
+
it("handles env and file tokens together", () => {
|
|
261
|
+
process.env.MC_COMBINED = "env-val";
|
|
262
|
+
const keyFile = join(tmpDir, "combined.txt");
|
|
263
|
+
writeFileSync(keyFile, "file-val");
|
|
264
|
+
|
|
265
|
+
const input = `{ "e": "{env:MC_COMBINED}", "f": "{file:${keyFile}}" }`;
|
|
266
|
+
const result = substituteConfigVariables({ text: input });
|
|
267
|
+
|
|
268
|
+
expect(result.text).toBe(`{ "e": "env-val", "f": "file-val" }`);
|
|
269
|
+
expect(result.warnings).toHaveLength(0);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("env tokens inside {file:} path expand before file read", () => {
|
|
273
|
+
// Tokens are substituted left-to-right in passes: env first, then
|
|
274
|
+
// file. So an env var naming a file path gets resolved during file
|
|
275
|
+
// substitution.
|
|
276
|
+
process.env.MC_FILE_DIR = tmpDir;
|
|
277
|
+
const keyFile = join(tmpDir, "indirect.txt");
|
|
278
|
+
writeFileSync(keyFile, "indirect-value");
|
|
279
|
+
|
|
280
|
+
const input = `{ "api_key": "{file:{env:MC_FILE_DIR}/indirect.txt}" }`;
|
|
281
|
+
const result = substituteConfigVariables({ text: input });
|
|
282
|
+
|
|
283
|
+
expect(result.text).toBe(`{ "api_key": "indirect-value" }`);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("no-op cases", () => {
|
|
288
|
+
it("returns text unchanged when no tokens present", () => {
|
|
289
|
+
const input = `{ "api_key": "literal-value", "provider": "openai-compatible" }`;
|
|
290
|
+
|
|
291
|
+
const result = substituteConfigVariables({ text: input });
|
|
292
|
+
|
|
293
|
+
expect(result.text).toBe(input);
|
|
294
|
+
expect(result.warnings).toHaveLength(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("leaves partial patterns like {env alone", () => {
|
|
298
|
+
const input = `{ "note": "this {env is not a token" }`;
|
|
299
|
+
|
|
300
|
+
const result = substituteConfigVariables({ text: input });
|
|
301
|
+
|
|
302
|
+
expect(result.text).toBe(input);
|
|
303
|
+
expect(result.warnings).toHaveLength(0);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("sensitive-path warnings", () => {
|
|
308
|
+
it("warns when a user {file:} resolves into ~/.ssh", () => {
|
|
309
|
+
const input = `{ "key": "{file:~/.ssh/id_rsa}" }`;
|
|
310
|
+
const result = substituteConfigVariables({ text: input });
|
|
311
|
+
// Whether the file exists or not, the sensitive-path warning fires
|
|
312
|
+
// before the existence check.
|
|
313
|
+
expect(result.warnings.some((w) => w.includes("sensitive path"))).toBe(true);
|
|
314
|
+
expect(result.warnings.some((w) => w.includes("SSH keys"))).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("does NOT warn for an ordinary {file:} path", () => {
|
|
318
|
+
const input = `{ "prompt": "{file:~/notes/context.md}" }`;
|
|
319
|
+
const result = substituteConfigVariables({ text: input });
|
|
320
|
+
expect(result.warnings.some((w) => w.includes("sensitive path"))).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { stripJsonComments } from "../shared/jsonc-parser";
|
|
6
|
+
|
|
7
|
+
export interface SubstituteInput {
|
|
8
|
+
/** Raw config text before JSONC parsing. */
|
|
9
|
+
text: string;
|
|
10
|
+
/**
|
|
11
|
+
* Path of the config file the text came from. Used to resolve relative
|
|
12
|
+
* `{file:...}` references and to emit useful warnings. Pass undefined for
|
|
13
|
+
* virtual/synthetic inputs (tests) — in that case `{file:}` tokens with
|
|
14
|
+
* relative paths resolve against `cwd`, which is rarely what callers want,
|
|
15
|
+
* so callers should prefer passing a real path when one exists.
|
|
16
|
+
*/
|
|
17
|
+
configPath?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Project-level config files are untrusted repository input. Do not expand
|
|
20
|
+
* secret-bearing tokens there; leave them literal and warn instead.
|
|
21
|
+
*/
|
|
22
|
+
isProjectConfig?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SubstituteResult {
|
|
26
|
+
/** Config text with all `{env:X}` and `{file:path}` tokens replaced. */
|
|
27
|
+
text: string;
|
|
28
|
+
/**
|
|
29
|
+
* Human-readable warnings for missing env vars, unreadable files, and
|
|
30
|
+
* unresolved tokens that fell back to empty string. Safe to surface to the
|
|
31
|
+
* user via logger/toast/startup notification.
|
|
32
|
+
*/
|
|
33
|
+
warnings: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ENV_PATTERN = /\{env:([^}]+)\}/g;
|
|
37
|
+
const FILE_PATTERN = /\{file:([^}]+)\}/g;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Directories that almost never belong in a config file's `{file:}` inline.
|
|
41
|
+
* Returns a short human label when `resolvedPath` sits inside one, else null.
|
|
42
|
+
* Used to WARN (not block) on user-level config — see the call site.
|
|
43
|
+
*/
|
|
44
|
+
function sensitiveFilePathReason(resolvedPath: string): string | null {
|
|
45
|
+
const home = homedir();
|
|
46
|
+
const sensitiveDirs: Array<{ dir: string; label: string }> = [
|
|
47
|
+
{ dir: resolve(home, ".ssh"), label: "SSH keys" },
|
|
48
|
+
{ dir: resolve(home, ".aws"), label: "AWS credentials" },
|
|
49
|
+
{ dir: resolve(home, ".gnupg"), label: "GnuPG keyring" },
|
|
50
|
+
{ dir: resolve(home, ".config", "gh"), label: "GitHub CLI auth" },
|
|
51
|
+
];
|
|
52
|
+
for (const { dir, label } of sensitiveDirs) {
|
|
53
|
+
if (resolvedPath === dir || resolvedPath.startsWith(`${dir}/`)) {
|
|
54
|
+
return label;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Expand `{env:VAR}` and `{file:path}` tokens in raw config text.
|
|
62
|
+
*
|
|
63
|
+
* Mirrors OpenCode's `ConfigVariable.substitute` semantics so users can share
|
|
64
|
+
* the same patterns across `opencode.json(c)` and `magic-context.jsonc`:
|
|
65
|
+
* - `{env:VAR}` → `process.env.VAR` (trimmed key), JSON-escaped for safe inlining, empty string when missing
|
|
66
|
+
* - `{file:~/path}` → contents of `~/path`, JSON-escaped for safe inlining
|
|
67
|
+
* - `{file:./rel}` or `{file:rel}` → resolved against the config file's dir
|
|
68
|
+
* - `{file:/abs}` → resolved as absolute
|
|
69
|
+
*
|
|
70
|
+
* Unlike OpenCode we treat missing values as warnings rather than hard errors:
|
|
71
|
+
* magic-context config is less critical than the main OpenCode config, and a
|
|
72
|
+
* typo in an optional embedding key should not prevent the plugin from loading
|
|
73
|
+
* with other (valid) settings.
|
|
74
|
+
*
|
|
75
|
+
* File and env value substitution is JSON-escaped (wrapped then unwrapped
|
|
76
|
+
* through `JSON.stringify`) so line breaks, quotes, and backslashes survive
|
|
77
|
+
* the subsequent JSONC parse.
|
|
78
|
+
*/
|
|
79
|
+
export function substituteConfigVariables(input: SubstituteInput): SubstituteResult {
|
|
80
|
+
const warnings: string[] = [];
|
|
81
|
+
let text = input.text;
|
|
82
|
+
|
|
83
|
+
if (input.isProjectConfig) {
|
|
84
|
+
const hasEnvTokens = ENV_PATTERN.test(text);
|
|
85
|
+
const hasFileTokens = FILE_PATTERN.test(text);
|
|
86
|
+
ENV_PATTERN.lastIndex = 0;
|
|
87
|
+
FILE_PATTERN.lastIndex = 0;
|
|
88
|
+
if (hasEnvTokens || hasFileTokens) {
|
|
89
|
+
const tokenTypes = [
|
|
90
|
+
hasEnvTokens ? "{env:}" : undefined,
|
|
91
|
+
hasFileTokens ? "{file:}" : undefined,
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(" and ");
|
|
95
|
+
warnings.push(
|
|
96
|
+
`Project-level config no longer supports ${tokenTypes} tokens for security reasons; leaving tokens literal. Move secret expansion to user-level config.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return { text, warnings };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Strip JSONC comments before substitution so tokens in comments cannot
|
|
103
|
+
// trigger env/file side effects. The shared parser helper is string-aware,
|
|
104
|
+
// so URL strings and literal comment markers inside strings are preserved.
|
|
105
|
+
text = stripJsonComments(text);
|
|
106
|
+
|
|
107
|
+
text = text.replace(ENV_PATTERN, (_, rawName: string) => {
|
|
108
|
+
const varName = rawName.trim();
|
|
109
|
+
const value = varName ? process.env[varName] : undefined;
|
|
110
|
+
if (value === undefined || value === "") {
|
|
111
|
+
warnings.push(
|
|
112
|
+
`Environment variable ${varName} is not set (referenced via {env:${varName}}); using empty string`,
|
|
113
|
+
);
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return JSON.stringify(value).slice(1, -1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const fileMatches = Array.from(text.matchAll(FILE_PATTERN));
|
|
121
|
+
if (fileMatches.length === 0) {
|
|
122
|
+
return { text, warnings };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const configDir = input.configPath ? dirname(input.configPath) : process.cwd();
|
|
126
|
+
|
|
127
|
+
let output = "";
|
|
128
|
+
let cursor = 0;
|
|
129
|
+
|
|
130
|
+
for (const match of fileMatches) {
|
|
131
|
+
const token = match[0];
|
|
132
|
+
const rawPath = match[1] ?? "";
|
|
133
|
+
const index = match.index ?? 0;
|
|
134
|
+
|
|
135
|
+
output += text.slice(cursor, index);
|
|
136
|
+
cursor = index + token.length;
|
|
137
|
+
|
|
138
|
+
// Skip tokens inside line comments: agents-only feature, matches OpenCode.
|
|
139
|
+
// We run before JSONC parsing so raw comments are still in the text.
|
|
140
|
+
const lineStart = text.lastIndexOf("\n", index - 1) + 1;
|
|
141
|
+
const prefix = text.slice(lineStart, index).trimStart();
|
|
142
|
+
if (prefix.startsWith("//")) {
|
|
143
|
+
output += token;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let filePath = rawPath.trim();
|
|
148
|
+
if (filePath.startsWith("~/")) {
|
|
149
|
+
filePath = resolve(homedir(), filePath.slice(2));
|
|
150
|
+
} else if (!isAbsolute(filePath)) {
|
|
151
|
+
filePath = resolve(configDir, filePath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Warn (don't block — this is user-level config, mirroring OpenCode's
|
|
155
|
+
// {file:} semantics) when a {file:} token resolves into a known-sensitive
|
|
156
|
+
// directory. A user pasting `{file:~/.ssh/id_rsa}` from a copied snippet
|
|
157
|
+
// would otherwise silently inline a private key into the prompt.
|
|
158
|
+
const sensitiveReason = sensitiveFilePathReason(filePath);
|
|
159
|
+
if (sensitiveReason) {
|
|
160
|
+
warnings.push(
|
|
161
|
+
`${token} resolves to a sensitive path (${sensitiveReason}: ${filePath}); ` +
|
|
162
|
+
"inlining its contents into config — make sure this is intentional.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!existsSync(filePath)) {
|
|
167
|
+
warnings.push(
|
|
168
|
+
`File not found for ${token} (resolved to ${filePath}); using empty string`,
|
|
169
|
+
);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let contents: string;
|
|
174
|
+
try {
|
|
175
|
+
contents = readFileSync(filePath, "utf-8").trim();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
178
|
+
warnings.push(
|
|
179
|
+
`Failed to read file for ${token} (${filePath}): ${message}; using empty string`,
|
|
180
|
+
);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// JSON-escape so embedded quotes, newlines, and backslashes don't break
|
|
185
|
+
// the surrounding JSONC. Slice(1, -1) strips the outer quotes that
|
|
186
|
+
// JSON.stringify adds so the token stays inside the caller's own string.
|
|
187
|
+
output += JSON.stringify(contents).slice(1, -1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
output += text.slice(cursor);
|
|
191
|
+
return { text: output, warnings };
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_PROTECTED_TAGS = 20;
|