@xynogen/pix-core 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-core",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Pi extension — core UI/UX bundle (welcome banner, footer, model picker, self-update)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -42,6 +42,7 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
+ "@xynogen/pix-data": "^0.1.0",
45
46
  "@xynogen/pix-skills": "^0.1.1",
46
47
  "typebox": "^1.1.38"
47
48
  },
package/src/lib/data.ts CHANGED
@@ -1,241 +1,33 @@
1
1
  /**
2
2
  * data.ts — model data layer (shim)
3
3
  *
4
- * Re-exports shared data from pix-data (github.com/xynogen/pix-data).
5
- * Cache lives at ~/.cache/pi/ and is shared across all Pi extensions.
4
+ * Thin re-export of the shared data layer from @xynogen/pix-data
5
+ * (github.com/xynogen/pix-mono/tree/main/packages/pix-data). Cache lives at
6
+ * ~/.cache/pi/ and is shared across all Pi extensions — pix-data warms it on
7
+ * session start; this extension reads from it.
6
8
  *
7
9
  * Consumers in this extension dir:
8
10
  * footer.ts — lookupModelsDev, lookupBenchmark, ModelsDevModel
9
11
  * models.ts — lookupModelsDev, lookupBenchmark
10
12
  */
11
13
 
12
- import { existsSync, readFileSync } from "node:fs";
13
- import { mkdir, readFile, writeFile } from "node:fs/promises";
14
- import { homedir } from "node:os";
15
- import { dirname, join } from "node:path";
16
-
17
- // ── Types ─────────────────────────────────────────────────────────────────────
18
-
19
- export interface ModelsDevModel {
20
- id: string;
21
- name?: string;
22
- reasoning?: boolean;
23
- modalities?: { input?: string[]; output?: string[] };
24
- limit?: { context?: number; output?: number };
25
- cost?: {
26
- input?: number;
27
- output?: number;
28
- cache_read?: number;
29
- cache_write?: number;
30
- };
31
- }
32
-
33
- export type ModelsDevApi = Record<
34
- string,
35
- { models?: Record<string, ModelsDevModel> }
36
- >;
37
-
38
- export interface BenchmarkEntry {
39
- rank: number;
40
- model: string;
41
- creator: string;
42
- sourceType?: string;
43
- overallScore: number | null;
44
- categoryScores?: Record<string, number | null>;
45
- inputPrice: number | null;
46
- outputPrice: number | null;
47
- }
48
-
49
- interface BenchmarkResponse {
50
- lastUpdated?: string;
51
- mode?: string;
52
- models: BenchmarkEntry[];
53
- }
54
-
55
- // ── DataSource ─────────────────────────────────────────────────────────────────
56
-
57
- interface DataSourceOptions<T> {
58
- url: string | (() => string);
59
- headers?: () => Record<string, string> | undefined;
60
- cachePath: string;
61
- ttlMs?: number;
62
- timeoutMs?: number;
63
- parse: (raw: unknown) => T;
64
- parseCache: (data: unknown) => T;
65
- empty: T;
66
- label: string;
67
- skip?: () => boolean;
68
- }
69
-
70
- class DataSource<T> {
71
- private _mem: T | null = null;
72
- private _inflight: Promise<T> | null = null;
73
- private readonly opts: Required<DataSourceOptions<T>>;
74
-
75
- constructor(opts: DataSourceOptions<T>) {
76
- this.opts = {
77
- ttlMs: 24 * 60 * 60 * 1000,
78
- timeoutMs: 10_000,
79
- headers: () => undefined,
80
- skip: () => false,
81
- ...opts,
82
- };
83
- }
84
-
85
- async get(): Promise<T> {
86
- if (this._inflight) return this._inflight;
87
- this._inflight = this._load().finally(() => {
88
- this._inflight = null;
89
- });
90
- return this._inflight;
91
- }
92
-
93
- getCached(): T {
94
- if (this._mem) return this._mem;
95
- try {
96
- if (existsSync(this.opts.cachePath)) {
97
- const raw = JSON.parse(readFileSync(this.opts.cachePath, "utf-8")) as {
98
- data: unknown;
99
- };
100
- this._mem = this.opts.parseCache(raw.data);
101
- return this._mem;
102
- }
103
- } catch {
104
- // No cache file or parse error — return empty
105
- }
106
- return this.opts.empty;
107
- }
108
-
109
- private async _load(): Promise<T> {
110
- if (this.opts.skip()) {
111
- this._mem = this.opts.empty;
112
- return this.opts.empty;
113
- }
114
- const cached = await this._readCache();
115
- if (cached !== undefined && Date.now() - cached.ts < this.opts.ttlMs) {
116
- const val = this.opts.parseCache(cached.data);
117
- this._mem = val;
118
- return val;
119
- }
120
- try {
121
- const url =
122
- typeof this.opts.url === "function" ? this.opts.url() : this.opts.url;
123
- const controller = new AbortController();
124
- const timer = setTimeout(() => controller.abort(), this.opts.timeoutMs);
125
- const response = await fetch(url, {
126
- signal: controller.signal,
127
- headers: this.opts.headers(),
128
- }).finally(() => clearTimeout(timer));
129
- if (!response.ok)
130
- throw new Error(`${this.opts.label} fetch failed: ${response.status}`);
131
- const raw = await response.json();
132
- const val = this.opts.parse(raw);
133
- this._mem = val;
134
- void this._writeCache(raw);
135
- return val;
136
- } catch (error) {
137
- const msg = error instanceof Error ? error.message : String(error);
138
- if (cached !== undefined) {
139
- console.warn(
140
- `${this.opts.label} fetch failed, using stale cache: ${msg}`,
141
- );
142
- const val = this.opts.parseCache(cached.data);
143
- this._mem = val;
144
- return val;
145
- }
146
- console.warn(`${this.opts.label} unavailable: ${msg}`);
147
- return this.opts.empty;
148
- }
149
- }
150
-
151
- private async _readCache(): Promise<
152
- { ts: number; data: unknown } | undefined
153
- > {
154
- try {
155
- const raw = await readFile(this.opts.cachePath, "utf8");
156
- const parsed = JSON.parse(raw) as { ts: number; data: unknown };
157
- if (typeof parsed.ts !== "number") return undefined;
158
- return parsed;
159
- } catch {
160
- return undefined;
161
- }
162
- }
163
-
164
- private async _writeCache(data: unknown): Promise<void> {
165
- try {
166
- await mkdir(dirname(this.opts.cachePath), { recursive: true });
167
- await writeFile(
168
- this.opts.cachePath,
169
- JSON.stringify({ ts: Date.now(), data }),
170
- );
171
- } catch {
172
- // Write failure is non-fatal
173
- }
174
- }
175
- }
176
-
177
- // ── Cache dir ─────────────────────────────────────────────────────────────────
178
-
179
- const CACHE_DIR = join(
180
- process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
181
- "pi",
182
- );
183
-
184
- // ── Data sources (shared cache with pix-data) ─────────────────────────────────
185
-
186
- const modelsDev = new DataSource<ModelsDevApi>({
187
- label: "models.dev",
188
- url: "https://models.dev/api.json",
189
- cachePath: join(CACHE_DIR, "models.json"),
190
- parse: (raw) => raw as ModelsDevApi,
191
- parseCache: (data) => (data as ModelsDevApi) ?? {},
192
- empty: {},
193
- });
194
-
195
- const benchmark = new DataSource<BenchmarkEntry[]>({
196
- label: "benchlm",
197
- url: "https://benchlm.ai/api/data/leaderboard",
198
- cachePath: join(CACHE_DIR, "benchlm.json"),
199
- parse: (raw) => (raw as BenchmarkResponse).models ?? [],
200
- parseCache: (data) => (data as BenchmarkResponse)?.models ?? [],
201
- empty: [],
202
- });
203
-
204
- // ── Lookup helpers ─────────────────────────────────────────────────────────────
205
-
206
- export function lookupModelsDev(
207
- provider: string,
208
- id: string,
209
- ): ModelsDevModel | undefined {
210
- const data = modelsDev.getCached();
211
- const canonical = id.includes("/") ? id.slice(id.lastIndexOf("/") + 1) : id;
212
- const exact = data[provider]?.models?.[canonical];
213
- if (exact) return exact;
214
- for (const p of Object.keys(data)) {
215
- const hit = data[p]?.models?.[canonical];
216
- if (hit) return hit;
217
- }
218
- return undefined;
219
- }
220
-
221
- function normBench(s: string): string {
222
- return s
223
- .toLowerCase()
224
- .replace(/[-_.]+/g, " ")
225
- .replace(/\s+/g, " ")
226
- .trim();
227
- }
228
-
229
- export function lookupBenchmark(modelName: string): BenchmarkEntry | undefined {
230
- const entries = benchmark.getCached();
231
- const needle = normBench(modelName);
232
- return (
233
- entries.find((e) => normBench(e.model) === needle) ??
234
- entries.find((e) => normBench(e.model).includes(needle)) ??
235
- entries.find((e) => needle.includes(normBench(e.model)))
236
- );
237
- }
14
+ export {
15
+ benchmark,
16
+ buildModelsDevIndex,
17
+ CACHE_DIR,
18
+ DataSource,
19
+ fetchModelsDevIndex,
20
+ lookupBenchmark,
21
+ lookupInIndex,
22
+ lookupModelsDev,
23
+ modelsDev,
24
+ } from "@xynogen/pix-data";
25
+ export type {
26
+ BenchmarkEntry,
27
+ ModelsDevApi,
28
+ ModelsDevModel,
29
+ } from "@xynogen/pix-data";
238
30
 
239
31
  export default function (_pi: unknown): void {
240
- // pix-data warms this cache on startup — nothing to do here
32
+ // pix-data warms this cache on startup — nothing to do here.
241
33
  }
@@ -1,8 +1,11 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { describe, expect, test } from "bun:test";
2
4
  import {
3
5
  buildOrientation,
4
6
  CAPABILITY_REMINDER,
5
7
  countInvocableSkills,
8
+ graphifyHint,
6
9
  partitionTools,
7
10
  } from "./capability.ts";
8
11
 
@@ -201,4 +204,32 @@ describe("buildOrientation", () => {
201
204
  const out = buildOrientation([tool("read", "builtin")], []);
202
205
  expect(out.toLowerCase()).toContain("improvis");
203
206
  });
207
+
208
+ test("frames the block as non-actionable so the model acts on the prompt", () => {
209
+ const out = buildOrientation([tool("read", "builtin")], []);
210
+ const last = out.trim().split("\n").at(-1) ?? "";
211
+ expect(last.toLowerCase()).toContain("not a task");
212
+ expect(last.toLowerCase()).toContain("do not reply");
213
+ });
214
+ });
215
+
216
+ describe("graphifyHint", () => {
217
+ const tmpDir = join(import.meta.dir, ".graphify-hint-test-tmp");
218
+
219
+ test("returns undefined when graphify-out/graph.json absent", () => {
220
+ expect(graphifyHint(tmpDir)).toBeUndefined();
221
+ });
222
+
223
+ test("returns hint string when graphify-out/graph.json exists", () => {
224
+ try {
225
+ mkdirSync(join(tmpDir, "graphify-out"), { recursive: true });
226
+ writeFileSync(join(tmpDir, "graphify-out", "graph.json"), "{}");
227
+ const hint = graphifyHint(tmpDir);
228
+ expect(hint).toBeTypeOf("string");
229
+ expect(hint).toContain("graphify");
230
+ expect(hint).toContain("graphify query");
231
+ } finally {
232
+ rmSync(tmpDir, { recursive: true, force: true });
233
+ }
234
+ });
204
235
  });
@@ -20,6 +20,8 @@
20
20
  * The `skill` tool IS model-callable: skill() lists/loads bundled skills.
21
21
  */
22
22
 
23
+ import { existsSync } from "node:fs";
24
+ import { join } from "node:path";
23
25
  import type {
24
26
  BuildSystemPromptOptions,
25
27
  ExtensionAPI,
@@ -36,6 +38,20 @@ export const CAPABILITY_REMINDER =
36
38
  "Use /toolbox to discover/enable gated tools. " +
37
39
  "All tools callable via function definitions.";
38
40
 
41
+ /**
42
+ * Build the optional graphify hint line.
43
+ * Returns a string if graphify-out/graph.json exists in cwd, else undefined.
44
+ */
45
+ export function graphifyHint(cwd: string): string | undefined {
46
+ if (existsSync(join(cwd, "graphify-out", "graph.json"))) {
47
+ return (
48
+ "graphify-out/graph.json exists — for codebase questions (how does X work, " +
49
+ 'where is Y, trace Z) run `graphify query "<question>"` before reading files.'
50
+ );
51
+ }
52
+ return undefined;
53
+ }
54
+
39
55
  /** Count model-invocable skills (excludes user-only /skill:name entries). */
40
56
  export function countInvocableSkills(
41
57
  skills: LoadedSkill[] | undefined,
@@ -118,6 +134,15 @@ export function buildOrientation(
118
134
  if (skillNames.length) {
119
135
  lines.push(`Skills: ${skillNames.join(", ")}.`);
120
136
  }
137
+ // Graphify hint — only when a graph is already built for this project
138
+ const gHint = graphifyHint(process.cwd());
139
+ if (gHint) lines.push(gHint);
140
+ // Framing — this block is orientation context, not a task. Without it the
141
+ // model can mistake the first-turn orientation for the prompt and reply
142
+ // "Ready, waiting for task" instead of acting on the user's request.
143
+ lines.push(
144
+ "(Orientation only — not a task. Act on the user's request now; do not reply to this notice.)",
145
+ );
121
146
  return lines.join("\n");
122
147
  }
123
148
 
@@ -146,7 +171,12 @@ export default function registerCapabilityNudge(pi: ExtensionAPI): void {
146
171
  }
147
172
  content = buildOrientation(tools, skills, activeToolNames);
148
173
  } else {
149
- content = CAPABILITY_REMINDER;
174
+ // Per-turn reminder — append graphify hint when graph exists
175
+ const cwd = process.cwd();
176
+ const gHint = graphifyHint(cwd);
177
+ content = gHint
178
+ ? `${CAPABILITY_REMINDER}\n${gHint}`
179
+ : CAPABILITY_REMINDER;
150
180
  }
151
181
 
152
182
  return {