pi-ghost-autocomplete 0.3.1 → 0.3.2

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.
Files changed (155) hide show
  1. package/README.md +101 -6
  2. package/dist/bench-cli.js +81 -5
  3. package/dist/bench-cli.js.map +1 -1
  4. package/dist/bench.d.ts +130 -2
  5. package/dist/bench.d.ts.map +1 -1
  6. package/dist/bench.js +285 -4
  7. package/dist/bench.js.map +1 -1
  8. package/dist/cache.d.ts +124 -0
  9. package/dist/cache.d.ts.map +1 -0
  10. package/dist/cache.js +173 -0
  11. package/dist/cache.js.map +1 -0
  12. package/dist/candidate-rerank.d.ts +57 -0
  13. package/dist/candidate-rerank.d.ts.map +1 -0
  14. package/dist/candidate-rerank.js +78 -0
  15. package/dist/candidate-rerank.js.map +1 -0
  16. package/dist/config.d.ts +43 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +148 -18
  19. package/dist/config.js.map +1 -1
  20. package/dist/context-aggregator.d.ts +14 -5
  21. package/dist/context-aggregator.d.ts.map +1 -1
  22. package/dist/context-aggregator.js +38 -9
  23. package/dist/context-aggregator.js.map +1 -1
  24. package/dist/context-selector.d.ts +128 -0
  25. package/dist/context-selector.d.ts.map +1 -0
  26. package/dist/context-selector.js +300 -0
  27. package/dist/context-selector.js.map +1 -0
  28. package/dist/cycle-manager.d.ts +102 -0
  29. package/dist/cycle-manager.d.ts.map +1 -0
  30. package/dist/cycle-manager.js +168 -0
  31. package/dist/cycle-manager.js.map +1 -0
  32. package/dist/edit-history-ring.d.ts +148 -0
  33. package/dist/edit-history-ring.d.ts.map +1 -0
  34. package/dist/edit-history-ring.js +262 -0
  35. package/dist/edit-history-ring.js.map +1 -0
  36. package/dist/entropy-stop.d.ts +102 -0
  37. package/dist/entropy-stop.d.ts.map +1 -0
  38. package/dist/entropy-stop.js +141 -0
  39. package/dist/entropy-stop.js.map +1 -0
  40. package/dist/eval-dataset.d.ts +66 -0
  41. package/dist/eval-dataset.d.ts.map +1 -0
  42. package/dist/eval-dataset.js +127 -0
  43. package/dist/eval-dataset.js.map +1 -0
  44. package/dist/fast-path-engine.d.ts +286 -0
  45. package/dist/fast-path-engine.d.ts.map +1 -0
  46. package/dist/fast-path-engine.js +341 -0
  47. package/dist/fast-path-engine.js.map +1 -0
  48. package/dist/fuzzy-matcher.d.ts +8 -0
  49. package/dist/fuzzy-matcher.d.ts.map +1 -1
  50. package/dist/fuzzy-matcher.js +5 -3
  51. package/dist/fuzzy-matcher.js.map +1 -1
  52. package/dist/ghost-editor.d.ts +321 -4
  53. package/dist/ghost-editor.d.ts.map +1 -1
  54. package/dist/ghost-editor.js +1190 -144
  55. package/dist/ghost-editor.js.map +1 -1
  56. package/dist/index.d.ts +17 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +81 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/keystroke-timer.d.ts +59 -0
  61. package/dist/keystroke-timer.d.ts.map +1 -0
  62. package/dist/keystroke-timer.js +94 -0
  63. package/dist/keystroke-timer.js.map +1 -0
  64. package/dist/metrics-builder.d.ts +71 -0
  65. package/dist/metrics-builder.d.ts.map +1 -0
  66. package/dist/metrics-builder.js +71 -0
  67. package/dist/metrics-builder.js.map +1 -0
  68. package/dist/metrics.d.ts +99 -2
  69. package/dist/metrics.d.ts.map +1 -1
  70. package/dist/metrics.js +131 -17
  71. package/dist/metrics.js.map +1 -1
  72. package/dist/path-matcher.d.ts +9 -0
  73. package/dist/path-matcher.d.ts.map +1 -1
  74. package/dist/path-matcher.js +5 -2
  75. package/dist/path-matcher.js.map +1 -1
  76. package/dist/profile-store.d.ts +82 -0
  77. package/dist/profile-store.d.ts.map +1 -0
  78. package/dist/profile-store.js +199 -0
  79. package/dist/profile-store.js.map +1 -0
  80. package/dist/profile.d.ts +185 -0
  81. package/dist/profile.d.ts.map +1 -0
  82. package/dist/profile.js +434 -0
  83. package/dist/profile.js.map +1 -0
  84. package/dist/providers/cloud-provider.d.ts +10 -1
  85. package/dist/providers/cloud-provider.d.ts.map +1 -1
  86. package/dist/providers/cloud-provider.js +71 -14
  87. package/dist/providers/cloud-provider.js.map +1 -1
  88. package/dist/providers/local-provider.d.ts +8 -1
  89. package/dist/providers/local-provider.d.ts.map +1 -1
  90. package/dist/providers/local-provider.js +15 -3
  91. package/dist/providers/local-provider.js.map +1 -1
  92. package/dist/providers/openai-compat-provider.d.ts +11 -1
  93. package/dist/providers/openai-compat-provider.d.ts.map +1 -1
  94. package/dist/providers/openai-compat-provider.js +93 -6
  95. package/dist/providers/openai-compat-provider.js.map +1 -1
  96. package/dist/providers/openai-completions-runner.d.ts +25 -1
  97. package/dist/providers/openai-completions-runner.d.ts.map +1 -1
  98. package/dist/providers/openai-completions-runner.js +109 -14
  99. package/dist/providers/openai-completions-runner.js.map +1 -1
  100. package/dist/providers/race.d.ts +58 -1
  101. package/dist/providers/race.d.ts.map +1 -1
  102. package/dist/providers/race.js +317 -4
  103. package/dist/providers/race.js.map +1 -1
  104. package/dist/providers/truncate.d.ts.map +1 -1
  105. package/dist/providers/truncate.js +20 -2
  106. package/dist/providers/truncate.js.map +1 -1
  107. package/dist/providers/types.d.ts +96 -1
  108. package/dist/providers/types.d.ts.map +1 -1
  109. package/dist/providers/types.js +59 -1
  110. package/dist/providers/types.js.map +1 -1
  111. package/dist/provisional-manager.d.ts +93 -0
  112. package/dist/provisional-manager.d.ts.map +1 -0
  113. package/dist/provisional-manager.js +98 -0
  114. package/dist/provisional-manager.js.map +1 -0
  115. package/dist/provisional.d.ts +150 -0
  116. package/dist/provisional.d.ts.map +1 -0
  117. package/dist/provisional.js +238 -0
  118. package/dist/provisional.js.map +1 -0
  119. package/dist/regret-tracker.d.ts +84 -0
  120. package/dist/regret-tracker.d.ts.map +1 -0
  121. package/dist/regret-tracker.js +145 -0
  122. package/dist/regret-tracker.js.map +1 -0
  123. package/dist/replay-cli.d.ts +13 -0
  124. package/dist/replay-cli.d.ts.map +1 -0
  125. package/dist/replay-cli.js +167 -0
  126. package/dist/replay-cli.js.map +1 -0
  127. package/dist/replay.d.ts +107 -0
  128. package/dist/replay.d.ts.map +1 -0
  129. package/dist/replay.js +225 -0
  130. package/dist/replay.js.map +1 -0
  131. package/dist/reranker.d.ts +181 -0
  132. package/dist/reranker.d.ts.map +1 -0
  133. package/dist/reranker.js +324 -0
  134. package/dist/reranker.js.map +1 -0
  135. package/dist/session-trie.d.ts +103 -0
  136. package/dist/session-trie.d.ts.map +1 -0
  137. package/dist/session-trie.js +280 -0
  138. package/dist/session-trie.js.map +1 -0
  139. package/dist/speculative.d.ts +74 -0
  140. package/dist/speculative.d.ts.map +1 -0
  141. package/dist/speculative.js +107 -0
  142. package/dist/speculative.js.map +1 -0
  143. package/dist/tool-summarize.d.ts +40 -0
  144. package/dist/tool-summarize.d.ts.map +1 -0
  145. package/dist/tool-summarize.js +245 -0
  146. package/dist/tool-summarize.js.map +1 -0
  147. package/dist/trie-sanitize.d.ts +22 -0
  148. package/dist/trie-sanitize.d.ts.map +1 -0
  149. package/dist/trie-sanitize.js +142 -0
  150. package/dist/trie-sanitize.js.map +1 -0
  151. package/dist/versions.d.ts +23 -0
  152. package/dist/versions.d.ts.map +1 -0
  153. package/dist/versions.js +31 -0
  154. package/dist/versions.js.map +1 -0
  155. package/package.json +3 -2
package/dist/cache.js ADDED
@@ -0,0 +1,173 @@
1
+ import { createHash } from "node:crypto";
2
+ import { combinedVersionFingerprint } from "./versions.js";
3
+ import { tryWarm } from "./speculative.js";
4
+ export const DEFAULT_CAPACITY = 256;
5
+ export const DEFAULT_TTL_MS = 10 * 60 * 1000;
6
+ /**
7
+ * Build a stable cache key. Combines prefix, context fingerprint, provider
8
+ * fingerprint, the global behavior fingerprint, and (optionally) the engine
9
+ * config fingerprint so that prompt-template, sanitizer, context-aggregator,
10
+ * reranker, profile-hint, AND env-knob changes all invalidate stale entries.
11
+ */
12
+ export function makeKey(input) {
13
+ const sep = "\x00";
14
+ const raw = [
15
+ input.prefix,
16
+ input.contextFingerprint,
17
+ input.providerFingerprint,
18
+ combinedVersionFingerprint(),
19
+ input.configFingerprint ?? "",
20
+ ].join(sep);
21
+ return createHash("sha1").update(raw).digest("hex");
22
+ }
23
+ /**
24
+ * Fingerprint over the final provider-visible input. The PRD requires this
25
+ * to be computed over the rendered final input (after pruning/truncation)
26
+ * rather than a raw branch slice. Callers should pass the same messages
27
+ * array the provider will receive.
28
+ *
29
+ * Tool-call metadata (`toolName`) participates in the hash so distinct tool
30
+ * results collapse to distinct fingerprints.
31
+ */
32
+ export function fingerprintRecentMessages(messages) {
33
+ const h = createHash("sha1");
34
+ for (const m of messages) {
35
+ h.update(m.role);
36
+ h.update("\x00");
37
+ h.update(m.text);
38
+ h.update("\x00");
39
+ h.update(m.toolName ?? "");
40
+ h.update("\x01");
41
+ }
42
+ return h.digest("hex");
43
+ }
44
+ /**
45
+ * Fingerprint identifying a provider configuration. Anything that, if
46
+ * changed, would produce a different completion belongs here.
47
+ */
48
+ export function fingerprintProvider(parts) {
49
+ const e = parts.extras ?? {};
50
+ const fields = [
51
+ ["kind", parts.kind],
52
+ ["providerName", parts.providerName],
53
+ ["modelId", parts.modelId],
54
+ ["temperature", String(parts.temperature)],
55
+ ["maxTokens", String(parts.maxTokens)],
56
+ ["maxRecentMessages", String(parts.maxRecentMessages)],
57
+ ...Object.keys(e)
58
+ .sort()
59
+ .map((k) => [k, String(e[k] ?? "")]),
60
+ ];
61
+ const h = createHash("sha1");
62
+ for (const [k, v] of fields) {
63
+ h.update(k);
64
+ h.update("=");
65
+ h.update(v);
66
+ h.update("\x00");
67
+ }
68
+ return h.digest("hex");
69
+ }
70
+ /**
71
+ * Small LRU cache for ghost completions.
72
+ *
73
+ * - Capacity-bounded; oldest entry is evicted on insert overflow.
74
+ * - TTL-bounded; expired entries are treated as miss and removed lazily on
75
+ * access.
76
+ * - `delete()` is exposed so callers can explicitly evict on divergent
77
+ * dismiss (see PRD §6 M1a).
78
+ *
79
+ * Not thread-safe, but the ghost editor is single-threaded.
80
+ */
81
+ export class CompletionCache {
82
+ capacity;
83
+ ttlMs;
84
+ store = new Map();
85
+ hits = 0;
86
+ misses = 0;
87
+ constructor(capacity = DEFAULT_CAPACITY, ttlMs = DEFAULT_TTL_MS) {
88
+ this.capacity = capacity;
89
+ this.ttlMs = ttlMs;
90
+ }
91
+ get size() {
92
+ return this.store.size;
93
+ }
94
+ /** PRD §M3g — exposed via `/ghost` diagnostics. */
95
+ get stats() {
96
+ const total = this.hits + this.misses;
97
+ return {
98
+ size: this.store.size,
99
+ hits: this.hits,
100
+ misses: this.misses,
101
+ hitRate: total > 0 ? this.hits / total : 0,
102
+ };
103
+ }
104
+ get(key, now = Date.now()) {
105
+ const v = this.store.get(key);
106
+ if (!v) {
107
+ this.misses++;
108
+ return null;
109
+ }
110
+ if (now - v.ts > this.ttlMs) {
111
+ this.store.delete(key);
112
+ this.misses++;
113
+ return null;
114
+ }
115
+ // LRU touch: re-insert to move to end.
116
+ this.store.delete(key);
117
+ this.store.set(key, v);
118
+ this.hits++;
119
+ return v;
120
+ }
121
+ set(key, value) {
122
+ if (this.store.has(key))
123
+ this.store.delete(key);
124
+ this.store.set(key, value);
125
+ while (this.store.size > this.capacity) {
126
+ const oldest = this.store.keys().next().value;
127
+ if (oldest === undefined)
128
+ break;
129
+ this.store.delete(oldest);
130
+ }
131
+ }
132
+ delete(key) {
133
+ return this.store.delete(key);
134
+ }
135
+ clear() {
136
+ this.store.clear();
137
+ }
138
+ }
139
+ export function decideWarmPath(input) {
140
+ const cacheKey = makeKey({
141
+ prefix: input.prefix,
142
+ contextFingerprint: input.contextFingerprint,
143
+ providerFingerprint: input.providerFingerprint,
144
+ ...(input.configFingerprint !== undefined ? { configFingerprint: input.configFingerprint } : {}),
145
+ });
146
+ const hit = input.cache.get(cacheKey, input.now);
147
+ if (hit && hit.ghost) {
148
+ return {
149
+ ghost: hit.ghost,
150
+ mode: "cache",
151
+ providerName: hit.providerName,
152
+ cacheKey,
153
+ };
154
+ }
155
+ const match = tryWarm(input.ring, {
156
+ prefix: input.prefix,
157
+ contextFingerprint: input.contextFingerprint,
158
+ providerFingerprint: input.providerFingerprint,
159
+ versionFingerprint: input.versionFingerprint,
160
+ ...(input.configFingerprint !== undefined ? { configFingerprint: input.configFingerprint } : {}),
161
+ cooldownActive: input.cooldownActive,
162
+ });
163
+ if (match) {
164
+ return {
165
+ ghost: match.ghost,
166
+ mode: "speculative-warm",
167
+ providerName: match.source.providerName,
168
+ cacheKey,
169
+ };
170
+ }
171
+ return null;
172
+ }
173
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Pure rerank pipeline for an LLM `CandidateSet`. Extracted from
3
+ * `GhostEditor.requestPrediction` as part of refactor #57 so the editor
4
+ * stays thin and the rerank-+-confidence-gate path is unit-testable in
5
+ * isolation.
6
+ *
7
+ * PRD §M3c (weighted reranker) + §M3c.1 (confidence gate) + §M4
8
+ * (profile-bias signal). Single-candidate sets bypass the reranker
9
+ * entirely — there is no reordering to do and the confidence gate is
10
+ * not applicable (it operates on the reranked top-vs-second margin).
11
+ *
12
+ * No `this`, no I/O, no editor state — caller supplies env strings and
13
+ * an optional `profileBias` function.
14
+ */
15
+ export interface RerankCandidateSetInput {
16
+ /** Provider's raw candidate list (post-dedup). At least one entry expected. */
17
+ candidates: {
18
+ text: string;
19
+ avgLogprob?: number;
20
+ }[];
21
+ /** Provider name — drives the local-vs-cloud bias in the reranker. */
22
+ providerName: string;
23
+ /** Full text-before-cursor at request time. Used to slice the
24
+ * last-line prefix for the lengthPrior curve. */
25
+ prefix: string;
26
+ /** Raw `PI_GHOST_RERANK_WEIGHTS` env value. */
27
+ weightsEnvRaw: string | undefined;
28
+ /** Raw `PI_GHOST_MIN_CONFIDENCE` env value. `undefined` disables
29
+ * the gate entirely; an invalid string is treated as "no gate". */
30
+ minConfidenceEnvRaw: string | undefined;
31
+ /** Optional cross-session profile-bias hook (M4). */
32
+ profileBias?: (text: string) => number;
33
+ /**
34
+ * Enhancement #1 — optional edit-history proximity scorer. Receives raw
35
+ * candidate text; returns a real number (negative permitted for tokens
36
+ * recently deleted). Missing → all candidates score 0 in that feature.
37
+ */
38
+ editProximityScorer?: (text: string) => number;
39
+ }
40
+ export type RerankCandidateSetResult = {
41
+ gated: false;
42
+ candidateTexts: string[];
43
+ topText: string;
44
+ /** Top-candidate raw editProximity score (un-normalized). Populated
45
+ * only when `editProximityScorer` was supplied. */
46
+ topEditProximityScore?: number;
47
+ } | {
48
+ gated: true;
49
+ };
50
+ /**
51
+ * Rerank a candidate set + apply the optional confidence gate. Returns
52
+ * `{ gated: true }` when the gate rejected the result (caller should emit
53
+ * a `rejected` metric and skip rendering). Otherwise returns the reordered
54
+ * candidate texts plus the top pick.
55
+ */
56
+ export declare function rerankCandidateSet(input: RerankCandidateSetInput): RerankCandidateSetResult;
57
+ //# sourceMappingURL=candidate-rerank.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"candidate-rerank.d.ts","sourceRoot":"","sources":["../src/candidate-rerank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH,MAAM,WAAW,uBAAuB;IACvC,+EAA+E;IAC/E,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpD,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB;sDACkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC;wEACoE;IACpE,mBAAmB,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,qDAAqD;IACrD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/C;AAED,MAAM,MAAM,wBAAwB,GACjC;IACA,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB;wDACoD;IACpD,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC9B,GACD;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,CAAC;AAEnB;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,wBAAwB,CAsD3F"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Pure rerank pipeline for an LLM `CandidateSet`. Extracted from
3
+ * `GhostEditor.requestPrediction` as part of refactor #57 so the editor
4
+ * stays thin and the rerank-+-confidence-gate path is unit-testable in
5
+ * isolation.
6
+ *
7
+ * PRD §M3c (weighted reranker) + §M3c.1 (confidence gate) + §M4
8
+ * (profile-bias signal). Single-candidate sets bypass the reranker
9
+ * entirely — there is no reordering to do and the confidence gate is
10
+ * not applicable (it operates on the reranked top-vs-second margin).
11
+ *
12
+ * No `this`, no I/O, no editor state — caller supplies env strings and
13
+ * an optional `profileBias` function.
14
+ */
15
+ import { applyConfidenceGate, parseMinConfidenceEnv, parseWeightsEnv, rerank, } from "./reranker.js";
16
+ /**
17
+ * Rerank a candidate set + apply the optional confidence gate. Returns
18
+ * `{ gated: true }` when the gate rejected the result (caller should emit
19
+ * a `rejected` metric and skip rendering). Otherwise returns the reordered
20
+ * candidate texts plus the top pick.
21
+ */
22
+ export function rerankCandidateSet(input) {
23
+ // Empty set is structurally invalid for this function — the caller
24
+ // must guard for `!result || !set` BEFORE invoking. Treat as gated so
25
+ // any future re-use can't silently render an empty ghost.
26
+ if (input.candidates.length === 0) {
27
+ return { gated: true };
28
+ }
29
+ // Single-candidate path: nothing to reorder, gate doesn't apply
30
+ // (it needs a top-vs-second margin). Still surface editProximity raw
31
+ // score for metric capture when scorer was supplied.
32
+ if (input.candidates.length === 1) {
33
+ const only = input.candidates[0];
34
+ const ep = input.editProximityScorer ? input.editProximityScorer(only.text) : undefined;
35
+ return {
36
+ gated: false,
37
+ candidateTexts: [only.text],
38
+ topText: only.text,
39
+ ...(ep !== undefined ? { topEditProximityScore: ep } : {}),
40
+ };
41
+ }
42
+ const local = input.providerName === "ghost-local";
43
+ const inputs = input.candidates.map((c) => ({
44
+ text: c.text,
45
+ provider: input.providerName,
46
+ local,
47
+ ...(c.avgLogprob !== undefined ? { avgLogprob: c.avgLogprob } : {}),
48
+ }));
49
+ // PRD-adjacent (#5): pass last-line prefix length so the lengthPrior
50
+ // curve scales for long-prefix queries. Document-level length would
51
+ // over-extend the curve on multi-line buffers; the active line is
52
+ // what the user is currently typing.
53
+ const lastLinePrefix = input.prefix.split("\n").pop() ?? "";
54
+ const ranked = rerank(inputs, {
55
+ weights: parseWeightsEnv(input.weightsEnvRaw),
56
+ prefixLen: lastLinePrefix.length,
57
+ ...(input.profileBias ? { profileBias: input.profileBias } : {}),
58
+ ...(input.editProximityScorer ? { editProximityScorer: input.editProximityScorer } : {}),
59
+ });
60
+ if (input.minConfidenceEnvRaw !== undefined) {
61
+ const gated = applyConfidenceGate(ranked, {
62
+ minConfidence: parseMinConfidenceEnv(input.minConfidenceEnvRaw),
63
+ });
64
+ if (!gated)
65
+ return { gated: true };
66
+ }
67
+ const topText = ranked[0].text;
68
+ const topEditProximityScore = input.editProximityScorer
69
+ ? input.editProximityScorer(topText)
70
+ : undefined;
71
+ return {
72
+ gated: false,
73
+ candidateTexts: ranked.map((r) => r.text),
74
+ topText,
75
+ ...(topEditProximityScore !== undefined ? { topEditProximityScore } : {}),
76
+ };
77
+ }
78
+ //# sourceMappingURL=candidate-rerank.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"candidate-rerank.js","sourceRoot":"","sources":["../src/candidate-rerank.ts"],"names":[],"mappings":"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"}
package/dist/config.d.ts CHANGED
@@ -10,8 +10,17 @@ export interface OpenAICompatProviderConfig {
10
10
  debounceMs: number;
11
11
  maxTokens: number;
12
12
  maxRecentMessages: number;
13
+ /** Sampling temperature. 0 for deterministic-ish output (best-effort). */
14
+ temperature: number;
13
15
  /** Optional context-window hint passed to pi-ai. Defaults to 8192. */
14
16
  contextWindow?: number;
17
+ /**
18
+ * Number of parallel candidate completions to fire when `predictMulti`
19
+ * is called. 1 means no parallelism. Useful only at temperature > 0
20
+ * (otherwise all parallel calls return the same greedy result).
21
+ * Default 1. Override via `PI_GHOST_MERCURY_PARALLEL_N`.
22
+ */
23
+ parallelN: number;
15
24
  }
16
25
  export interface CloudProviderConfig {
17
26
  kind: "cloud";
@@ -21,6 +30,7 @@ export interface CloudProviderConfig {
21
30
  debounceMs: number;
22
31
  maxTokens: number;
23
32
  maxRecentMessages: number;
33
+ temperature: number;
24
34
  }
25
35
  export interface LocalProviderConfig {
26
36
  kind: "local";
@@ -29,6 +39,7 @@ export interface LocalProviderConfig {
29
39
  debounceMs: number;
30
40
  maxTokens: number;
31
41
  maxRecentMessages: number;
42
+ temperature: number;
32
43
  /** Optional context-window hint passed to pi-ai. Defaults to 8192. */
33
44
  contextWindow?: number;
34
45
  }
@@ -52,4 +63,36 @@ export declare const DEFAULT_LOCAL_CONFIG: LocalProviderConfig;
52
63
  export declare const DEFAULT_MERCURY_CONFIG: OpenAICompatProviderConfig;
53
64
  export declare const DEFAULT_CONFIG: GhostConfig;
54
65
  export declare function readConfigFromEnv(): GhostConfig;
66
+ /**
67
+ * Strip CR/LF and surrounding whitespace from an API key (SEC-7). Prevents
68
+ * HTTP header injection via `Authorization: Bearer ${apiKey}` when a
69
+ * malformed `.env` file or environment manipulation injects newlines.
70
+ * Returns undefined for empty results so callers fall back to env lookup.
71
+ */
72
+ export declare function sanitizeApiKey(raw: string | undefined): string | undefined;
73
+ /**
74
+ * Produce a deep copy of a config with all `apiKey` fields removed (SEC-8).
75
+ * Use before any logging, diagnostics dump, telemetry, or serialization.
76
+ * The original config remains live with its keys for actual provider use.
77
+ *
78
+ * Defensive: even fields not currently typed as containing keys are scrubbed
79
+ * via a recursive walk that drops any property named `apiKey` (case-sensitive).
80
+ *
81
+ * Caveats: class instances become plain objects (methods lost). Cyclic
82
+ * structures would recurse infinitely — caller must ensure acyclic input.
83
+ * Both caveats are fine for `GhostConfig` which is plain JSON-shaped.
84
+ */
85
+ export declare function redactConfig<T>(cfg: T): T;
86
+ /**
87
+ * Validate a provider baseUrl (SEC-1). Returns the URL on success, null
88
+ * on failure with a stderr warning. Rules:
89
+ * - Must be a parseable absolute URL.
90
+ * - `https:` always allowed.
91
+ * - `http:` only allowed for `localhost` / `127.0.0.1` / `::1` — sending
92
+ * API keys + prompt context in cleartext to anything else is almost
93
+ * certainly a misconfiguration. Setting `PI_GHOST_ALLOW_INSECURE=1`
94
+ * bypasses this check.
95
+ * - Other schemes (`file:`, `data:`, etc.) rejected unconditionally.
96
+ */
97
+ export declare function validateBaseUrl(raw: string, fieldName?: string): string | null;
55
98
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEtE,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,eAAe,GAAG,MAAM,CAAC;AAExE,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,eAAe,CAAC;IACtB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,CAAC,mBAAmB,GAAG,mBAAmB,GAAG,0BAA0B,CAAC,CAAC;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,GACvB,mBAAmB,GACnB,mBAAmB,GACnB,0BAA0B,GAC1B,kBAAkB,CAAC;AAEtB,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,oBAAoB,EAAE,mBAOlC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,mBAOlC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,0BAQpC,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,WAK5B,CAAC;AAqBF,wBAAgB,iBAAiB,IAAI,WAAW,CAwC/C"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEtE,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,eAAe,GAAG,MAAM,CAAC;AAExE,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,eAAe,CAAC;IACtB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,CAAC,mBAAmB,GAAG,mBAAmB,GAAG,0BAA0B,CAAC,CAAC;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,GACvB,mBAAmB,GACnB,mBAAmB,GACnB,0BAA0B,GAC1B,kBAAkB,CAAC;AAEtB,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,oBAAoB,EAAE,mBAQlC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,mBAQlC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,0BAUpC,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,WAK5B,CAAC;AAqBF,wBAAgB,iBAAiB,IAAI,WAAW,CAwD/C;AAsHD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAI1E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAEzC;AAaD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAsB,GAAG,MAAM,GAAG,IAAI,CAsB3F"}
package/dist/config.js CHANGED
@@ -6,6 +6,7 @@ export const DEFAULT_CLOUD_CONFIG = {
6
6
  debounceMs: 150,
7
7
  maxTokens: 64,
8
8
  maxRecentMessages: 4,
9
+ temperature: 0,
9
10
  };
10
11
  export const DEFAULT_LOCAL_CONFIG = {
11
12
  kind: "local",
@@ -14,6 +15,7 @@ export const DEFAULT_LOCAL_CONFIG = {
14
15
  debounceMs: 400,
15
16
  maxTokens: 48,
16
17
  maxRecentMessages: 2,
18
+ temperature: 0,
17
19
  };
18
20
  export const DEFAULT_MERCURY_CONFIG = {
19
21
  kind: "openai-compat",
@@ -23,6 +25,8 @@ export const DEFAULT_MERCURY_CONFIG = {
23
25
  debounceMs: 150,
24
26
  maxTokens: 64,
25
27
  maxRecentMessages: 4,
28
+ temperature: 0,
29
+ parallelN: 1,
26
30
  };
27
31
  export const DEFAULT_CONFIG = {
28
32
  enabled: true,
@@ -67,35 +71,50 @@ export function readConfigFromEnv() {
67
71
  cfg.provider.provider = process.env.PI_GHOST_PROVIDER_NAME;
68
72
  if (process.env.PI_GHOST_MODEL)
69
73
  cfg.provider.modelId = process.env.PI_GHOST_MODEL;
70
- if (process.env.PI_GHOST_API_KEY)
71
- cfg.provider.apiKey = process.env.PI_GHOST_API_KEY;
74
+ const apiKey = sanitizeApiKey(process.env.PI_GHOST_API_KEY);
75
+ if (apiKey !== undefined)
76
+ cfg.provider.apiKey = apiKey;
72
77
  cfg.provider.debounceMs = numEnv("PI_GHOST_DEBOUNCE_MS", cfg.provider.debounceMs);
73
78
  cfg.provider.maxTokens = numEnv("PI_GHOST_MAX_TOKENS", cfg.provider.maxTokens);
74
79
  cfg.provider.maxRecentMessages = numEnv("PI_GHOST_RECENT", cfg.provider.maxRecentMessages);
80
+ cfg.provider.temperature = tempEnv(cfg.provider.temperature);
75
81
  }
76
82
  else if (cfg.provider.kind === "openai-compat") {
77
- if (process.env.PI_GHOST_BASE_URL)
78
- cfg.provider.baseUrl = process.env.PI_GHOST_BASE_URL;
79
- else if (process.env.INCEPTION_BASE_URL)
80
- cfg.provider.baseUrl = process.env.INCEPTION_BASE_URL;
83
+ const baseUrlRaw = process.env.PI_GHOST_BASE_URL ?? process.env.INCEPTION_BASE_URL;
84
+ if (baseUrlRaw) {
85
+ const fieldName = process.env.PI_GHOST_BASE_URL ? "PI_GHOST_BASE_URL" : "INCEPTION_BASE_URL";
86
+ const validated = validateBaseUrl(baseUrlRaw, fieldName);
87
+ if (validated)
88
+ cfg.provider.baseUrl = validated;
89
+ }
81
90
  if (process.env.PI_GHOST_MODEL)
82
91
  cfg.provider.modelId = process.env.PI_GHOST_MODEL;
83
- if (process.env.PI_GHOST_API_KEY)
84
- cfg.provider.apiKey = process.env.PI_GHOST_API_KEY;
85
- else if (process.env.INCEPTION_API_KEY)
86
- cfg.provider.apiKey = process.env.INCEPTION_API_KEY;
92
+ const apiKey = sanitizeApiKey(process.env.PI_GHOST_API_KEY ?? process.env.INCEPTION_API_KEY);
93
+ if (apiKey !== undefined)
94
+ cfg.provider.apiKey = apiKey;
87
95
  cfg.provider.debounceMs = numEnv("PI_GHOST_DEBOUNCE_MS", cfg.provider.debounceMs);
88
96
  cfg.provider.maxTokens = numEnv("PI_GHOST_MAX_TOKENS", cfg.provider.maxTokens);
89
97
  cfg.provider.maxRecentMessages = numEnv("PI_GHOST_RECENT", cfg.provider.maxRecentMessages);
98
+ cfg.provider.temperature = tempEnv(cfg.provider.temperature);
99
+ cfg.provider.parallelN = parallelNEnv(cfg.provider.parallelN);
90
100
  }
91
101
  else if (cfg.provider.kind === "local") {
92
- if (process.env.PI_GHOST_BASE_URL)
93
- cfg.provider.baseUrl = process.env.PI_GHOST_BASE_URL;
102
+ if (process.env.PI_GHOST_BASE_URL) {
103
+ const validated = validateBaseUrl(process.env.PI_GHOST_BASE_URL, "PI_GHOST_BASE_URL");
104
+ if (validated)
105
+ cfg.provider.baseUrl = validated;
106
+ }
94
107
  if (process.env.PI_GHOST_MODEL)
95
108
  cfg.provider.modelId = process.env.PI_GHOST_MODEL;
96
109
  cfg.provider.debounceMs = numEnv("PI_GHOST_DEBOUNCE_MS", cfg.provider.debounceMs);
97
110
  cfg.provider.maxTokens = numEnv("PI_GHOST_MAX_TOKENS", cfg.provider.maxTokens);
98
111
  cfg.provider.maxRecentMessages = numEnv("PI_GHOST_RECENT", cfg.provider.maxRecentMessages);
112
+ cfg.provider.temperature = tempEnv(cfg.provider.temperature);
113
+ }
114
+ else if (cfg.provider.kind === "race") {
115
+ for (const m of cfg.provider.members) {
116
+ m.temperature = tempEnv(m.temperature);
117
+ }
99
118
  }
100
119
  return cfg;
101
120
  }
@@ -125,18 +144,23 @@ function buildRaceFromEnv(raceList) {
125
144
  function buildRaceMember(name) {
126
145
  const upper = name.replace(/[^A-Za-z0-9]/g, "_").toUpperCase();
127
146
  if (name === "local" || name === "ollama") {
147
+ const rawBase = process.env[`PI_GHOST_RACE_${upper}_BASE_URL`];
148
+ const baseUrl = rawBase
149
+ ? (validateBaseUrl(rawBase, `PI_GHOST_RACE_${upper}_BASE_URL`) ?? DEFAULT_LOCAL_CONFIG.baseUrl)
150
+ : DEFAULT_LOCAL_CONFIG.baseUrl;
128
151
  return {
129
152
  ...DEFAULT_LOCAL_CONFIG,
130
- baseUrl: process.env[`PI_GHOST_RACE_${upper}_BASE_URL`] ?? DEFAULT_LOCAL_CONFIG.baseUrl,
153
+ baseUrl,
131
154
  modelId: process.env[`PI_GHOST_RACE_${upper}_MODEL`] ?? DEFAULT_LOCAL_CONFIG.modelId,
132
155
  };
133
156
  }
134
157
  if (MERCURY_ALIASES.has(name)) {
135
- const baseUrl = process.env[`PI_GHOST_RACE_${upper}_BASE_URL`] ??
136
- process.env.INCEPTION_BASE_URL ??
137
- MERCURY_DEFAULT_BASE_URL;
158
+ const rawBase = process.env[`PI_GHOST_RACE_${upper}_BASE_URL`] ?? process.env.INCEPTION_BASE_URL;
159
+ const baseUrl = rawBase
160
+ ? (validateBaseUrl(rawBase, `PI_GHOST_RACE_${upper}_BASE_URL`) ?? MERCURY_DEFAULT_BASE_URL)
161
+ : MERCURY_DEFAULT_BASE_URL;
138
162
  const modelId = process.env[`PI_GHOST_RACE_${upper}_MODEL`] ?? MERCURY_DEFAULT_MODEL;
139
- const apiKey = process.env[`PI_GHOST_RACE_${upper}_API_KEY`] ?? process.env.INCEPTION_API_KEY;
163
+ const apiKey = sanitizeApiKey(process.env[`PI_GHOST_RACE_${upper}_API_KEY`] ?? process.env.INCEPTION_API_KEY);
140
164
  return {
141
165
  kind: "openai-compat",
142
166
  providerName: "mercury-edit",
@@ -146,6 +170,8 @@ function buildRaceMember(name) {
146
170
  debounceMs: DEFAULT_CLOUD_CONFIG.debounceMs,
147
171
  maxTokens: DEFAULT_CLOUD_CONFIG.maxTokens,
148
172
  maxRecentMessages: DEFAULT_CLOUD_CONFIG.maxRecentMessages,
173
+ temperature: DEFAULT_MERCURY_CONFIG.temperature,
174
+ parallelN: DEFAULT_MERCURY_CONFIG.parallelN,
149
175
  };
150
176
  }
151
177
  const provider = name;
@@ -155,7 +181,7 @@ function buildRaceMember(name) {
155
181
  }
156
182
  const modelEnv = process.env[`PI_GHOST_RACE_${upper}_MODEL`];
157
183
  const modelId = modelEnv ?? RACE_DEFAULT_MODELS[provider] ?? DEFAULT_CLOUD_CONFIG.modelId;
158
- const apiKey = process.env[`PI_GHOST_RACE_${upper}_API_KEY`];
184
+ const apiKey = sanitizeApiKey(process.env[`PI_GHOST_RACE_${upper}_API_KEY`]);
159
185
  return {
160
186
  ...DEFAULT_CLOUD_CONFIG,
161
187
  provider,
@@ -184,4 +210,108 @@ function numEnv(name, fallback) {
184
210
  const v = Number(raw);
185
211
  return Number.isFinite(v) && v >= 0 ? v : fallback;
186
212
  }
213
+ /**
214
+ * Parse PI_GHOST_TEMPERATURE. Returns fallback when unset or out of [0, 2].
215
+ * The provider implementations use 0 by default; users can opt back in to
216
+ * sampling for cache-miss diversity. Note: temperature=0 is best-effort
217
+ * across vendors and does NOT guarantee identical outputs across calls
218
+ * (kernel scheduling / batching cause variance). The completion cache is
219
+ * the real determinism guarantee.
220
+ */
221
+ function tempEnv(fallback) {
222
+ const raw = process.env.PI_GHOST_TEMPERATURE;
223
+ if (raw === undefined)
224
+ return fallback;
225
+ const v = Number(raw);
226
+ return Number.isFinite(v) && v >= 0 && v <= 2 ? v : fallback;
227
+ }
228
+ /**
229
+ * Strip CR/LF and surrounding whitespace from an API key (SEC-7). Prevents
230
+ * HTTP header injection via `Authorization: Bearer ${apiKey}` when a
231
+ * malformed `.env` file or environment manipulation injects newlines.
232
+ * Returns undefined for empty results so callers fall back to env lookup.
233
+ */
234
+ export function sanitizeApiKey(raw) {
235
+ if (raw === undefined)
236
+ return undefined;
237
+ const cleaned = raw.replace(/[\r\n]/g, "").trim();
238
+ return cleaned.length > 0 ? cleaned : undefined;
239
+ }
240
+ /**
241
+ * Produce a deep copy of a config with all `apiKey` fields removed (SEC-8).
242
+ * Use before any logging, diagnostics dump, telemetry, or serialization.
243
+ * The original config remains live with its keys for actual provider use.
244
+ *
245
+ * Defensive: even fields not currently typed as containing keys are scrubbed
246
+ * via a recursive walk that drops any property named `apiKey` (case-sensitive).
247
+ *
248
+ * Caveats: class instances become plain objects (methods lost). Cyclic
249
+ * structures would recurse infinitely — caller must ensure acyclic input.
250
+ * Both caveats are fine for `GhostConfig` which is plain JSON-shaped.
251
+ */
252
+ export function redactConfig(cfg) {
253
+ return redactWalk(cfg);
254
+ }
255
+ function redactWalk(value) {
256
+ if (value === null || typeof value !== "object")
257
+ return value;
258
+ if (Array.isArray(value))
259
+ return value.map(redactWalk);
260
+ const out = {};
261
+ for (const [k, v] of Object.entries(value)) {
262
+ if (k === "apiKey")
263
+ continue;
264
+ out[k] = redactWalk(v);
265
+ }
266
+ return out;
267
+ }
268
+ /**
269
+ * Validate a provider baseUrl (SEC-1). Returns the URL on success, null
270
+ * on failure with a stderr warning. Rules:
271
+ * - Must be a parseable absolute URL.
272
+ * - `https:` always allowed.
273
+ * - `http:` only allowed for `localhost` / `127.0.0.1` / `::1` — sending
274
+ * API keys + prompt context in cleartext to anything else is almost
275
+ * certainly a misconfiguration. Setting `PI_GHOST_ALLOW_INSECURE=1`
276
+ * bypasses this check.
277
+ * - Other schemes (`file:`, `data:`, etc.) rejected unconditionally.
278
+ */
279
+ export function validateBaseUrl(raw, fieldName = "PI_GHOST_BASE_URL") {
280
+ let url;
281
+ try {
282
+ url = new URL(raw);
283
+ }
284
+ catch {
285
+ process.stderr.write(`Pi Ghost: ignoring invalid ${fieldName}=${raw}\n`);
286
+ return null;
287
+ }
288
+ if (url.protocol === "https:")
289
+ return raw;
290
+ if (url.protocol === "http:") {
291
+ const host = url.hostname;
292
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1")
293
+ return raw;
294
+ if (process.env.PI_GHOST_ALLOW_INSECURE === "1")
295
+ return raw;
296
+ process.stderr.write(`Pi Ghost: refusing to use insecure ${fieldName}=${raw} (set PI_GHOST_ALLOW_INSECURE=1 to override)\n`);
297
+ return null;
298
+ }
299
+ process.stderr.write(`Pi Ghost: rejecting ${fieldName} with unsupported scheme '${url.protocol}'\n`);
300
+ return null;
301
+ }
302
+ /**
303
+ * Parse PI_GHOST_MERCURY_PARALLEL_N. Returns fallback when unset, non-integer,
304
+ * or outside [1, 8]. The upper bound prevents accidental cost explosions —
305
+ * each parallel call costs the same as a normal completion.
306
+ */
307
+ function parallelNEnv(fallback) {
308
+ const raw = process.env.PI_GHOST_MERCURY_PARALLEL_N;
309
+ if (raw === undefined)
310
+ return fallback;
311
+ const v = Number(raw);
312
+ if (!Number.isFinite(v))
313
+ return fallback;
314
+ const n = Math.floor(v);
315
+ return n >= 1 && n <= 8 ? n : fallback;
316
+ }
187
317
  //# sourceMappingURL=config.js.map