@gonrocca/zero-pi 0.1.14 → 0.1.16
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.
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// zero-pi — OpenCode model-catalog discovery.
|
|
2
|
+
//
|
|
3
|
+
// Surfaces the model providers a user can run by cross-referencing OpenCode's
|
|
4
|
+
// model catalog cache with the user's OpenCode auth file. This is the same
|
|
5
|
+
// approach the `zero` installer's model picker uses, so `/zero-models` offers
|
|
6
|
+
// the same providers — opencode-go, openai (codex/gpt), anthropic, … — as the
|
|
7
|
+
// installer, instead of a hardcoded Claude list.
|
|
8
|
+
//
|
|
9
|
+
// Sources, under the user's home (ZERO_HOME-overridable for tests):
|
|
10
|
+
// ~/.cache/opencode/models.json — the full provider/model catalog
|
|
11
|
+
// ~/.local/share/opencode/auth.json — the providers the user authenticated
|
|
12
|
+
//
|
|
13
|
+
// Discovery is total: a missing or corrupt file yields an empty list, so a
|
|
14
|
+
// user without OpenCode simply falls back to pi's own model registry.
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
/** A model discovered from OpenCode's catalog. */
|
|
21
|
+
export interface DiscoveredModel {
|
|
22
|
+
/** The provider id, e.g. `opencode-go`, `openai`, `anthropic`. */
|
|
23
|
+
provider: string;
|
|
24
|
+
/** The model id, e.g. `gpt-5-codex`. */
|
|
25
|
+
id: string;
|
|
26
|
+
/** The human-readable model name. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Whether the model supports tool calls — required for the SDD phases. */
|
|
29
|
+
toolCall: boolean;
|
|
30
|
+
/** Whether the model exposes a reasoning-effort setting. */
|
|
31
|
+
reasoning: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The home directory discovery resolves OpenCode paths against. */
|
|
35
|
+
function discoveryHome(): string {
|
|
36
|
+
const override = process.env.ZERO_HOME;
|
|
37
|
+
return typeof override === "string" && override.trim() !== "" ? override : homedir();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True for a plain (non-array) object. */
|
|
41
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read and parse a JSON file; returns null on any failure. */
|
|
46
|
+
function readJson(path: string): unknown {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract a provider's models from its OpenCode catalog entry.
|
|
56
|
+
*
|
|
57
|
+
* Exported for tests.
|
|
58
|
+
*/
|
|
59
|
+
export function extractModels(
|
|
60
|
+
providerId: string,
|
|
61
|
+
entry: Record<string, unknown>,
|
|
62
|
+
): DiscoveredModel[] {
|
|
63
|
+
const raw = entry.models;
|
|
64
|
+
if (!isObject(raw)) return [];
|
|
65
|
+
|
|
66
|
+
const models: DiscoveredModel[] = [];
|
|
67
|
+
for (const [id, model] of Object.entries(raw)) {
|
|
68
|
+
if (!isObject(model)) continue;
|
|
69
|
+
models.push({
|
|
70
|
+
provider: providerId,
|
|
71
|
+
id,
|
|
72
|
+
name: typeof model.name === "string" ? model.name : id,
|
|
73
|
+
toolCall: model.tool_call === true,
|
|
74
|
+
reasoning: model.reasoning === true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return models;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Discover every model the user can run, flattened across all OpenCode-
|
|
82
|
+
* authenticated providers.
|
|
83
|
+
*
|
|
84
|
+
* `catalogJson` and `authJson` are injectable for tests; in normal use they
|
|
85
|
+
* are read from OpenCode's cache and auth files on disk. The result is empty
|
|
86
|
+
* when either source is absent or malformed.
|
|
87
|
+
*/
|
|
88
|
+
export function discoverModels(
|
|
89
|
+
catalogJson?: unknown,
|
|
90
|
+
authJson?: unknown,
|
|
91
|
+
): DiscoveredModel[] {
|
|
92
|
+
// Only touch disk when an argument is genuinely absent — an explicit value
|
|
93
|
+
// (including `null`) is used as given, so tests never read real files.
|
|
94
|
+
const auth =
|
|
95
|
+
authJson === undefined
|
|
96
|
+
? readJson(join(discoveryHome(), ".local", "share", "opencode", "auth.json"))
|
|
97
|
+
: authJson;
|
|
98
|
+
const catalog =
|
|
99
|
+
catalogJson === undefined
|
|
100
|
+
? readJson(join(discoveryHome(), ".cache", "opencode", "models.json"))
|
|
101
|
+
: catalogJson;
|
|
102
|
+
if (!isObject(auth) || !isObject(catalog)) return [];
|
|
103
|
+
|
|
104
|
+
const models: DiscoveredModel[] = [];
|
|
105
|
+
for (const providerId of Object.keys(auth)) {
|
|
106
|
+
const entry = catalog[providerId];
|
|
107
|
+
if (!isObject(entry)) continue;
|
|
108
|
+
models.push(...extractModels(providerId, entry));
|
|
109
|
+
}
|
|
110
|
+
return models;
|
|
111
|
+
}
|
|
@@ -83,7 +83,7 @@ export default function register(): void {
|
|
|
83
83
|
const require = createRequire(import.meta.url);
|
|
84
84
|
const cp = require("node:child_process") as {
|
|
85
85
|
spawn: (...args: unknown[]) => KillableChild;
|
|
86
|
-
|
|
86
|
+
exec: (command: string, callback: (error: unknown) => void) => unknown;
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
const originalSpawn = cp.spawn as typeof cp.spawn & { [WRAPPED]?: boolean };
|
|
@@ -92,8 +92,12 @@ export default function register(): void {
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Fire-and-forget, ASYNC. A blocking execSync here would freeze pi's whole
|
|
96
|
+
// event loop on every subprocess kill — and pi kills a subprocess on most
|
|
97
|
+
// turns (pi-claude-cli's break-early). taskkill can take seconds or hang,
|
|
98
|
+
// so it must never run on the event loop.
|
|
95
99
|
const exec = (command: string): void => {
|
|
96
|
-
cp.
|
|
100
|
+
cp.exec(command, () => {});
|
|
97
101
|
};
|
|
98
102
|
|
|
99
103
|
const patchedSpawn = function (this: unknown, ...args: unknown[]): KillableChild {
|
|
@@ -169,8 +169,13 @@ export function spinnerFrames(theme: Theme): string[] {
|
|
|
169
169
|
|
|
170
170
|
// ── Extension entry point ──────────────────────────────────────────────────
|
|
171
171
|
|
|
172
|
+
/** Module guard so a double-load never stacks handlers or a second timer. */
|
|
173
|
+
let registered = false;
|
|
174
|
+
|
|
172
175
|
export default function register(pi?: PiAPI): void {
|
|
173
176
|
if (!pi || typeof pi.on !== "function") return;
|
|
177
|
+
if (registered) return;
|
|
178
|
+
registered = true;
|
|
174
179
|
|
|
175
180
|
// The last UI handle seen on any event — the rotation timer uses it.
|
|
176
181
|
let ui: PiUI | undefined;
|
|
@@ -21,6 +21,7 @@ import { join } from "node:path";
|
|
|
21
21
|
|
|
22
22
|
import { readAutotuneMode, type AutotuneMode } from "./autotune.ts";
|
|
23
23
|
import type { AutotunePending } from "./autotune-extension.ts";
|
|
24
|
+
import { discoverModels } from "./opencode-models.ts";
|
|
24
25
|
|
|
25
26
|
/** The SDD phases, in pipeline order. */
|
|
26
27
|
export const PHASES = ["explore", "plan", "build", "veredicto"] as const;
|
|
@@ -211,34 +212,56 @@ const CUSTOM_MODEL = "— otro modelo (escribir) —";
|
|
|
211
212
|
const CUSTOM_PROVIDER = "— otro provider (escribir) —";
|
|
212
213
|
|
|
213
214
|
/**
|
|
214
|
-
* Group
|
|
215
|
-
*
|
|
216
|
-
*
|
|
215
|
+
* Group models by provider for the picker.
|
|
216
|
+
*
|
|
217
|
+
* The primary source is OpenCode's model catalog — the same one the `zero`
|
|
218
|
+
* installer's picker uses — so every provider the user authenticated there
|
|
219
|
+
* (opencode-go, openai/codex, anthropic, …) is offered, with only tool-call-
|
|
220
|
+
* capable models (the SDD phases need tool calls). When OpenCode is not
|
|
221
|
+
* present it falls back to pi's own model registry, then to an empty map.
|
|
217
222
|
*/
|
|
218
223
|
function providerGroups(registry: PiModelRegistry | undefined): Map<string, string[]> {
|
|
219
|
-
if (!registry || typeof registry.getAll !== "function") return new Map();
|
|
220
224
|
try {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
return groupByProvider(source ?? []);
|
|
225
|
+
const discovered = discoverModels().filter((m) => m.toolCall);
|
|
226
|
+
if (discovered.length > 0) return groupByProvider(discovered);
|
|
224
227
|
} catch {
|
|
225
|
-
|
|
228
|
+
/* fall through to pi's registry */
|
|
229
|
+
}
|
|
230
|
+
if (registry && typeof registry.getAll === "function") {
|
|
231
|
+
try {
|
|
232
|
+
const all = registry.getAll();
|
|
233
|
+
if (all && all.length > 0) return groupByProvider(all);
|
|
234
|
+
} catch {
|
|
235
|
+
/* fall through */
|
|
236
|
+
}
|
|
226
237
|
}
|
|
238
|
+
return new Map();
|
|
227
239
|
}
|
|
228
240
|
|
|
229
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* Find the provider that owns a model id — checked against OpenCode's catalog
|
|
243
|
+
* first, then pi's own registry.
|
|
244
|
+
*/
|
|
230
245
|
function resolveProvider(
|
|
231
246
|
registry: PiModelRegistry | undefined,
|
|
232
247
|
modelId: string,
|
|
233
248
|
): string | undefined {
|
|
234
|
-
if (!registry || typeof registry.getAll !== "function") return undefined;
|
|
235
249
|
try {
|
|
236
|
-
for (const m of
|
|
237
|
-
if (m
|
|
250
|
+
for (const m of discoverModels()) {
|
|
251
|
+
if (m.id === modelId) return m.provider;
|
|
238
252
|
}
|
|
239
253
|
} catch {
|
|
240
254
|
/* ignore */
|
|
241
255
|
}
|
|
256
|
+
if (registry && typeof registry.getAll === "function") {
|
|
257
|
+
try {
|
|
258
|
+
for (const m of registry.getAll()) {
|
|
259
|
+
if (m && m.id === modelId && typeof m.provider === "string") return m.provider;
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
/* ignore */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
242
265
|
return undefined;
|
|
243
266
|
}
|
|
244
267
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gonrocca/zero-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, skill auto-learning, and an animated ZERO startup banner. Adds capability to pi without modifying pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"extensions/win-tree-kill.ts",
|
|
43
43
|
"extensions/conversation-resume.ts",
|
|
44
44
|
"extensions/zero-models.ts",
|
|
45
|
+
"extensions/opencode-models.ts",
|
|
45
46
|
"extensions/autotune.ts",
|
|
46
47
|
"extensions/autotune-extension.ts",
|
|
47
48
|
"extensions/spec-merge.ts",
|