@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 +2 -1
- package/src/lib/data.ts +21 -229
- package/src/nudge/capability.test.ts +31 -0
- package/src/nudge/capability.ts +31 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-core",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
*
|
|
5
|
-
* Cache lives at
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
});
|
package/src/nudge/capability.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|