pi-ghost-autocomplete 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +236 -0
  2. package/dist/bench-cli.d.ts +3 -0
  3. package/dist/bench-cli.d.ts.map +1 -0
  4. package/dist/bench-cli.js +48 -0
  5. package/dist/bench-cli.js.map +1 -0
  6. package/dist/bench.d.ts +93 -0
  7. package/dist/bench.d.ts.map +1 -0
  8. package/dist/bench.js +241 -0
  9. package/dist/bench.js.map +1 -0
  10. package/dist/config.d.ts +55 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +187 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/context-aggregator.d.ts +14 -0
  15. package/dist/context-aggregator.d.ts.map +1 -0
  16. package/dist/context-aggregator.js +58 -0
  17. package/dist/context-aggregator.js.map +1 -0
  18. package/dist/fuzzy-matcher.d.ts +50 -0
  19. package/dist/fuzzy-matcher.d.ts.map +1 -0
  20. package/dist/fuzzy-matcher.js +160 -0
  21. package/dist/fuzzy-matcher.js.map +1 -0
  22. package/dist/ghost-editor.d.ts +65 -0
  23. package/dist/ghost-editor.d.ts.map +1 -0
  24. package/dist/ghost-editor.js +445 -0
  25. package/dist/ghost-editor.js.map +1 -0
  26. package/dist/index.d.ts +25 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +210 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/metrics.d.ts +56 -0
  31. package/dist/metrics.d.ts.map +1 -0
  32. package/dist/metrics.js +98 -0
  33. package/dist/metrics.js.map +1 -0
  34. package/dist/path-index.d.ts +20 -0
  35. package/dist/path-index.d.ts.map +1 -0
  36. package/dist/path-index.js +148 -0
  37. package/dist/path-index.js.map +1 -0
  38. package/dist/path-matcher.d.ts +32 -0
  39. package/dist/path-matcher.d.ts.map +1 -0
  40. package/dist/path-matcher.js +85 -0
  41. package/dist/path-matcher.js.map +1 -0
  42. package/dist/providers/cloud-provider.d.ts +10 -0
  43. package/dist/providers/cloud-provider.d.ts.map +1 -0
  44. package/dist/providers/cloud-provider.js +52 -0
  45. package/dist/providers/cloud-provider.js.map +1 -0
  46. package/dist/providers/local-provider.d.ts +11 -0
  47. package/dist/providers/local-provider.d.ts.map +1 -0
  48. package/dist/providers/local-provider.js +36 -0
  49. package/dist/providers/local-provider.js.map +1 -0
  50. package/dist/providers/openai-compat-provider.d.ts +29 -0
  51. package/dist/providers/openai-compat-provider.d.ts.map +1 -0
  52. package/dist/providers/openai-compat-provider.js +128 -0
  53. package/dist/providers/openai-compat-provider.js.map +1 -0
  54. package/dist/providers/openai-completions-runner.d.ts +28 -0
  55. package/dist/providers/openai-completions-runner.d.ts.map +1 -0
  56. package/dist/providers/openai-completions-runner.js +46 -0
  57. package/dist/providers/openai-completions-runner.js.map +1 -0
  58. package/dist/providers/race.d.ts +40 -0
  59. package/dist/providers/race.d.ts.map +1 -0
  60. package/dist/providers/race.js +128 -0
  61. package/dist/providers/race.js.map +1 -0
  62. package/dist/providers/truncate.d.ts +7 -0
  63. package/dist/providers/truncate.d.ts.map +1 -0
  64. package/dist/providers/truncate.js +39 -0
  65. package/dist/providers/truncate.js.map +1 -0
  66. package/dist/providers/types.d.ts +18 -0
  67. package/dist/providers/types.d.ts.map +1 -0
  68. package/dist/providers/types.js +2 -0
  69. package/dist/providers/types.js.map +1 -0
  70. package/dist/recent-paths.d.ts +25 -0
  71. package/dist/recent-paths.d.ts.map +1 -0
  72. package/dist/recent-paths.js +114 -0
  73. package/dist/recent-paths.js.map +1 -0
  74. package/package.json +79 -0
@@ -0,0 +1,85 @@
1
+ import { recentBoost } from "./recent-paths.js";
2
+ const PATH_CHAR = /[A-Za-z0-9_./@\-]/;
3
+ const QUOTE_CHARS = new Set(['"', "'", "`"]);
4
+ /**
5
+ * Extract a path-like token from the end of `before` (text before the cursor).
6
+ * Returns null when no path-prefix mode is applicable: a token is only
7
+ * eligible when it contains a `/` (path-prefix mode is structural, not fuzzy).
8
+ *
9
+ * `insideQuote` is true when the character immediately preceding the token is
10
+ * a quote or backtick; callers can still trust the token in that case.
11
+ */
12
+ export function extractPathPrefixToken(before) {
13
+ if (!before)
14
+ return null;
15
+ let end = before.length;
16
+ let start = end;
17
+ while (start > 0) {
18
+ const ch = before[start - 1];
19
+ if (!ch || !PATH_CHAR.test(ch))
20
+ break;
21
+ start--;
22
+ }
23
+ if (start === end)
24
+ return null;
25
+ const token = before.slice(start, end);
26
+ if (!token.includes("/"))
27
+ return null;
28
+ const prev = start > 0 ? before[start - 1] : undefined;
29
+ const insideQuote = prev !== undefined && QUOTE_CHARS.has(prev);
30
+ return { token, insideQuote };
31
+ }
32
+ /**
33
+ * Pick the best path-prefix match for `token`. Strategy: enumerate matches
34
+ * (capped), prefer files inside `cwdRel` subtree, then shorter paths.
35
+ *
36
+ * Returns null when the token has no matches or matches itself exactly
37
+ * (nothing to insert).
38
+ */
39
+ export function findPathPrefix(index, token, opts = {}) {
40
+ // Back-compat: legacy callers pass a numeric `limit`.
41
+ const o = typeof opts === "number" ? { limit: opts } : opts;
42
+ const limit = o.limit ?? 64;
43
+ const recent = o.recentPaths ?? null;
44
+ // Normalize a leading `./` — extractPathPrefixToken preserves it from the
45
+ // user's text (e.g. "./src/con") but git ls-files entries never start with
46
+ // `./`, so the prefix lookup would otherwise miss every cwd-relative entry.
47
+ const lookupToken = token.startsWith("./") ? token.slice(2) : token;
48
+ if (!lookupToken)
49
+ return null;
50
+ const matches = index.prefixMatches(lookupToken, limit);
51
+ if (matches.length === 0)
52
+ return null;
53
+ const cwdRel = relSubtree(index.root, index.cwd);
54
+ let best = null;
55
+ let bestScore = -Infinity;
56
+ let bestBoost = 0;
57
+ for (const m of matches) {
58
+ const inSub = cwdRel === "" || m.startsWith(cwdRel);
59
+ const rb = recentBoost(m, recent);
60
+ // Lex-rank: subtree dominates, then recent-mention boost, then short.
61
+ const score = (inSub ? 1_000_000_000 : 0) + rb * 1_000_000 - m.length;
62
+ if (score > bestScore) {
63
+ bestScore = score;
64
+ best = m;
65
+ bestBoost = rb;
66
+ }
67
+ }
68
+ if (!best)
69
+ return null;
70
+ const suffix = best.slice(lookupToken.length);
71
+ if (!suffix)
72
+ return null;
73
+ return { match: best, suffix, score: 100 + bestBoost };
74
+ }
75
+ function relSubtree(root, cwd) {
76
+ const r = root.replace(/\\/g, "/").replace(/\/+$/, "");
77
+ const c = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
78
+ if (c === r)
79
+ return "";
80
+ const prefix = `${r}/`;
81
+ if (c.startsWith(prefix))
82
+ return `${c.slice(prefix.length)}/`;
83
+ return "";
84
+ }
85
+ //# sourceMappingURL=path-matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-matcher.js","sourceRoot":"","sources":["../src/path-matcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { CloudProviderConfig } from "../config.js";
2
+ import type { GhostProvider, GhostRequest } from "./types.js";
3
+ export declare class CloudProvider implements GhostProvider {
4
+ private readonly config;
5
+ readonly name = "ghost-cloud";
6
+ constructor(config: CloudProviderConfig);
7
+ get debounceMs(): number;
8
+ predict(req: GhostRequest): Promise<string | null>;
9
+ }
10
+ //# sourceMappingURL=cloud-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-provider.d.ts","sourceRoot":"","sources":["../../src/providers/cloud-provider.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE9D,qBAAa,aAAc,YAAW,aAAa;IAGtC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,QAAQ,CAAC,IAAI,iBAAiB;gBAED,MAAM,EAAE,mBAAmB;IAExD,IAAI,UAAU,IAAI,MAAM,CAEvB;IAEK,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAyBxD"}
@@ -0,0 +1,52 @@
1
+ import { complete, getEnvApiKey, getModels, } from "@earendil-works/pi-ai";
2
+ import { buildPromptMessages, sanitizeCompletion } from "./truncate.js";
3
+ export class CloudProvider {
4
+ config;
5
+ name = "ghost-cloud";
6
+ constructor(config) {
7
+ this.config = config;
8
+ }
9
+ get debounceMs() {
10
+ return this.config.debounceMs;
11
+ }
12
+ async predict(req) {
13
+ const apiKey = this.config.apiKey ?? getEnvApiKey(this.config.provider);
14
+ if (!apiKey)
15
+ return null;
16
+ const model = resolveModel(this.config.provider, this.config.modelId);
17
+ if (!model)
18
+ return null;
19
+ const { systemPrompt, user } = buildPromptMessages(req, this.config.maxRecentMessages);
20
+ const messages = [{ role: "user", content: user, timestamp: Date.now() }];
21
+ const ctx = { systemPrompt, messages };
22
+ const result = await complete(model, ctx, {
23
+ signal: req.signal,
24
+ temperature: 0.2,
25
+ maxTokens: this.config.maxTokens,
26
+ apiKey,
27
+ });
28
+ if (result.stopReason === "error" || result.stopReason === "aborted")
29
+ return null;
30
+ const text = result.content
31
+ .filter((c) => c.type === "text")
32
+ .map((c) => c.text)
33
+ .join("");
34
+ return sanitizeCompletion(text, req.editorTextBeforeCursor, 200);
35
+ }
36
+ }
37
+ /**
38
+ * Look up a model by id from pi-ai's registry. Returns null if the provider
39
+ * has no model with that id; the caller drops the prediction silently.
40
+ *
41
+ * Avoids `getModel(provider, id as never)` — the registry's typed lookup
42
+ * requires a literal id and would otherwise need an unsafe cast.
43
+ */
44
+ function resolveModel(provider, modelId) {
45
+ const models = getModels(provider);
46
+ for (const m of models) {
47
+ if (m.id === modelId)
48
+ return m;
49
+ }
50
+ return null;
51
+ }
52
+ //# sourceMappingURL=cloud-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-provider.js","sourceRoot":"","sources":["../../src/providers/cloud-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,QAAQ,EACR,YAAY,EACZ,SAAS,GAMT,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxE,MAAM,OAAO,aAAa;IAGI;IAFpB,IAAI,GAAG,aAAa,CAAC;IAE9B,YAA6B,MAA2B;QAA3B,WAAM,GAAN,MAAM,CAAqB;IAAG,CAAC;IAE5D,IAAI,UAAU;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAiB;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxE,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAEzB,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACtE,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAExB,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,GAAG,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACvF,MAAM,QAAQ,GAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACrF,MAAM,GAAG,GAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;QAEhD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE;YACzC,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,WAAW,EAAE,GAAG;YAChB,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;YAChC,MAAM;SACN,CAAC,CAAC;QAEH,IAAI,MAAM,CAAC,UAAU,KAAK,OAAO,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QAClF,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO;aACzB,MAAM,CAAC,CAAC,CAAC,EAAuC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;aACrE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aAClB,IAAI,CAAC,EAAE,CAAC,CAAC;QACX,OAAO,kBAAkB,CAAC,IAAI,EAAE,GAAG,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;IAClE,CAAC;CACD;AAED;;;;;;GAMG;AACH,SAAS,YAAY,CAAC,QAAuB,EAAE,OAAe;IAC7D,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,EAAE,KAAK,OAAO;YAAE,OAAO,CAAe,CAAC;IAC9C,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { LocalProviderConfig } from "../config.js";
2
+ import type { GhostProvider, GhostRequest } from "./types.js";
3
+ export declare class LocalProvider implements GhostProvider {
4
+ private readonly config;
5
+ readonly name = "ghost-local";
6
+ constructor(config: LocalProviderConfig);
7
+ get debounceMs(): number;
8
+ preflight(): Promise<boolean>;
9
+ predict(req: GhostRequest): Promise<string | null>;
10
+ }
11
+ //# sourceMappingURL=local-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-provider.d.ts","sourceRoot":"","sources":["../../src/providers/local-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI9D,qBAAa,aAAc,YAAW,aAAa;IAGtC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,QAAQ,CAAC,IAAI,iBAAiB;gBAED,MAAM,EAAE,mBAAmB;IAExD,IAAI,UAAU,IAAI,MAAM,CAEvB;IAEK,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAY7B,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAWxD"}
@@ -0,0 +1,36 @@
1
+ import { runOpenAICompletion, trimSlash } from "./openai-completions-runner.js";
2
+ const DEFAULT_CONTEXT_WINDOW = 8192;
3
+ export class LocalProvider {
4
+ config;
5
+ name = "ghost-local";
6
+ constructor(config) {
7
+ this.config = config;
8
+ }
9
+ get debounceMs() {
10
+ return this.config.debounceMs;
11
+ }
12
+ async preflight() {
13
+ try {
14
+ const ctrl = new AbortController();
15
+ const timeout = setTimeout(() => ctrl.abort(), 750);
16
+ const res = await fetch(`${trimSlash(this.config.baseUrl)}/api/tags`, { signal: ctrl.signal });
17
+ clearTimeout(timeout);
18
+ return res.ok;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ async predict(req) {
25
+ return runOpenAICompletion(req, {
26
+ baseUrl: `${trimSlash(this.config.baseUrl)}/v1`,
27
+ apiKey: "ollama",
28
+ modelId: this.config.modelId,
29
+ providerTag: "ollama",
30
+ maxTokens: this.config.maxTokens,
31
+ maxRecentMessages: this.config.maxRecentMessages,
32
+ contextWindow: this.config.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
33
+ });
34
+ }
35
+ }
36
+ //# sourceMappingURL=local-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-provider.js","sourceRoot":"","sources":["../../src/providers/local-provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAGhF,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAEpC,MAAM,OAAO,aAAa;IAGI;IAFpB,IAAI,GAAG,aAAa,CAAC;IAE9B,YAA6B,MAA2B;QAA3B,WAAM,GAAN,MAAM,CAAqB;IAAG,CAAC;IAE5D,IAAI,UAAU;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,SAAS;QACd,IAAI,CAAC;YACJ,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAC;YACnC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,GAAG,CAAC,CAAC;YACpD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YAC/F,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,OAAO,GAAG,CAAC,EAAE,CAAC;QACf,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,KAAK,CAAC;QACd,CAAC;IACF,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAiB;QAC9B,OAAO,mBAAmB,CAAC,GAAG,EAAE;YAC/B,OAAO,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK;YAC/C,MAAM,EAAE,QAAQ;YAChB,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO;YAC5B,WAAW,EAAE,QAAQ;YACrB,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;YAChC,iBAAiB,EAAE,IAAI,CAAC,MAAM,CAAC,iBAAiB;YAChD,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,aAAa,IAAI,sBAAsB;SAClE,CAAC,CAAC;IACJ,CAAC;CACD"}
@@ -0,0 +1,29 @@
1
+ import type { OpenAICompatProviderConfig } from "../config.js";
2
+ import type { GhostProvider, GhostRequest } from "./types.js";
3
+ export interface PreflightResult {
4
+ ok: boolean;
5
+ status?: number;
6
+ message: string;
7
+ }
8
+ /**
9
+ * Provider for FIM-completions backends (e.g. Mercury Edit 2 by Inception
10
+ * Labs). Calls `/fim/completions` with `prompt`/`suffix` and returns the
11
+ * raw text completion.
12
+ *
13
+ * A {@link preflight} method validates the API key at startup so the host
14
+ * can surface configuration errors early instead of silently dropping
15
+ * every prediction.
16
+ */
17
+ export declare class OpenAICompatProvider implements GhostProvider {
18
+ private readonly config;
19
+ readonly name: string;
20
+ constructor(config: OpenAICompatProviderConfig);
21
+ get debounceMs(): number;
22
+ /**
23
+ * Validate the API key with a minimal FIM request. A successful
24
+ * response (even an empty completion) proves auth + connectivity.
25
+ */
26
+ preflight(): Promise<PreflightResult>;
27
+ predict(req: GhostRequest): Promise<string | null>;
28
+ }
29
+ //# sourceMappingURL=openai-compat-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-compat-provider.d.ts","sourceRoot":"","sources":["../../src/providers/openai-compat-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI9D,MAAM,WAAW,eAAe;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,YAAW,aAAa;IAG7C,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEO,MAAM,EAAE,0BAA0B;IAI/D,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;;OAGG;IACG,SAAS,IAAI,OAAO,CAAC,eAAe,CAAC;IA+BrC,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAgCxD"}
@@ -0,0 +1,128 @@
1
+ import { sanitizeCompletion } from "./truncate.js";
2
+ const PREFLIGHT_TIMEOUT_MS = 5_000;
3
+ /**
4
+ * Provider for FIM-completions backends (e.g. Mercury Edit 2 by Inception
5
+ * Labs). Calls `/fim/completions` with `prompt`/`suffix` and returns the
6
+ * raw text completion.
7
+ *
8
+ * A {@link preflight} method validates the API key at startup so the host
9
+ * can surface configuration errors early instead of silently dropping
10
+ * every prediction.
11
+ */
12
+ export class OpenAICompatProvider {
13
+ config;
14
+ name;
15
+ constructor(config) {
16
+ this.config = config;
17
+ this.name = `ghost-${config.providerName}`;
18
+ }
19
+ get debounceMs() {
20
+ return this.config.debounceMs;
21
+ }
22
+ /**
23
+ * Validate the API key with a minimal FIM request. A successful
24
+ * response (even an empty completion) proves auth + connectivity.
25
+ */
26
+ async preflight() {
27
+ const apiKey = this.config.apiKey;
28
+ if (!apiKey)
29
+ return { ok: false, message: "no API key configured" };
30
+ const ctrl = new AbortController();
31
+ const timeout = setTimeout(() => ctrl.abort(), PREFLIGHT_TIMEOUT_MS);
32
+ try {
33
+ const res = await fetch(`${trimSlash(this.config.baseUrl)}/fim/completions`, {
34
+ method: "POST",
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ Authorization: `Bearer ${apiKey}`,
38
+ },
39
+ body: JSON.stringify({
40
+ model: this.config.modelId,
41
+ prompt: " ",
42
+ max_tokens: 1,
43
+ }),
44
+ signal: ctrl.signal,
45
+ });
46
+ clearTimeout(timeout);
47
+ if (res.ok)
48
+ return { ok: true, status: res.status, message: "ok" };
49
+ return { ok: false, status: res.status, message: describeHttpError(res.status) };
50
+ }
51
+ catch (err) {
52
+ clearTimeout(timeout);
53
+ const aborted = err?.name === "AbortError";
54
+ if (aborted)
55
+ return { ok: false, message: "endpoint timed out" };
56
+ return { ok: false, message: "endpoint unreachable" };
57
+ }
58
+ }
59
+ async predict(req) {
60
+ const apiKey = this.config.apiKey;
61
+ if (!apiKey)
62
+ return null;
63
+ const { prompt, suffix } = buildFIMPrompt(req, this.config.maxRecentMessages);
64
+ try {
65
+ const res = await fetch(`${trimSlash(this.config.baseUrl)}/fim/completions`, {
66
+ method: "POST",
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ Authorization: `Bearer ${apiKey}`,
70
+ },
71
+ body: JSON.stringify({
72
+ model: this.config.modelId,
73
+ prompt,
74
+ suffix,
75
+ max_tokens: this.config.maxTokens,
76
+ temperature: 0.2,
77
+ }),
78
+ signal: req.signal,
79
+ });
80
+ if (!res.ok)
81
+ return null;
82
+ const data = (await res.json());
83
+ const text = data.choices?.[0]?.text ?? "";
84
+ return sanitizeCompletion(text, req.editorTextBeforeCursor, 200);
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Build the FIM prompt/suffix pair from the ghost request.
93
+ *
94
+ * Recent conversation messages are prepended as comments in the prompt
95
+ * so the model has context about what the user is working on.
96
+ */
97
+ function buildFIMPrompt(req, maxRecent) {
98
+ const recent = req.recentMessages
99
+ .slice(-maxRecent)
100
+ .map((m) => `# ${m.role}: ${truncate(m.text, 280)}`)
101
+ .join("\n");
102
+ const ctx = recent ? `${recent}\n\n` : "";
103
+ // Split the editor text at cursor to form prompt (before) / suffix (after).
104
+ // Ghost autocomplete only fires when cursor is at end, so suffix is empty
105
+ // in the common case — but we handle the general case for correctness.
106
+ const before = req.editorTextBeforeCursor;
107
+ const after = req.editorText.slice(req.editorTextBeforeCursor.length);
108
+ return {
109
+ prompt: `${ctx}${before}`,
110
+ suffix: after,
111
+ };
112
+ }
113
+ function truncate(s, n) {
114
+ return s.length > n ? `${s.slice(0, n)}…` : s;
115
+ }
116
+ function trimSlash(url) {
117
+ return url.endsWith("/") ? url.slice(0, -1) : url;
118
+ }
119
+ function describeHttpError(status) {
120
+ switch (status) {
121
+ case 401: return "invalid API key (401 Unauthorized)";
122
+ case 402: return "API key has no credits — please top up (402 Payment Required)";
123
+ case 403: return "API key not permitted (403 Forbidden)";
124
+ case 429: return "rate limited (429 Too Many Requests)";
125
+ default: return `HTTP ${status}`;
126
+ }
127
+ }
128
+ //# sourceMappingURL=openai-compat-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-compat-provider.js","sourceRoot":"","sources":["../../src/providers/openai-compat-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { GhostRequest } from "./types.js";
2
+ export interface OpenAICompletionsCallOpts {
3
+ baseUrl: string;
4
+ apiKey: string;
5
+ modelId: string;
6
+ /**
7
+ * Metadata tag for pi-ai's `Model.provider`. Cosmetic for logs — the
8
+ * actual endpoint is driven by `baseUrl`. Accepts any string; cast to
9
+ * `KnownProvider` at the call site since the field is purely descriptive.
10
+ */
11
+ providerTag: string;
12
+ maxTokens: number;
13
+ maxRecentMessages: number;
14
+ contextWindow: number;
15
+ }
16
+ /**
17
+ * Shared OpenAI-completions request path used by both {@link LocalProvider}
18
+ * and {@link OpenAICompatProvider}. Builds the pi-ai model record, sends one
19
+ * `complete()` call with the user's abort signal, and returns a sanitized
20
+ * completion string (or null when the call aborts/errors/produces nothing
21
+ * usable).
22
+ *
23
+ * Kept as a free function rather than a base class to avoid one-method
24
+ * inheritance.
25
+ */
26
+ export declare function runOpenAICompletion(req: GhostRequest, opts: OpenAICompletionsCallOpts): Promise<string | null>;
27
+ export declare function trimSlash(url: string): string;
28
+ //# sourceMappingURL=openai-completions-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-completions-runner.d.ts","sourceRoot":"","sources":["../../src/providers/openai-completions-runner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,yBAAyB;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,yBAAyB,GAC7B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BxB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C"}
@@ -0,0 +1,46 @@
1
+ import { complete } from "@earendil-works/pi-ai";
2
+ import { buildPromptMessages, sanitizeCompletion } from "./truncate.js";
3
+ /**
4
+ * Shared OpenAI-completions request path used by both {@link LocalProvider}
5
+ * and {@link OpenAICompatProvider}. Builds the pi-ai model record, sends one
6
+ * `complete()` call with the user's abort signal, and returns a sanitized
7
+ * completion string (or null when the call aborts/errors/produces nothing
8
+ * usable).
9
+ *
10
+ * Kept as a free function rather than a base class to avoid one-method
11
+ * inheritance.
12
+ */
13
+ export async function runOpenAICompletion(req, opts) {
14
+ const model = {
15
+ id: opts.modelId,
16
+ name: opts.modelId,
17
+ api: "openai-completions",
18
+ provider: opts.providerTag,
19
+ baseUrl: trimSlash(opts.baseUrl),
20
+ reasoning: false,
21
+ input: ["text"],
22
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
23
+ contextWindow: opts.contextWindow,
24
+ maxTokens: opts.maxTokens,
25
+ };
26
+ const { systemPrompt, user } = buildPromptMessages(req, opts.maxRecentMessages);
27
+ const messages = [{ role: "user", content: user, timestamp: Date.now() }];
28
+ const ctx = { systemPrompt, messages };
29
+ const result = await complete(model, ctx, {
30
+ signal: req.signal,
31
+ temperature: 0.2,
32
+ maxTokens: opts.maxTokens,
33
+ apiKey: opts.apiKey,
34
+ });
35
+ if (result.stopReason === "error" || result.stopReason === "aborted")
36
+ return null;
37
+ const text = result.content
38
+ .filter((c) => c.type === "text")
39
+ .map((c) => c.text)
40
+ .join("");
41
+ return sanitizeCompletion(text, req.editorTextBeforeCursor, 200);
42
+ }
43
+ export function trimSlash(url) {
44
+ return url.endsWith("/") ? url.slice(0, -1) : url;
45
+ }
46
+ //# sourceMappingURL=openai-completions-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-completions-runner.js","sourceRoot":"","sources":["../../src/providers/openai-completions-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAA8D,MAAM,uBAAuB,CAAC;AAC7G,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAkBxE;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,GAAiB,EACjB,IAA+B;IAE/B,MAAM,KAAK,GAAgC;QAC1C,EAAE,EAAE,IAAI,CAAC,OAAO;QAChB,IAAI,EAAE,IAAI,CAAC,OAAO;QAClB,GAAG,EAAE,oBAAoB;QACzB,QAAQ,EAAE,IAAI,CAAC,WAA4B;QAC3C,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;QAChC,SAAS,EAAE,KAAK;QAChB,KAAK,EAAE,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;QAC1D,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,SAAS,EAAE,IAAI,CAAC,SAAS;KACzB,CAAC;IAEF,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,GAAG,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACrF,MAAM,GAAG,GAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;IAEhD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE;QACzC,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,WAAW,EAAE,GAAG;QAChB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,MAAM,EAAE,IAAI,CAAC,MAAM;KACnB,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,UAAU,KAAK,OAAO,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAClF,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO;SACzB,MAAM,CAAC,CAAC,CAAC,EAAuC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;SACrE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SAClB,IAAI,CAAC,EAAE,CAAC,CAAC;IACX,OAAO,kBAAkB,CAAC,IAAI,EAAE,GAAG,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAW;IACpC,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AACnD,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { ProviderLatency } from "../metrics.js";
2
+ import type { GhostProvider, GhostRequest } from "./types.js";
3
+ export interface RaceConfig {
4
+ /** Pause/debounce before race starts. Reported as `debounceMs`. */
5
+ pauseMs: number;
6
+ /** After a non-success race, suppress new races for this long. */
7
+ cooldownMs: number;
8
+ /** Hard timeout from race start; all in-flight providers aborted. */
9
+ cutoffMs: number;
10
+ }
11
+ export interface RaceTrace {
12
+ providers: ProviderLatency[];
13
+ winner?: string;
14
+ }
15
+ /**
16
+ * Implements {@link GhostProvider} by racing several providers in parallel.
17
+ * First non-null result wins; losers are aborted via a shared signal.
18
+ *
19
+ * Trace metadata for the most recent race is exposed via
20
+ * {@link consumeLastTrace}. The editor reads it after `predict()` to emit a
21
+ * multi-provider metrics record. Consumption is one-shot to avoid stale reuse.
22
+ */
23
+ export declare class RaceProvider implements GhostProvider {
24
+ private readonly members;
25
+ private readonly cfg;
26
+ readonly name: string;
27
+ private cooldownUntil;
28
+ private lastTrace;
29
+ constructor(members: GhostProvider[], cfg: RaceConfig);
30
+ get debounceMs(): number;
31
+ consumeLastTrace(): RaceTrace | null;
32
+ predict(req: GhostRequest): Promise<string | null>;
33
+ private runMember;
34
+ }
35
+ /**
36
+ * Resolve to the first non-null value among `promises`. Resolves null when
37
+ * all promises produce null (or throw). Never rejects.
38
+ */
39
+ export declare function firstNonNull<T>(promises: Array<Promise<T | null>>): Promise<T | null>;
40
+ //# sourceMappingURL=race.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"race.d.ts","sourceRoot":"","sources":["../../src/providers/race.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,UAAU;IAC1B,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACzB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,qBAAa,YAAa,YAAW,aAAa;IAMhD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG;IANrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,SAAS,CAA0B;gBAGzB,OAAO,EAAE,aAAa,EAAE,EACxB,GAAG,EAAE,UAAU;IAKjC,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,gBAAgB,IAAI,SAAS,GAAG,IAAI;IAM9B,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;YA6C1C,SAAS;CA8BvB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAyBrF"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Implements {@link GhostProvider} by racing several providers in parallel.
3
+ * First non-null result wins; losers are aborted via a shared signal.
4
+ *
5
+ * Trace metadata for the most recent race is exposed via
6
+ * {@link consumeLastTrace}. The editor reads it after `predict()` to emit a
7
+ * multi-provider metrics record. Consumption is one-shot to avoid stale reuse.
8
+ */
9
+ export class RaceProvider {
10
+ members;
11
+ cfg;
12
+ name;
13
+ cooldownUntil = 0;
14
+ lastTrace = null;
15
+ constructor(members, cfg) {
16
+ this.members = members;
17
+ this.cfg = cfg;
18
+ this.name = `ghost-race:${members.map((p) => p.name).join(",")}`;
19
+ }
20
+ get debounceMs() {
21
+ return this.cfg.pauseMs;
22
+ }
23
+ consumeLastTrace() {
24
+ const t = this.lastTrace;
25
+ this.lastTrace = null;
26
+ return t;
27
+ }
28
+ async predict(req) {
29
+ if (this.members.length === 0)
30
+ return null;
31
+ const now = Date.now();
32
+ if (now < this.cooldownUntil) {
33
+ this.lastTrace = { providers: [] };
34
+ return null;
35
+ }
36
+ const trace = [];
37
+ const localCtrl = new AbortController();
38
+ const start = now;
39
+ // Propagate caller cancellation; do not propagate our own internal aborts.
40
+ const onUserAbort = () => localCtrl.abort();
41
+ if (req.signal.aborted) {
42
+ localCtrl.abort();
43
+ }
44
+ else {
45
+ req.signal.addEventListener("abort", onUserAbort, { once: true });
46
+ }
47
+ const cutoffTimer = setTimeout(() => localCtrl.abort(), this.cfg.cutoffMs);
48
+ try {
49
+ const races = this.members.map((p) => this.runMember(p, { ...req, signal: localCtrl.signal }, start, trace));
50
+ const winner = await firstNonNull(races);
51
+ if (winner) {
52
+ localCtrl.abort();
53
+ this.lastTrace = { providers: trace, winner: winner.provider };
54
+ return winner.completion;
55
+ }
56
+ // Skip cooldown when the caller cancelled — that's a normal stutter,
57
+ // not a sign the providers are failing. Only true non-success (empty,
58
+ // rejected, error, internal-cutoff) should throttle the next race.
59
+ if (!req.signal.aborted) {
60
+ this.cooldownUntil = Date.now() + this.cfg.cooldownMs;
61
+ }
62
+ this.lastTrace = { providers: trace };
63
+ return null;
64
+ }
65
+ finally {
66
+ clearTimeout(cutoffTimer);
67
+ req.signal.removeEventListener("abort", onUserAbort);
68
+ }
69
+ }
70
+ async runMember(provider, req, start, trace) {
71
+ try {
72
+ const result = await provider.predict(req);
73
+ const latencyMs = Date.now() - start;
74
+ if (req.signal.aborted) {
75
+ trace.push({ provider: provider.name, latencyMs, result: "aborted" });
76
+ return null;
77
+ }
78
+ if (result === null || result === "") {
79
+ trace.push({ provider: provider.name, latencyMs, result: "empty" });
80
+ return null;
81
+ }
82
+ trace.push({ provider: provider.name, latencyMs, result: "produced" });
83
+ return { provider: provider.name, completion: result };
84
+ }
85
+ catch (err) {
86
+ const latencyMs = Date.now() - start;
87
+ const aborted = err?.name === "AbortError";
88
+ trace.push({
89
+ provider: provider.name,
90
+ latencyMs,
91
+ result: aborted ? "aborted" : "error",
92
+ });
93
+ return null;
94
+ }
95
+ }
96
+ }
97
+ /**
98
+ * Resolve to the first non-null value among `promises`. Resolves null when
99
+ * all promises produce null (or throw). Never rejects.
100
+ */
101
+ export function firstNonNull(promises) {
102
+ if (promises.length === 0)
103
+ return Promise.resolve(null);
104
+ return new Promise((resolve) => {
105
+ let remaining = promises.length;
106
+ let settled = false;
107
+ const finish = (v) => {
108
+ if (settled)
109
+ return;
110
+ settled = true;
111
+ resolve(v);
112
+ };
113
+ for (const p of promises) {
114
+ p.then((v) => {
115
+ if (v !== null) {
116
+ finish(v);
117
+ return;
118
+ }
119
+ if (--remaining === 0)
120
+ finish(null);
121
+ }, () => {
122
+ if (--remaining === 0)
123
+ finish(null);
124
+ });
125
+ }
126
+ });
127
+ }
128
+ //# sourceMappingURL=race.js.map