docks-kit 0.18.1 → 0.19.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.
- package/AGENTS.md +7 -5
- package/README.md +9 -8
- package/cli/docs/flags.md +2 -2
- package/cli/docs/models.md +16 -7
- package/cli/docs/omp-models.md +2 -2
- package/cli/docs/overview.md +1 -1
- package/cli/docs/sync-layers.md +1 -1
- package/cli/docs/toolchain.md +10 -0
- package/cli/src/commands/docs.ts +8 -2
- package/cli/src/commands/harnesses.ts +2 -2
- package/cli/src/commands/model.ts +5 -2
- package/cli/src/commands/models.ts +50 -23
- package/cli/src/commands/toolchain.ts +36 -18
- package/cli/src/commands/update.ts +6 -6
- package/cli/src/engine-native/claudePluginPasses.ts +26 -11
- package/cli/src/engine-native/codexConfig.ts +6 -6
- package/cli/src/engine-native/codexPlugins.ts +6 -5
- package/cli/src/engine-native/codexToml.ts +10 -6
- package/cli/src/engine-native/harnesses.ts +52 -78
- package/cli/src/engine-native/index.ts +1 -1
- package/cli/src/engine-native/kitDb.ts +250 -0
- package/cli/src/engine-native/liveModels.ts +263 -0
- package/cli/src/engine-native/models.ts +32 -26
- package/cli/src/engine-native/modes.ts +22 -21
- package/cli/src/engine-native/parseArgs.ts +3 -2
- package/cli/src/engine-native/parseModifiers.ts +8 -5
- package/cli/src/engine-native/sharedTypes.d.ts +19 -0
- package/cli/src/engine-native/syncDispatch.ts +1 -1
- package/cli/src/engine-native/toolchain.ts +161 -2
- package/cli/src/generated/sotPayload.ts +4 -4
- package/cli/src/main.ts +2 -2
- package/cli/src/manifests.ts +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { capture, p, which } from "./exec";
|
|
4
|
+
import { LEGACY_SELECTION, readHarnessSelection } from "./harnesses";
|
|
5
|
+
import { isObject, parseJson } from "./jq";
|
|
6
|
+
import { isNonBlankString, readCache, writeCache } from "./kitDb";
|
|
7
|
+
import { modelCatalog } from "./models";
|
|
8
|
+
import type { CatalogSource, CatalogTool, FetchLike, ResolvedCatalog } from "./sharedTypes";
|
|
9
|
+
|
|
10
|
+
export interface LiveCatalogInputs {
|
|
11
|
+
readonly home: string;
|
|
12
|
+
readonly refresh: boolean;
|
|
13
|
+
readonly fetch: FetchLike;
|
|
14
|
+
readonly which: (name: string) => string;
|
|
15
|
+
readonly capture: (cmd: string, args: ReadonlyArray<string>) => Promise<string>;
|
|
16
|
+
readonly now: () => number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const defaultLiveInputs = (home: string, refresh = false): LiveCatalogInputs => ({
|
|
20
|
+
home,
|
|
21
|
+
refresh,
|
|
22
|
+
fetch: globalThis.fetch,
|
|
23
|
+
which,
|
|
24
|
+
capture,
|
|
25
|
+
now: Date.now,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
type LiveModel = ResolvedCatalog["models"][number];
|
|
29
|
+
type LiveLookup = {
|
|
30
|
+
readonly source: Exclude<CatalogSource, "curated">;
|
|
31
|
+
readonly fetchedAt?: string;
|
|
32
|
+
readonly models: ReadonlyArray<LiveModel>;
|
|
33
|
+
};
|
|
34
|
+
type Lookup = LiveLookup | { readonly fallbackReason: string };
|
|
35
|
+
|
|
36
|
+
export function curatedCatalog(tool: CatalogTool): ResolvedCatalog {
|
|
37
|
+
if (tool === "omp") return { tool, source: "curated", verified: "?", models: [] };
|
|
38
|
+
return { tool, source: "curated", ...modelCatalog(tool) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mergeCurated(curated: ResolvedCatalog, live: ReadonlyArray<LiveModel>): Array<LiveModel> {
|
|
42
|
+
const models: Array<LiveModel> = [];
|
|
43
|
+
const notes = new Map<string, string>();
|
|
44
|
+
for (const entry of curated.models) {
|
|
45
|
+
if (entry.kind === "alias") models.push(entry);
|
|
46
|
+
if (entry.note !== undefined) notes.set(entry.id, entry.note);
|
|
47
|
+
}
|
|
48
|
+
for (const entry of live) {
|
|
49
|
+
const note = notes.get(entry.id) ?? entry.note;
|
|
50
|
+
models.push(
|
|
51
|
+
note === undefined ? { id: entry.id, kind: "id" } : { id: entry.id, kind: "id", note },
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return models;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function resolveCatalog(
|
|
58
|
+
tool: CatalogTool,
|
|
59
|
+
inputs: LiveCatalogInputs,
|
|
60
|
+
): Promise<ResolvedCatalog> {
|
|
61
|
+
const curated = curatedCatalog(tool);
|
|
62
|
+
if (!(readHarnessSelection(inputs.home) ?? LEGACY_SELECTION).includes(tool)) {
|
|
63
|
+
return { ...curated, fallbackReason: `${tool} harness not enabled (docks-kit harnesses)` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lookup =
|
|
67
|
+
tool === "claude"
|
|
68
|
+
? await claudeCatalog(inputs)
|
|
69
|
+
: tool === "codex"
|
|
70
|
+
? codexCatalog(inputs.home)
|
|
71
|
+
: await ompCatalog(inputs);
|
|
72
|
+
if ("fallbackReason" in lookup) return { ...curated, fallbackReason: lookup.fallbackReason };
|
|
73
|
+
return {
|
|
74
|
+
...curated,
|
|
75
|
+
source: lookup.source,
|
|
76
|
+
...(lookup.fetchedAt === undefined ? {} : { fetchedAt: lookup.fetchedAt }),
|
|
77
|
+
models: tool === "omp" ? lookup.models : mergeCurated(curated, lookup.models),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ANTHROPIC_CACHE_KEY = "models:anthropic";
|
|
82
|
+
const ANTHROPIC_CACHE_TTL = 6 * 3600_000;
|
|
83
|
+
const NO_CLAUDE_LOGIN = "no Claude Code login found in ~/.claude/.credentials.json";
|
|
84
|
+
|
|
85
|
+
function cachedClaudeCatalog(payload: string): LiveLookup | undefined {
|
|
86
|
+
const cached = parseJson(payload);
|
|
87
|
+
if (cached === undefined || !isObject(cached)) return undefined;
|
|
88
|
+
const fetchedAt = cached["fetchedAt"];
|
|
89
|
+
const ids = cached["ids"];
|
|
90
|
+
if (typeof fetchedAt !== "string" || !Array.isArray(ids)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
if (!ids.every(isNonBlankString)) return undefined;
|
|
94
|
+
const displayNames = cached["displayNames"];
|
|
95
|
+
// Names align with ids; older cache rows contain ids only.
|
|
96
|
+
const names =
|
|
97
|
+
Array.isArray(displayNames) &&
|
|
98
|
+
displayNames.length === ids.length &&
|
|
99
|
+
displayNames.every((name) => name === null || isNonBlankString(name))
|
|
100
|
+
? displayNames
|
|
101
|
+
: undefined;
|
|
102
|
+
return {
|
|
103
|
+
source: "anthropic-api",
|
|
104
|
+
fetchedAt,
|
|
105
|
+
models: ids.map((id, index) => {
|
|
106
|
+
const name = names?.[index];
|
|
107
|
+
return typeof name === "string" ? { id, kind: "id", note: name } : { id, kind: "id" };
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function claudeCatalog(inputs: LiveCatalogInputs): Promise<Lookup> {
|
|
113
|
+
if (!inputs.refresh) {
|
|
114
|
+
const cached = readCache(inputs.home, ANTHROPIC_CACHE_KEY, ANTHROPIC_CACHE_TTL, inputs.now());
|
|
115
|
+
if (cached !== undefined) {
|
|
116
|
+
const catalog = cachedClaudeCatalog(cached);
|
|
117
|
+
if (catalog !== undefined) return catalog;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let credentialsText: string;
|
|
122
|
+
try {
|
|
123
|
+
credentialsText = readFileSync(p(inputs.home, ".claude", ".credentials.json"), "utf8");
|
|
124
|
+
} catch {
|
|
125
|
+
return { fallbackReason: NO_CLAUDE_LOGIN };
|
|
126
|
+
}
|
|
127
|
+
const credentials = parseJson(credentialsText);
|
|
128
|
+
const oauth =
|
|
129
|
+
credentials !== undefined && isObject(credentials) ? credentials["claudeAiOauth"] : undefined;
|
|
130
|
+
if (oauth === undefined || !isObject(oauth)) return { fallbackReason: NO_CLAUDE_LOGIN };
|
|
131
|
+
const token = oauth["accessToken"];
|
|
132
|
+
const expiresAt = oauth["expiresAt"];
|
|
133
|
+
if (!isNonBlankString(token) || typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) {
|
|
134
|
+
return { fallbackReason: NO_CLAUDE_LOGIN };
|
|
135
|
+
}
|
|
136
|
+
if (expiresAt <= inputs.now()) {
|
|
137
|
+
return { fallbackReason: "Claude Code login expired; start claude once to refresh it" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const models: Array<LiveModel> = [];
|
|
141
|
+
let afterId: string | undefined;
|
|
142
|
+
try {
|
|
143
|
+
for (;;) {
|
|
144
|
+
const url = `https://api.anthropic.com/v1/models?limit=1000${afterId === undefined ? "" : `&after_id=${encodeURIComponent(afterId)}`}`;
|
|
145
|
+
const response = await inputs.fetch(url, {
|
|
146
|
+
method: "GET",
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Bearer ${token}`,
|
|
149
|
+
"anthropic-version": "2023-06-01",
|
|
150
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
151
|
+
},
|
|
152
|
+
signal: AbortSignal.timeout(5000),
|
|
153
|
+
});
|
|
154
|
+
if (response.status !== 200) {
|
|
155
|
+
return { fallbackReason: `Anthropic API returned HTTP ${response.status}` };
|
|
156
|
+
}
|
|
157
|
+
const page: unknown = await response.json();
|
|
158
|
+
if (typeof page !== "object" || page === null || Array.isArray(page)) {
|
|
159
|
+
throw new Error("invalid response");
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
!("data" in page) ||
|
|
163
|
+
!Array.isArray(page.data) ||
|
|
164
|
+
!("has_more" in page) ||
|
|
165
|
+
typeof page.has_more !== "boolean"
|
|
166
|
+
) {
|
|
167
|
+
throw new Error("invalid response");
|
|
168
|
+
}
|
|
169
|
+
for (const row of page.data) {
|
|
170
|
+
if (typeof row !== "object" || row === null || Array.isArray(row) || !("id" in row))
|
|
171
|
+
continue;
|
|
172
|
+
const id = row.id;
|
|
173
|
+
if (!isNonBlankString(id)) continue;
|
|
174
|
+
const name = "display_name" in row ? row.display_name : undefined;
|
|
175
|
+
models.push(isNonBlankString(name) ? { id, kind: "id", note: name } : { id, kind: "id" });
|
|
176
|
+
}
|
|
177
|
+
if (!page.has_more) break;
|
|
178
|
+
const lastId = "last_id" in page ? page.last_id : undefined;
|
|
179
|
+
if (!isNonBlankString(lastId) || lastId === afterId) throw new Error("invalid response");
|
|
180
|
+
afterId = lastId;
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
fallbackReason: `Anthropic API unreachable: ${error instanceof Error ? error.message : String(error)}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// An empty list would drop every curated id; never cache it.
|
|
189
|
+
if (models.length === 0) return { fallbackReason: "Anthropic API returned no models" };
|
|
190
|
+
const fetched = inputs.now();
|
|
191
|
+
const fetchedAt = new Date(fetched).toISOString();
|
|
192
|
+
writeCache(
|
|
193
|
+
inputs.home,
|
|
194
|
+
ANTHROPIC_CACHE_KEY,
|
|
195
|
+
JSON.stringify({
|
|
196
|
+
fetchedAt,
|
|
197
|
+
ids: models.map((model) => model.id),
|
|
198
|
+
displayNames: models.map((model) => model.note ?? null),
|
|
199
|
+
}),
|
|
200
|
+
fetched,
|
|
201
|
+
);
|
|
202
|
+
return { source: "anthropic-api", fetchedAt, models };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const NO_CODEX_CACHE = "no Codex model cache (~/.codex/models_cache.json); run codex once";
|
|
206
|
+
const UNREADABLE_CODEX_CACHE = "Codex model cache is unreadable";
|
|
207
|
+
|
|
208
|
+
function codexCatalog(home: string): Lookup {
|
|
209
|
+
let text: string;
|
|
210
|
+
try {
|
|
211
|
+
text = readFileSync(p(home, ".codex", "models_cache.json"), "utf8");
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const missing =
|
|
214
|
+
typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
215
|
+
return { fallbackReason: missing ? NO_CODEX_CACHE : UNREADABLE_CODEX_CACHE };
|
|
216
|
+
}
|
|
217
|
+
const cache = parseJson(text);
|
|
218
|
+
if (cache === undefined || !isObject(cache) || !Array.isArray(cache["models"])) {
|
|
219
|
+
return { fallbackReason: UNREADABLE_CODEX_CACHE };
|
|
220
|
+
}
|
|
221
|
+
const models: Array<LiveModel> = [];
|
|
222
|
+
for (const row of cache["models"]) {
|
|
223
|
+
if (!isObject(row) || row["visibility"] !== "list") continue;
|
|
224
|
+
const id = row["slug"];
|
|
225
|
+
if (!isNonBlankString(id)) continue;
|
|
226
|
+
const name = row["display_name"];
|
|
227
|
+
models.push(isNonBlankString(name) ? { id, kind: "id", note: name } : { id, kind: "id" });
|
|
228
|
+
}
|
|
229
|
+
if (models.length === 0) return { fallbackReason: "Codex model cache lists no models" };
|
|
230
|
+
const fetchedAt = cache["fetched_at"];
|
|
231
|
+
return {
|
|
232
|
+
source: "codex-cache",
|
|
233
|
+
...(typeof fetchedAt === "string" ? { fetchedAt } : {}),
|
|
234
|
+
models,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const NO_OMP_CATALOG = "'omp models --json' returned no usable catalog";
|
|
239
|
+
|
|
240
|
+
async function ompCatalog(inputs: LiveCatalogInputs): Promise<Lookup> {
|
|
241
|
+
const omp = inputs.which("omp");
|
|
242
|
+
if (omp === "") return { fallbackReason: "omp not found on PATH" };
|
|
243
|
+
let raw: string;
|
|
244
|
+
try {
|
|
245
|
+
raw = await inputs.capture(omp, ["models", "--json"]);
|
|
246
|
+
} catch {
|
|
247
|
+
return { fallbackReason: NO_OMP_CATALOG };
|
|
248
|
+
}
|
|
249
|
+
const catalog = parseJson(raw);
|
|
250
|
+
if (catalog === undefined || !isObject(catalog) || !Array.isArray(catalog["models"])) {
|
|
251
|
+
return { fallbackReason: NO_OMP_CATALOG };
|
|
252
|
+
}
|
|
253
|
+
const models: Array<LiveModel> = [];
|
|
254
|
+
for (const row of catalog["models"]) {
|
|
255
|
+
if (!isObject(row) || row["kind"] !== "chat") continue;
|
|
256
|
+
const id = row["selector"];
|
|
257
|
+
if (!isNonBlankString(id)) continue;
|
|
258
|
+
const name = row["name"];
|
|
259
|
+
models.push(isNonBlankString(name) ? { id, kind: "id", note: name } : { id, kind: "id" });
|
|
260
|
+
}
|
|
261
|
+
if (models.length === 0) return { fallbackReason: NO_OMP_CATALOG };
|
|
262
|
+
return { source: "omp-cli", fetchedAt: new Date(inputs.now()).toISOString(), models };
|
|
263
|
+
}
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model-catalog helpers:
|
|
3
|
-
*
|
|
2
|
+
* Model-catalog helpers: SoT parsing, listing, and Claude/Codex model validation.
|
|
3
|
+
* Curated message strings are covered by the golden suites.
|
|
4
4
|
*/
|
|
5
5
|
import type { Ctx } from "./index";
|
|
6
6
|
import { isObject, parseJson } from "./jq";
|
|
7
7
|
import { payloadDisplayPath, payloadText } from "../payload";
|
|
8
|
-
import type { JsonObject } from "./sharedTypes";
|
|
8
|
+
import type { CatalogSource, JsonObject, ResolvedCatalog } from "./sharedTypes";
|
|
9
9
|
|
|
10
|
-
export
|
|
11
|
-
readonly id: string;
|
|
12
|
-
readonly kind: "alias" | "id";
|
|
13
|
-
readonly note?: string;
|
|
14
|
-
}
|
|
10
|
+
export type ModelEntry = ResolvedCatalog["models"][number];
|
|
15
11
|
|
|
16
12
|
export interface ModelCatalog {
|
|
17
13
|
readonly verified: string;
|
|
18
14
|
readonly models: ReadonlyArray<ModelEntry>;
|
|
19
15
|
}
|
|
20
16
|
|
|
17
|
+
const LIVE_SOURCE_LABELS: Record<Exclude<CatalogSource, "curated">, string> = {
|
|
18
|
+
"anthropic-api": "Anthropic API via Claude Code login",
|
|
19
|
+
"codex-cache": "~/.codex/models_cache.json",
|
|
20
|
+
"omp-cli": "omp models --json",
|
|
21
|
+
};
|
|
22
|
+
|
|
21
23
|
function toolEntry(tool: string): JsonObject | undefined {
|
|
22
24
|
const doc = parseJson(payloadText("SoT/models.json"));
|
|
23
25
|
if (doc === undefined || !isObject(doc)) return undefined;
|
|
@@ -49,43 +51,47 @@ export function modelCatalog(tool: string): ModelCatalog {
|
|
|
49
51
|
return { verified: typeof verified === "string" ? verified : "?", models };
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
export function printModels(ctx: Ctx,
|
|
54
|
+
export function printModels(ctx: Ctx, catalog: ResolvedCatalog): void {
|
|
53
55
|
const { echo, warn } = ctx.services.logger;
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
+
const { tool, models, source, verified, fetchedAt } = catalog;
|
|
57
|
+
if (source === "curated" && models.length === 0) {
|
|
56
58
|
warn(`Model catalog unavailable (${payloadDisplayPath("SoT/models.json")})`);
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
const header =
|
|
62
|
+
source === "curated"
|
|
63
|
+
? `Available ${tool} models (kit-verified ${verified} — SoT/models.json):`
|
|
64
|
+
: `Available ${tool} models (live — ${LIVE_SOURCE_LABELS[source]}, fetched ${fetchedAt ?? "?"}; aliases and notes from SoT/models.json):`;
|
|
65
|
+
echo(header);
|
|
66
|
+
for (const model of models) {
|
|
67
|
+
echo(` ${model.id}${model.note === undefined ? "" : ` — ${model.note}`}`);
|
|
64
68
|
}
|
|
65
69
|
if (tool === "claude")
|
|
66
|
-
|
|
67
|
-
if (tool === "codex")
|
|
68
|
-
lines.push(" (well-formed IDs outside the catalog are accepted with a warning)");
|
|
69
|
-
for (const line of lines) echo(line);
|
|
70
|
+
echo(" (full claude-* model IDs outside the catalog are accepted with a warning)");
|
|
71
|
+
if (tool === "codex") echo(" (well-formed IDs outside the catalog are accepted with a warning)");
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
export function validateClaudeModel(ctx: Ctx, m: string): boolean {
|
|
74
|
+
export function validateClaudeModel(ctx: Ctx, m: string, catalog: ResolvedCatalog): boolean {
|
|
73
75
|
if (m === "") return false;
|
|
74
|
-
if (
|
|
76
|
+
if (catalog.models.some((entry) => entry.id === m)) return true;
|
|
75
77
|
if (m.startsWith("claude-")) {
|
|
76
78
|
ctx.services.logger.warn(
|
|
77
|
-
|
|
79
|
+
catalog.source === "curated"
|
|
80
|
+
? `Claude model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway`
|
|
81
|
+
: `Claude model '${m}' is not in the live Anthropic model list — applying anyway`,
|
|
78
82
|
);
|
|
79
83
|
return true;
|
|
80
84
|
}
|
|
81
85
|
return false;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
export function validateCodexModel(ctx: Ctx, m: string): boolean {
|
|
88
|
+
export function validateCodexModel(ctx: Ctx, m: string, catalog: ResolvedCatalog): boolean {
|
|
85
89
|
if (!/^[A-Za-z0-9._-]+$/.test(m)) return false;
|
|
86
|
-
if (!
|
|
90
|
+
if (!catalog.models.some((entry) => entry.id === m)) {
|
|
87
91
|
ctx.services.logger.warn(
|
|
88
|
-
|
|
92
|
+
catalog.source === "curated"
|
|
93
|
+
? `Codex model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway (check ~/.codex/config.toml if Codex rejects it)`
|
|
94
|
+
: `Codex model '${m}' is not in the live Codex model list — applying anyway (check ~/.codex/config.toml if Codex rejects it)`,
|
|
89
95
|
);
|
|
90
96
|
}
|
|
91
97
|
return true;
|
|
@@ -11,12 +11,14 @@ import { syncCodexModel } from "./codexToml";
|
|
|
11
11
|
import type { Ctx } from "./index";
|
|
12
12
|
import { isObject, parseJson, type Json } from "./jq";
|
|
13
13
|
import { printModels, validateClaudeModel, validateCodexModel } from "./models";
|
|
14
|
+
import { defaultLiveInputs, resolveCatalog } from "./liveModels";
|
|
14
15
|
import { bunBootstrap } from "./bun";
|
|
15
|
-
import { installedVersion, present, report } from "./toolchain";
|
|
16
|
+
import { installedVersion, outdatedReport, present, report } from "./toolchain";
|
|
17
|
+
import { topLevelTomlString } from "../manifests";
|
|
16
18
|
|
|
17
|
-
export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
19
|
+
export async function modeModel(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
18
20
|
const { echo, err, warn } = ctx.services.logger;
|
|
19
|
-
let tool = "";
|
|
21
|
+
let tool: "claude" | "codex" | "" = "";
|
|
20
22
|
let value = "";
|
|
21
23
|
for (const arg of args) {
|
|
22
24
|
if (arg === "--dry-run") ctx.dryRun = true;
|
|
@@ -32,7 +34,7 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
32
34
|
err("Usage: model <claude|codex> [value] [--dry-run]");
|
|
33
35
|
return 2;
|
|
34
36
|
}
|
|
35
|
-
|
|
37
|
+
const catalog = await resolveCatalog(tool, defaultLiveInputs(ctx.home));
|
|
36
38
|
if (value === "") {
|
|
37
39
|
if (tool === "claude") {
|
|
38
40
|
const deployed = p(ctx.home, ".claude", "settings.json");
|
|
@@ -58,23 +60,23 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
58
60
|
err(`Failed to read ~/.codex/config.toml: ${String(result.error)}`);
|
|
59
61
|
return 1;
|
|
60
62
|
}
|
|
61
|
-
echo(`deployed: ${
|
|
62
|
-
echo(`SoT: ${
|
|
63
|
+
echo(`deployed: ${topLevelTomlString(result.data, "model") ?? ""}`);
|
|
64
|
+
echo(`SoT: ${topLevelTomlString(payloadText("SoT/.codex/config.toml"), "model") ?? ""}`);
|
|
63
65
|
}
|
|
64
|
-
printModels(ctx,
|
|
66
|
+
printModels(ctx, catalog);
|
|
65
67
|
return 0;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
if (tool === "claude") {
|
|
69
|
-
if (!validateClaudeModel(ctx, value)) {
|
|
70
|
-
printModels(ctx,
|
|
71
|
+
if (!validateClaudeModel(ctx, value, catalog)) {
|
|
72
|
+
printModels(ctx, catalog);
|
|
71
73
|
err(`Invalid Claude model '${value}'`);
|
|
72
74
|
return 2;
|
|
73
75
|
}
|
|
74
76
|
syncClaudeModel(ctx, value);
|
|
75
77
|
} else {
|
|
76
|
-
if (!validateCodexModel(ctx, value)) {
|
|
77
|
-
printModels(ctx,
|
|
78
|
+
if (!validateCodexModel(ctx, value, catalog)) {
|
|
79
|
+
printModels(ctx, catalog);
|
|
78
80
|
err(`Invalid Codex model '${value}'`);
|
|
79
81
|
return 2;
|
|
80
82
|
}
|
|
@@ -111,15 +113,6 @@ function jsonModelText(text: string): string {
|
|
|
111
113
|
return typeof v === "string" ? v : JSON.stringify(v);
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
/** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
|
|
115
|
-
|
|
116
|
-
function tomlModelText(text: string): string {
|
|
117
|
-
for (const line of text.split("\n")) {
|
|
118
|
-
if (/^model[ \t]*=/.test(line)) return line.split('"')[1] ?? "";
|
|
119
|
-
}
|
|
120
|
-
return "";
|
|
121
|
-
}
|
|
122
|
-
|
|
123
116
|
export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
124
117
|
const { echo, err, verbose } = ctx.services.logger;
|
|
125
118
|
const words = args.filter((arg) => !arg.startsWith("--"));
|
|
@@ -128,13 +121,21 @@ export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Prom
|
|
|
128
121
|
for (const arg of args) {
|
|
129
122
|
if (arg === "--verbose") ctx.verbose = true;
|
|
130
123
|
}
|
|
124
|
+
if (words.length > (op === "ensure" ? 2 : 1)) {
|
|
125
|
+
err("Usage: toolchain [check|ensure <tool>|outdated [--refresh]]");
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
131
128
|
|
|
132
129
|
if (op === "check") {
|
|
133
130
|
await report(ctx);
|
|
134
131
|
return 0;
|
|
135
132
|
}
|
|
133
|
+
if (op === "outdated") {
|
|
134
|
+
await outdatedReport(ctx, { refresh: args.includes("--refresh") });
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
136
137
|
if (op !== "ensure") {
|
|
137
|
-
err("Usage: toolchain [check|ensure <tool
|
|
138
|
+
err("Usage: toolchain [check|ensure <tool>|outdated [--refresh]]");
|
|
138
139
|
return 2;
|
|
139
140
|
}
|
|
140
141
|
if (tool === "") {
|
|
@@ -8,6 +8,7 @@ import type { Ctx } from "./index";
|
|
|
8
8
|
import { LEGACY_SELECTION, readHarnessSelection, type Harness } from "./harnesses";
|
|
9
9
|
import { advisorCatalog, advisorFlagGrammar, effortCatalog, effortFlagGrammar } from "../efforts";
|
|
10
10
|
import { printModels } from "./models";
|
|
11
|
+
import { curatedCatalog } from "./liveModels";
|
|
11
12
|
import { ExitError, printCatalog, printUsage } from "./parseHelp";
|
|
12
13
|
import {
|
|
13
14
|
KNOWN_CLAUDE_OPTIN_PLUGINS,
|
|
@@ -93,11 +94,11 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
93
94
|
ctx.verbose = true;
|
|
94
95
|
continue;
|
|
95
96
|
case "--claude-model":
|
|
96
|
-
printModels(ctx, "claude");
|
|
97
|
+
printModels(ctx, curatedCatalog("claude"));
|
|
97
98
|
err("--claude-model requires a value: --claude-model=<model>");
|
|
98
99
|
throw new ExitError(2);
|
|
99
100
|
case "--codex-model":
|
|
100
|
-
printModels(ctx, "codex");
|
|
101
|
+
printModels(ctx, curatedCatalog("codex"));
|
|
101
102
|
err("--codex-model requires a value: --codex-model=<model>");
|
|
102
103
|
throw new ExitError(2);
|
|
103
104
|
case "--claude-effort":
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
isEffortModifierValue,
|
|
18
18
|
} from "../efforts";
|
|
19
19
|
import { printModels, validateClaudeModel, validateCodexModel } from "./models";
|
|
20
|
+
import { defaultLiveInputs, resolveCatalog } from "./liveModels";
|
|
20
21
|
import { ExitError, printCatalog } from "./parseHelp";
|
|
21
22
|
|
|
22
23
|
export const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"];
|
|
@@ -169,7 +170,7 @@ export function isScalarModifierFlag(value: string): value is ScalarModifierFlag
|
|
|
169
170
|
return SCALAR_MODIFIER_FLAGS[value as ScalarModifierFlag] === true;
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
export function validateModifierFlags(ctx: Ctx): void {
|
|
173
|
+
export async function validateModifierFlags(ctx: Ctx): Promise<void> {
|
|
173
174
|
const { err, warn } = ctx.services.logger;
|
|
174
175
|
const supplied = (flag: ModifierFlag): boolean =>
|
|
175
176
|
MODIFIER_METADATA[flag].hasValue(ctx) || ctx.modifierFlags?.has(flag) === true;
|
|
@@ -185,8 +186,9 @@ export function validateModifierFlags(ctx: Ctx): void {
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
if (supplied("--claude-model")) {
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
const catalog = await resolveCatalog("claude", defaultLiveInputs(ctx.home));
|
|
190
|
+
if (!validateClaudeModel(ctx, ctx.claudeModel, catalog)) {
|
|
191
|
+
printModels(ctx, catalog);
|
|
190
192
|
err(`Invalid Claude model '${ctx.claudeModel}' — use an alias above or a full claude-* ID`);
|
|
191
193
|
throw new ExitError(2);
|
|
192
194
|
}
|
|
@@ -206,8 +208,9 @@ export function validateModifierFlags(ctx: Ctx): void {
|
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
210
|
if (supplied("--codex-model")) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
const catalog = await resolveCatalog("codex", defaultLiveInputs(ctx.home));
|
|
212
|
+
if (!validateCodexModel(ctx, ctx.codexModel, catalog)) {
|
|
213
|
+
printModels(ctx, catalog);
|
|
211
214
|
err(`Invalid Codex model '${ctx.codexModel}' — must match ^[A-Za-z0-9._-]+$`);
|
|
212
215
|
throw new ExitError(2);
|
|
213
216
|
}
|
|
@@ -9,6 +9,22 @@ import type { ModifierFlag } from "./engineCtx";
|
|
|
9
9
|
/** Manifest-record object: the `{ [key]: Json }` shape redeclared across manifest readers. */
|
|
10
10
|
export type JsonObject = { [key: string]: Json };
|
|
11
11
|
|
|
12
|
+
export type CatalogTool = "claude" | "codex" | "omp";
|
|
13
|
+
export type CatalogSource = "anthropic-api" | "codex-cache" | "omp-cli" | "curated";
|
|
14
|
+
|
|
15
|
+
export interface ResolvedCatalog {
|
|
16
|
+
readonly tool: CatalogTool;
|
|
17
|
+
readonly source: CatalogSource;
|
|
18
|
+
readonly verified: string;
|
|
19
|
+
readonly fetchedAt?: string;
|
|
20
|
+
readonly fallbackReason?: string;
|
|
21
|
+
readonly models: ReadonlyArray<{
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly kind: "alias" | "id";
|
|
24
|
+
readonly note?: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
/**
|
|
13
29
|
* Shared core of per-tool settings-edit descriptors (Claude JSON modifiers,
|
|
14
30
|
* Codex TOML passes). Each tool narrows `key` to its own vocabulary and keeps
|
|
@@ -44,3 +60,6 @@ export type ScalarModifierFlag = Exclude<
|
|
|
44
60
|
ModifierFlag,
|
|
45
61
|
"--claude-compact-window" | "--claude-permissive" | "--claude-plugin"
|
|
46
62
|
>;
|
|
63
|
+
|
|
64
|
+
/** Injected HTTP client: the subset of fetch the network lookups call. */
|
|
65
|
+
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
export async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
22
22
|
const { acquireTerminal, echo, err } = ctx.services.logger;
|
|
23
23
|
parseArgs(ctx, args);
|
|
24
|
-
validateModifierFlags(ctx);
|
|
24
|
+
await validateModifierFlags(ctx);
|
|
25
25
|
const configuredConcurrency = process.env["DOCKS_KIT_SYNC_CONCURRENCY"];
|
|
26
26
|
if (configuredConcurrency === undefined || configuredConcurrency === "") {
|
|
27
27
|
ctx.syncConcurrency = 3;
|