clef-mcp 0.3.1 → 0.4.0

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/dist/cli.js CHANGED
@@ -104,9 +104,9 @@ __export(setup_exports, {
104
104
  });
105
105
  import { execFile as execFile5 } from "child_process";
106
106
  import fsSync4 from "fs";
107
- import fsp10 from "fs/promises";
108
- import os4 from "os";
109
- import path11 from "path";
107
+ import fsp13 from "fs/promises";
108
+ import os5 from "os";
109
+ import path12 from "path";
110
110
  import { fileURLToPath as fileURLToPath2 } from "url";
111
111
  function bundledSkillPath() {
112
112
  return fileURLToPath2(new URL("../skills/clef-decisions/SKILL.md", import.meta.url));
@@ -114,7 +114,7 @@ function bundledSkillPath() {
114
114
  async function mergeJsonServerEntry(file, serverKey, container) {
115
115
  let doc = {};
116
116
  try {
117
- doc = JSON.parse(await fsp10.readFile(file, "utf8"));
117
+ doc = JSON.parse(await fsp13.readFile(file, "utf8"));
118
118
  } catch {
119
119
  doc = {};
120
120
  }
@@ -131,15 +131,15 @@ async function mergeJsonServerEntry(file, serverKey, container) {
131
131
  servers[serverKey] = { command: SERVER_ENTRY.command, args: [...SERVER_ENTRY.args] };
132
132
  doc.mcpServers = servers;
133
133
  }
134
- await fsp10.mkdir(path11.dirname(file), { recursive: true });
135
- await fsp10.writeFile(file, JSON.stringify(doc, null, 2) + "\n", "utf8");
134
+ await fsp13.mkdir(path12.dirname(file), { recursive: true });
135
+ await fsp13.writeFile(file, JSON.stringify(doc, null, 2) + "\n", "utf8");
136
136
  return "configured";
137
137
  }
138
138
  async function setupCodex() {
139
- const file = path11.join(HOME(), ".codex", "config.toml");
139
+ const file = path12.join(HOME(), ".codex", "config.toml");
140
140
  let content = "";
141
141
  try {
142
- content = await fsp10.readFile(file, "utf8");
142
+ content = await fsp13.readFile(file, "utf8");
143
143
  } catch {
144
144
  content = "";
145
145
  }
@@ -147,24 +147,24 @@ async function setupCodex() {
147
147
  return { client: "codex", status: "already-configured", detail: file };
148
148
  }
149
149
  const next = (content && !content.endsWith("\n") ? content + "\n" : content) + "\n" + CODEX_SECTION;
150
- await fsp10.mkdir(path11.dirname(file), { recursive: true });
151
- await fsp10.writeFile(file, next, "utf8");
150
+ await fsp13.mkdir(path12.dirname(file), { recursive: true });
151
+ await fsp13.writeFile(file, next, "utf8");
152
152
  return { client: "codex", status: "configured", detail: file };
153
153
  }
154
154
  async function setupZcode() {
155
- const file = path11.join(HOME(), ".zcode", "cli", "config.json");
155
+ const file = path12.join(HOME(), ".zcode", "cli", "config.json");
156
156
  const status = await mergeJsonServerEntry(file, "clef-mcp", "mcp.servers");
157
157
  return { client: "zcode", status, detail: file };
158
158
  }
159
159
  async function setupCursor() {
160
- const file = path11.join(HOME(), ".cursor", "mcp.json");
160
+ const file = path12.join(HOME(), ".cursor", "mcp.json");
161
161
  const status = await mergeJsonServerEntry(file, "clef-mcp", "mcpServers");
162
162
  return { client: "cursor", status, detail: file };
163
163
  }
164
164
  function whichCli(bin) {
165
- const dirs = (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean);
165
+ const dirs = (process.env.PATH ?? "").split(path12.delimiter).filter(Boolean);
166
166
  for (const dir of dirs) {
167
- const candidate = path11.join(dir, bin);
167
+ const candidate = path12.join(dir, bin);
168
168
  try {
169
169
  fsSync4.accessSync(candidate, fsSync4.constants.X_OK);
170
170
  return candidate;
@@ -200,19 +200,19 @@ async function setupClaudeCode() {
200
200
  }
201
201
  async function installSkill() {
202
202
  const src = bundledSkillPath();
203
- const destDir = path11.join(HOME(), ".agents", "skills", "clef-decisions");
204
- const dest = path11.join(destDir, "SKILL.md");
203
+ const destDir = path12.join(HOME(), ".agents", "skills", "clef-decisions");
204
+ const dest = path12.join(destDir, "SKILL.md");
205
205
  let content = "";
206
206
  try {
207
- content = await fsp10.readFile(dest, "utf8");
207
+ content = await fsp13.readFile(dest, "utf8");
208
208
  } catch {
209
209
  }
210
- const incoming = await fsp10.readFile(src, "utf8");
210
+ const incoming = await fsp13.readFile(src, "utf8");
211
211
  if (content === incoming) {
212
212
  return { client: "skill", status: "already-configured", detail: dest };
213
213
  }
214
- await fsp10.mkdir(destDir, { recursive: true });
215
- await fsp10.writeFile(dest, incoming, "utf8");
214
+ await fsp13.mkdir(destDir, { recursive: true });
215
+ await fsp13.writeFile(dest, incoming, "utf8");
216
216
  return { client: "skill", status: "configured", detail: dest };
217
217
  }
218
218
  function parseClients(raw) {
@@ -281,7 +281,7 @@ var init_setup = __esm({
281
281
  init_errors();
282
282
  init_prompt();
283
283
  SERVER_ENTRY = { command: "clef-mcp", args: [], type: "stdio" };
284
- HOME = () => os4.homedir();
284
+ HOME = () => os5.homedir();
285
285
  CODEX_SECTION = [
286
286
  "[mcp_servers.clef-mcp]",
287
287
  'command = "clef-mcp"',
@@ -492,6 +492,15 @@ var DEFAULT_LIMITS = {
492
492
  maxCriteriaDescChars: 1e3,
493
493
  maxQuestionIdChars: 128
494
494
  };
495
+ var CLASSIFY_LIMITS = {
496
+ maxProfileBytes: 1024 * 1024,
497
+ maxRecordBytes: 1024 * 1024,
498
+ maxResultBytes: 1024 * 1024,
499
+ maxExamples: 20,
500
+ maxFields: 1e3,
501
+ maxDepth: 64,
502
+ maxMetadataBytes: 16 * 1024
503
+ };
495
504
  function intFromEnv(env, key, fallback) {
496
505
  const raw = env[key];
497
506
  if (!raw?.trim()) return fallback;
@@ -947,21 +956,21 @@ function whichBin(name, platform) {
947
956
  }
948
957
  return void 0;
949
958
  }
950
- function managedBinary(clefHome, platform, arch, exists) {
959
+ function managedBinary(clefHome, platform, arch, exists2) {
951
960
  const platformDir = llamaRuntimeDir(pathsFor(clefHome).runtimeDir, platform, arch);
952
961
  const manifest = readRuntimeManifest(path5.join(platformDir, "runtime-manifest.json"));
953
- if (manifest && exists(manifest.binaryPath)) return manifest.binaryPath;
962
+ if (manifest && exists2(manifest.binaryPath)) return manifest.binaryPath;
954
963
  const legacy = path5.join(platformDir, llamaServerName(platform));
955
- if (exists(legacy)) return legacy;
964
+ if (exists2(legacy)) return legacy;
956
965
  return void 0;
957
966
  }
958
967
  function resolveLlamaServerBinary(cfg, deps = {}) {
959
- const exists = deps.existsExecutable ?? existsExecutable;
968
+ const exists2 = deps.existsExecutable ?? existsExecutable;
960
969
  const which = deps.which ?? ((name) => whichBin(name, deps.platform ?? process.platform));
961
970
  const platform = deps.platform ?? process.platform;
962
971
  const arch = deps.arch ?? process.arch;
963
972
  if (cfg.llamaBin) {
964
- if (!exists(cfg.llamaBin)) {
973
+ if (!exists2(cfg.llamaBin)) {
965
974
  throw new ClefError(
966
975
  ClefErrorCode.RUNTIME_NOT_FOUND,
967
976
  `CLEF_LLAMA_BIN points to a missing or non-executable file: ${cfg.llamaBin}`
@@ -969,7 +978,7 @@ function resolveLlamaServerBinary(cfg, deps = {}) {
969
978
  }
970
979
  return { source: "env", path: cfg.llamaBin };
971
980
  }
972
- const managed = managedBinary(cfg.clefHome, platform, arch, exists);
981
+ const managed = managedBinary(cfg.clefHome, platform, arch, exists2);
973
982
  if (managed) return { source: "managed", path: managed };
974
983
  const onPath = which("llama-server");
975
984
  if (onPath) return { source: "path", path: onPath };
@@ -2350,18 +2359,603 @@ function validateDoc(config, doc) {
2350
2359
  return parsed;
2351
2360
  }
2352
2361
 
2362
+ // src/cli/classify.ts
2363
+ init_errors();
2364
+
2365
+ // src/classify/input.ts
2366
+ init_errors();
2367
+ import { createHash as createHash3 } from "crypto";
2368
+ import fsp8 from "fs/promises";
2369
+ import { Readable } from "stream";
2370
+ import { parse } from "csv-parse";
2371
+
2372
+ // src/classify/profile.ts
2373
+ init_errors();
2374
+ import { createHash as createHash2 } from "crypto";
2375
+ import fsp7 from "fs/promises";
2376
+ import { parseAllDocuments } from "yaml";
2377
+ import { z as z3 } from "zod";
2378
+ function invalid2(message) {
2379
+ return new ClefError(ClefErrorCode.INVALID_INPUT, message);
2380
+ }
2381
+ var safeId = z3.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/);
2382
+ var threshold = z3.number().finite().min(0).max(1);
2383
+ var reserved = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype", "<<"]);
2384
+ function validateData(value, depth = 0) {
2385
+ if (depth > CLASSIFY_LIMITS.maxDepth) throw invalid2("Profile nesting exceeds the limit");
2386
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
2387
+ if (typeof value === "number" && Number.isFinite(value)) return;
2388
+ if (typeof value !== "object" || value instanceof Date || value instanceof Map || value instanceof Set) throw invalid2("Profile must contain JSON-compatible data");
2389
+ for (const [key, child] of Object.entries(value)) {
2390
+ if (reserved.has(key)) throw invalid2(`Reserved profile key "${key}"`);
2391
+ validateData(child, depth + 1);
2392
+ }
2393
+ }
2394
+ function canonicalJson(value) {
2395
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
2396
+ if (value !== null && typeof value === "object") {
2397
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
2398
+ }
2399
+ return JSON.stringify(value);
2400
+ }
2401
+ function profileSchema(limits) {
2402
+ return z3.strictObject({
2403
+ schema_version: z3.literal(1),
2404
+ name: safeId.max(64),
2405
+ instructions: z3.string().trim().min(1).max(limits.maxInstructionsChars),
2406
+ labels: z3.record(safeId, z3.string().trim().min(1).max(limits.maxCriteriaDescChars)).refine((v) => Object.keys(v).length >= 2 && Object.keys(v).length <= Math.min(100, limits.maxCriteriaPerQuestion), "Expected 2\u2013100 labels within the configured criteria limit"),
2407
+ model: z3.string().trim().min(1).optional(),
2408
+ input: z3.strictObject({
2409
+ id_field: z3.string().min(1).optional(),
2410
+ fields: z3.array(z3.string().min(1)).min(1).max(CLASSIFY_LIMITS.maxFields).refine((v) => new Set(v).size === v.length, "Selected fields must be unique").optional()
2411
+ }).optional(),
2412
+ policy: z3.strictObject({
2413
+ min_probability: threshold.default(0.8),
2414
+ min_margin: threshold.default(0.15),
2415
+ min_confidence: threshold.optional()
2416
+ }).default({ min_probability: 0.8, min_margin: 0.15 }),
2417
+ examples: z3.array(z3.strictObject({ input: z3.record(z3.string(), z3.unknown()), label: safeId })).max(CLASSIFY_LIMITS.maxExamples).optional()
2418
+ }).superRefine((p, ctx) => {
2419
+ for (const example of p.examples ?? []) {
2420
+ if (!Object.hasOwn(p.labels, example.label)) ctx.addIssue({ code: "custom", message: `Example label "${example.label}" is not configured` });
2421
+ }
2422
+ });
2423
+ }
2424
+ function parseProfile(text, limits) {
2425
+ if (Buffer.byteLength(text) > CLASSIFY_LIMITS.maxProfileBytes) throw invalid2("Profile exceeds the maximum size in bytes");
2426
+ try {
2427
+ const docs = parseAllDocuments(text, { version: "1.2", schema: "core", uniqueKeys: true, stringKeys: true, strict: true, resolveKnownTags: false, customTags: [], merge: false, logLevel: "silent" });
2428
+ if (docs.length !== 1) throw invalid2("Profile must contain exactly one YAML document");
2429
+ const doc = docs[0];
2430
+ if (doc.errors.length || doc.warnings.length) throw invalid2(`Invalid YAML profile: ${(doc.errors[0] ?? doc.warnings[0]).message}`);
2431
+ const data = doc.toJS({ maxAliasCount: 0 });
2432
+ validateData(data);
2433
+ return profileSchema(limits).parse(JSON.parse(canonicalJson(data)));
2434
+ } catch (err) {
2435
+ if (err instanceof ClefError) throw err;
2436
+ throw invalid2(`Invalid profile: ${err instanceof Error ? err.message : String(err)}`);
2437
+ }
2438
+ }
2439
+ async function loadProfile(file, limits) {
2440
+ const handle = await fsp7.open(file, "r");
2441
+ try {
2442
+ const stat = await handle.stat();
2443
+ if (!stat.isFile() || stat.size > CLASSIFY_LIMITS.maxProfileBytes) throw invalid2("Profile must be a regular file within the size limit");
2444
+ const chunks = [];
2445
+ let size = 0;
2446
+ const stream = handle.createReadStream({ autoClose: false });
2447
+ for await (const chunk of stream) {
2448
+ size += chunk.length;
2449
+ if (size > CLASSIFY_LIMITS.maxProfileBytes) throw invalid2("Profile exceeds the maximum size in bytes");
2450
+ chunks.push(chunk);
2451
+ }
2452
+ return parseProfile(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)), limits);
2453
+ } finally {
2454
+ await handle.close();
2455
+ }
2456
+ }
2457
+ function selectRecord(profile, record) {
2458
+ if (profile.input?.id_field) {
2459
+ const id = record[profile.input.id_field];
2460
+ if (!Object.hasOwn(record, profile.input.id_field) || !(typeof id === "string" || typeof id === "number" && Number.isFinite(id))) throw invalid2(`Missing or invalid id field "${profile.input.id_field}"`);
2461
+ }
2462
+ if (!profile.input?.fields) return record;
2463
+ return Object.fromEntries(profile.input.fields.map((field) => {
2464
+ if (!Object.hasOwn(record, field)) throw invalid2(`Missing selected field "${field}"`);
2465
+ return [field, record[field]];
2466
+ }));
2467
+ }
2468
+ function profileState(profile, record) {
2469
+ return { record, ...profile.examples?.length ? { examples: profile.examples } : {} };
2470
+ }
2471
+ function profileDigest(profile) {
2472
+ return createHash2("sha256").update(canonicalJson(profile)).digest("hex");
2473
+ }
2474
+
2475
+ // src/classify/input.ts
2476
+ async function* decoded(source) {
2477
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
2478
+ try {
2479
+ for await (const chunk of source) yield decoder.decode(typeof chunk === "string" ? Buffer.from(chunk) : chunk, { stream: true });
2480
+ const tail = decoder.decode();
2481
+ if (tail) yield tail;
2482
+ } catch (err) {
2483
+ throw invalid2(`Cannot read UTF-8 input: ${err instanceof Error ? err.message : String(err)}`);
2484
+ }
2485
+ }
2486
+ async function* boundedLines(source, maxBytes, discardUnterminated = false) {
2487
+ let pieces = [], size = 0, oversized = false, first = true;
2488
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
2489
+ const line = (terminated) => {
2490
+ let text = "";
2491
+ if (!oversized && (terminated || !discardUnterminated)) {
2492
+ try {
2493
+ text = decoder.decode(Buffer.concat(pieces, size));
2494
+ } catch {
2495
+ throw invalid2("Cannot read UTF-8 input: invalid encoded record");
2496
+ }
2497
+ if (first) text = text.replace(/^\uFEFF/, "");
2498
+ text = text.replace(/\r$/, "");
2499
+ }
2500
+ first = false;
2501
+ return { text, oversized, terminated, bytes: size + (terminated ? 1 : 0) };
2502
+ };
2503
+ for await (const raw of source) {
2504
+ const chunk = typeof raw === "string" ? Buffer.from(raw) : raw;
2505
+ let start = 0;
2506
+ for (; ; ) {
2507
+ const end = chunk.indexOf(10, start);
2508
+ const part = chunk.subarray(start, end < 0 ? void 0 : end);
2509
+ size += part.length;
2510
+ if (!oversized) {
2511
+ if (size > maxBytes) {
2512
+ oversized = true;
2513
+ pieces = [];
2514
+ } else if (part.length) pieces.push(part);
2515
+ }
2516
+ if (end < 0) break;
2517
+ yield line(true);
2518
+ pieces = [];
2519
+ size = 0;
2520
+ oversized = false;
2521
+ start = end + 1;
2522
+ }
2523
+ }
2524
+ if (size) yield line(false);
2525
+ }
2526
+ async function* readRows(source, format) {
2527
+ let row = 0;
2528
+ if (format === "jsonl") {
2529
+ for await (const line of boundedLines(source, CLASSIFY_LIMITS.maxRecordBytes)) {
2530
+ if (!line.oversized && !line.text.trim()) continue;
2531
+ row++;
2532
+ try {
2533
+ if (line.oversized) throw invalid2("Record exceeds the maximum size");
2534
+ const record = JSON.parse(line.text);
2535
+ if (record === null || typeof record !== "object" || Array.isArray(record)) throw invalid2("JSONL records must be JSON objects");
2536
+ yield { row, record };
2537
+ } catch (err) {
2538
+ yield { row, error: errorJson(invalid2(`Row ${row}: ${err instanceof Error ? err.message : String(err)}`)).error };
2539
+ }
2540
+ }
2541
+ return;
2542
+ }
2543
+ const parser = parse({
2544
+ bom: true,
2545
+ cast: false,
2546
+ skip_empty_lines: true,
2547
+ relax_column_count: false,
2548
+ max_record_size: CLASSIFY_LIMITS.maxRecordBytes,
2549
+ columns: (headers) => {
2550
+ if (headers.length > CLASSIFY_LIMITS.maxFields || headers.some((h) => !h.trim()) || new Set(headers).size !== headers.length) throw invalid2("CSV headers must be nonempty, unique and within the field limit");
2551
+ if (headers.some((h) => ["__proto__", "constructor", "prototype"].includes(h))) throw invalid2("Reserved CSV header");
2552
+ return headers;
2553
+ }
2554
+ });
2555
+ const upstream = Readable.from(decoded(source));
2556
+ upstream.on("error", (err) => parser.destroy(err));
2557
+ upstream.pipe(parser);
2558
+ try {
2559
+ for await (const record of parser) yield { row: ++row, record };
2560
+ } catch (err) {
2561
+ throw invalid2(`Invalid CSV: ${err instanceof Error ? err.message : String(err)}`);
2562
+ } finally {
2563
+ upstream.destroy();
2564
+ parser.destroy();
2565
+ }
2566
+ }
2567
+ async function* fileChunks(handle) {
2568
+ let position = 0;
2569
+ for (; ; ) {
2570
+ const buffer = Buffer.alloc(64 * 1024);
2571
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
2572
+ if (!bytesRead) return;
2573
+ position += bytesRead;
2574
+ yield buffer.subarray(0, bytesRead);
2575
+ }
2576
+ }
2577
+ function sameFile(a, b) {
2578
+ return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
2579
+ }
2580
+ async function openInput(file, requestedFormat, _limits) {
2581
+ const format = requestedFormat ?? (file === "-" ? "jsonl" : file.toLowerCase().endsWith(".jsonl") ? "jsonl" : file.toLowerCase().endsWith(".csv") ? "csv" : void 0);
2582
+ if (format !== "jsonl" && format !== "csv") throw invalid2("Use --format jsonl|csv for this input");
2583
+ if (file === "-") return { format, records: () => readRows(process.stdin.iterator({ destroyOnReturn: false }), format), verifyUnchanged: async () => {
2584
+ }, close: async () => {
2585
+ } };
2586
+ const handle = await fsp8.open(file, "r");
2587
+ try {
2588
+ const initial = await handle.stat();
2589
+ if (!initial.isFile()) throw invalid2("Input must be a regular file or stdin");
2590
+ const verifyUnchanged = async () => {
2591
+ let currentPath;
2592
+ try {
2593
+ currentPath = await fsp8.stat(file);
2594
+ } catch {
2595
+ throw invalid2("Input changed during classification");
2596
+ }
2597
+ if (!sameFile(initial, await handle.stat()) || !sameFile(initial, currentPath)) throw invalid2("Input changed during classification");
2598
+ };
2599
+ const hash = createHash3("sha256");
2600
+ for await (const chunk of fileChunks(handle)) hash.update(chunk);
2601
+ await verifyUnchanged();
2602
+ return { format, fingerprint: hash.digest("hex"), records: () => readRows(fileChunks(handle), format), verifyUnchanged, close: () => handle.close() };
2603
+ } catch (err) {
2604
+ await handle.close();
2605
+ throw err;
2606
+ }
2607
+ }
2608
+
2609
+ // src/classify/output.ts
2610
+ init_errors();
2611
+ import { randomUUID } from "crypto";
2612
+ import { constants } from "fs";
2613
+ import fsp9 from "fs/promises";
2614
+ import os2 from "os";
2615
+ import path8 from "path";
2616
+ import { z as z4 } from "zod";
2617
+ var baseShape = { row: z4.number().int().positive(), id: z4.string().optional(), profile: z4.string().min(1), model: z4.string().min(1) };
2618
+ var policyShape = {
2619
+ label: z4.string().nullable(),
2620
+ suggested_label: z4.string(),
2621
+ probability: z4.number().min(0).max(1),
2622
+ margin: z4.number().min(0).max(1),
2623
+ probabilities: z4.record(z4.string(), z4.number().min(0).max(1)),
2624
+ confidence: z4.number().min(0).max(1).optional(),
2625
+ review_reasons: z4.array(z4.enum(["low_probability", "low_margin", "tie", "low_confidence", "missing_confidence"])),
2626
+ usage: z4.strictObject({ input_tokens: z4.number().optional(), output_tokens: z4.number().optional(), latency_ms: z4.number().optional() }).optional()
2627
+ };
2628
+ var resultSchema = z4.discriminatedUnion("status", [
2629
+ z4.strictObject({ ...baseShape, ...policyShape, status: z4.literal("classified") }),
2630
+ z4.strictObject({ ...baseShape, ...policyShape, status: z4.literal("needs_review") }),
2631
+ z4.strictObject({ ...baseShape, status: z4.literal("error"), label: z4.null(), error: z4.strictObject({ code: z4.enum(Object.values(ClefErrorCode)), message: z4.string(), hint: z4.string().optional() }) })
2632
+ ]);
2633
+ function validateResult(value, expectedRow, metadata) {
2634
+ const parsed = resultSchema.safeParse(value);
2635
+ if (!parsed.success) throw invalid2("Invalid classification result in checkpoint");
2636
+ const result = parsed.data;
2637
+ if (result.row !== expectedRow || result.profile !== metadata.profile || result.model !== metadata.model) throw invalid2("Checkpoint row sequence or profile/model identity does not match");
2638
+ if (result.status === "error") return;
2639
+ const entries = Object.entries(result.probabilities).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
2640
+ if (entries.length < 2 || entries.length > 100 || Math.abs(entries.reduce((s, [, p]) => s + p, 0) - 1) > 1e-3 + 1e-12 || entries[0][0] !== result.suggested_label || Math.abs(entries[0][1] - result.probability) > 1e-12 || Math.abs(entries[0][1] - entries[1][1] - result.margin) > 1e-12 || (result.status === "classified" ? result.label !== result.suggested_label || result.review_reasons.length !== 0 || result.margin === 0 : result.label !== null || result.review_reasons.length === 0)) throw invalid2("Invalid classification policy fields in checkpoint");
2641
+ }
2642
+ async function exists(file) {
2643
+ try {
2644
+ await fsp9.lstat(file);
2645
+ return true;
2646
+ } catch (err) {
2647
+ if (err.code === "ENOENT") return false;
2648
+ throw err;
2649
+ }
2650
+ }
2651
+ async function protectFiles(candidates, protectedPaths) {
2652
+ for (const candidate of candidates) {
2653
+ for (const source of protectedPaths.filter((p) => p !== "-")) {
2654
+ if (path8.resolve(candidate) === path8.resolve(source)) throw invalid2("Output must not overwrite an input or profile file");
2655
+ try {
2656
+ const [a, b] = await Promise.all([fsp9.stat(candidate), fsp9.stat(source)]);
2657
+ if (a.dev === b.dev && a.ino === b.ino) throw invalid2("Output aliases an input or profile file");
2658
+ } catch (err) {
2659
+ if (err.code !== "ENOENT") throw err;
2660
+ }
2661
+ }
2662
+ }
2663
+ }
2664
+ async function openRegular(file, write = false) {
2665
+ const handle = await fsp9.open(file, (write ? constants.O_RDWR | constants.O_APPEND : constants.O_RDONLY) | (constants.O_NOFOLLOW ?? 0));
2666
+ const stat = await handle.stat();
2667
+ if (!stat.isFile() || stat.nlink !== 1 || (await fsp9.lstat(file)).isSymbolicLink()) {
2668
+ await handle.close();
2669
+ throw invalid2("Checkpoint and metadata must be regular files without links");
2670
+ }
2671
+ return handle;
2672
+ }
2673
+ async function readJson(file) {
2674
+ const handle = await openRegular(file);
2675
+ try {
2676
+ const chunks = [];
2677
+ let size = 0;
2678
+ for await (const chunk of fileChunks(handle)) {
2679
+ size += chunk.length;
2680
+ if (size > CLASSIFY_LIMITS.maxMetadataBytes) throw invalid2("Checkpoint metadata or lock exceeds the size limit");
2681
+ chunks.push(chunk);
2682
+ }
2683
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
2684
+ } finally {
2685
+ await handle.close();
2686
+ }
2687
+ }
2688
+ async function acquireLock(file, resume) {
2689
+ const lock = `${file}.lock`, claim = `${lock}.claim`, token = randomUUID();
2690
+ let guard;
2691
+ try {
2692
+ guard = await fsp9.open(claim, "wx", 384);
2693
+ } catch {
2694
+ throw invalid2(`Output lock acquisition is busy: ${claim}. Preserve active locks; inspect a crash-left claim before removing it.`);
2695
+ }
2696
+ try {
2697
+ if (await exists(lock)) {
2698
+ const owner = await readJson(lock);
2699
+ let dead = false;
2700
+ if (resume && owner.host === os2.hostname() && Number.isInteger(owner.pid) && owner.pid > 0 && owner.pid <= 2147483647 && typeof owner.token === "string") {
2701
+ try {
2702
+ process.kill(owner.pid, 0);
2703
+ } catch (err) {
2704
+ dead = err.code === "ESRCH";
2705
+ }
2706
+ }
2707
+ if (!dead) throw invalid2(`Output lock belongs to an active or unverifiable writer: ${lock}`);
2708
+ await fsp9.unlink(lock);
2709
+ }
2710
+ const handle = await fsp9.open(lock, "wx", 384);
2711
+ try {
2712
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os2.hostname(), token }));
2713
+ await handle.sync();
2714
+ } finally {
2715
+ await handle.close();
2716
+ }
2717
+ } finally {
2718
+ await guard.close();
2719
+ await fsp9.unlink(claim);
2720
+ }
2721
+ return async () => {
2722
+ const owner = await readJson(lock);
2723
+ if (owner.token === token) await fsp9.unlink(lock);
2724
+ };
2725
+ }
2726
+ async function writeMetadata(file, metadata) {
2727
+ const temp = `${file}.${randomUUID()}.tmp`;
2728
+ const handle = await fsp9.open(temp, "wx", 384);
2729
+ try {
2730
+ await handle.writeFile(JSON.stringify(metadata) + "\n");
2731
+ await handle.sync();
2732
+ await fsp9.link(temp, file);
2733
+ } finally {
2734
+ await handle.close();
2735
+ await fsp9.unlink(temp);
2736
+ }
2737
+ }
2738
+ async function openOutput(options) {
2739
+ const { metadata } = options;
2740
+ let handle, release;
2741
+ let completed = 0, closed = false, createdOutput = false;
2742
+ try {
2743
+ if (options.resume && !options.path) throw invalid2("--resume requires --output");
2744
+ if (options.path) {
2745
+ const file = options.path;
2746
+ await protectFiles([file, `${file}.meta.json`, `${file}.lock`, `${file}.lock.claim`], options.protectedPaths ?? []);
2747
+ if (!options.resume && (await exists(file) || await exists(`${file}.meta.json`))) throw invalid2("Output or metadata already exists; use --resume or a new output path");
2748
+ release = await acquireLock(file, !!options.resume);
2749
+ if (options.resume) {
2750
+ if (canonicalJson(await readJson(`${file}.meta.json`)) !== canonicalJson(metadata)) throw invalid2("Resume metadata does not match input, profile, model, runtime or format");
2751
+ handle = await openRegular(file, true);
2752
+ let safeOffset = 0, partial = false;
2753
+ for await (const line of boundedLines(fileChunks(handle), CLASSIFY_LIMITS.maxResultBytes, true)) {
2754
+ if (!line.terminated) {
2755
+ partial = true;
2756
+ break;
2757
+ }
2758
+ if (line.oversized) throw invalid2("Checkpoint result exceeds the size limit");
2759
+ let value;
2760
+ try {
2761
+ value = JSON.parse(line.text);
2762
+ } catch {
2763
+ throw invalid2("Corrupt complete JSONL checkpoint row");
2764
+ }
2765
+ validateResult(value, completed + 1, metadata);
2766
+ completed++;
2767
+ safeOffset += line.bytes;
2768
+ }
2769
+ if (partial) {
2770
+ await handle.truncate(safeOffset);
2771
+ await handle.sync();
2772
+ }
2773
+ } else {
2774
+ handle = await fsp9.open(file, "wx", 384);
2775
+ createdOutput = true;
2776
+ await writeMetadata(`${file}.meta.json`, metadata);
2777
+ }
2778
+ }
2779
+ } catch (err) {
2780
+ try {
2781
+ await handle?.close();
2782
+ } finally {
2783
+ if (createdOutput && options.path) await fsp9.unlink(options.path);
2784
+ await release?.();
2785
+ }
2786
+ throw err;
2787
+ }
2788
+ const stream = options.stream ?? process.stdout;
2789
+ return {
2790
+ get completedRows() {
2791
+ return completed;
2792
+ },
2793
+ async append(result) {
2794
+ if (closed) throw invalid2("Output is closed");
2795
+ validateResult(result, completed + 1, metadata);
2796
+ const line = JSON.stringify(result) + "\n";
2797
+ if (Buffer.byteLength(line) > CLASSIFY_LIMITS.maxResultBytes) throw invalid2("Result exceeds the maximum size");
2798
+ if (handle) {
2799
+ await handle.writeFile(line, "utf8");
2800
+ await handle.sync();
2801
+ } else await new Promise((resolve, reject) => stream.write(line, (err) => err ? reject(err) : resolve()));
2802
+ completed = result.row;
2803
+ },
2804
+ async finish(totalRows) {
2805
+ if (completed !== totalRows) throw invalid2("Checkpoint rows exceed the input or processing is incomplete");
2806
+ },
2807
+ async close() {
2808
+ if (closed) return;
2809
+ closed = true;
2810
+ try {
2811
+ await handle?.close();
2812
+ } finally {
2813
+ await release?.();
2814
+ }
2815
+ }
2816
+ };
2817
+ }
2818
+
2819
+ // src/classify/policy.ts
2820
+ init_errors();
2821
+ function evaluateClassification(profile, decision) {
2822
+ const entries = Object.entries(decision.answer);
2823
+ const labels = Object.keys(profile.labels);
2824
+ const total = entries.reduce((sum, [, p]) => sum + p, 0);
2825
+ if (entries.length !== labels.length || entries.some(([label, p]) => !Object.hasOwn(profile.labels, label) || !Number.isFinite(p) || p < 0 || p > 1) || Math.abs(total - 1) > 1e-3 + 1e-12 || decision.confidence !== void 0 && (!Number.isFinite(decision.confidence) || decision.confidence < 0 || decision.confidence > 1)) {
2826
+ throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, "Runtime returned an invalid classification distribution or confidence");
2827
+ }
2828
+ const ranked = entries.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
2829
+ const top = ranked[0];
2830
+ const margin = top[1] - ranked[1][1];
2831
+ const reasons = [];
2832
+ if (top[1] < profile.policy.min_probability - 1e-12) reasons.push("low_probability");
2833
+ if (margin < profile.policy.min_margin - 1e-12) reasons.push("low_margin");
2834
+ if (top[1] === ranked[1][1]) reasons.push("tie");
2835
+ if (profile.policy.min_confidence !== void 0) {
2836
+ if (decision.confidence === void 0) reasons.push("missing_confidence");
2837
+ else if (decision.confidence < profile.policy.min_confidence - 1e-12) reasons.push("low_confidence");
2838
+ }
2839
+ return {
2840
+ status: reasons.length ? "needs_review" : "classified",
2841
+ label: reasons.length ? null : top[0],
2842
+ suggested_label: top[0],
2843
+ probability: top[1],
2844
+ margin,
2845
+ probabilities: decision.answer,
2846
+ ...decision.confidence !== void 0 ? { confidence: decision.confidence } : {},
2847
+ review_reasons: reasons
2848
+ };
2849
+ }
2850
+
2851
+ // src/cli/classify.ts
2852
+ async function runClassify(inputPath, options) {
2853
+ const config = loadConfig();
2854
+ const profile = await loadProfile(options.profile, config.limits);
2855
+ const model = options.model ?? profile.model ?? config.model;
2856
+ const spec = getModelSpec(model);
2857
+ if (options.resume && (inputPath === "-" || !options.output)) throw invalid2("--resume requires a regular input file and --output");
2858
+ const loader = new RuntimeLoader(config, createLogger(config.logLevel));
2859
+ let source, output;
2860
+ let interrupted = false, total = 0;
2861
+ const counts = { classified: 0, needs_review: 0, errors: 0, skipped: 0 };
2862
+ const interrupt = () => {
2863
+ interrupted = true;
2864
+ if (inputPath === "-") process.stdin.destroy();
2865
+ };
2866
+ process.on("SIGINT", interrupt);
2867
+ process.on("SIGTERM", interrupt);
2868
+ try {
2869
+ source = await openInput(inputPath, options.format, config.limits);
2870
+ if (interrupted) return 130;
2871
+ output = await openOutput({
2872
+ path: options.output,
2873
+ resume: options.resume,
2874
+ protectedPaths: [inputPath, options.profile],
2875
+ metadata: { schema_version: 1, input_sha256: source.fingerprint ?? "", profile_sha256: profileDigest(profile), profile: profile.name, model, runtime: config.runtime, format: source.format }
2876
+ });
2877
+ await source.verifyUnchanged();
2878
+ const committed = output.completedRows;
2879
+ const commit = async (result) => {
2880
+ await source.verifyUnchanged();
2881
+ await output.append(result);
2882
+ };
2883
+ for await (const row of source.records()) {
2884
+ if (interrupted) break;
2885
+ total = row.row;
2886
+ if (row.row <= committed) {
2887
+ counts.skipped++;
2888
+ continue;
2889
+ }
2890
+ const rawId = profile.input?.id_field && row.record ? row.record[profile.input.id_field] : void 0;
2891
+ const base = { row: row.row, ...typeof rawId === "string" || typeof rawId === "number" && Number.isFinite(rawId) ? { id: String(rawId) } : {}, profile: profile.name, model };
2892
+ let doc;
2893
+ let rowError = row.error;
2894
+ if (!rowError) {
2895
+ try {
2896
+ const selected = selectRecord(profile, row.record);
2897
+ doc = { model, state: profileState(profile, selected), questions: {
2898
+ classification: { type: "choice", instructions: profile.instructions, criteria: profile.labels }
2899
+ } };
2900
+ clefDecideInputSchema(config.limits).parse(doc);
2901
+ validateStateSize(doc.state, config.limits);
2902
+ } catch (err) {
2903
+ rowError = errorJson(err instanceof ClefError ? err : invalid2(err instanceof Error ? err.message : String(err))).error;
2904
+ }
2905
+ }
2906
+ if (rowError) {
2907
+ await commit({ ...base, status: "error", label: null, error: rowError });
2908
+ counts.errors++;
2909
+ continue;
2910
+ }
2911
+ const { runtime } = await loader.ensure(spec);
2912
+ if (interrupted) break;
2913
+ await source.verifyUnchanged();
2914
+ const response = await runtime.decide(toSystemOneRequest(doc, model));
2915
+ const adapted = fromSystemOneResponse(response, doc.questions, model);
2916
+ const result = evaluateClassification(profile, adapted.decisions.classification);
2917
+ await commit({ ...base, ...result, ...adapted.usage ? { usage: adapted.usage } : {} });
2918
+ counts[result.status]++;
2919
+ }
2920
+ if (interrupted) return 130;
2921
+ await source.verifyUnchanged();
2922
+ await output.finish(total);
2923
+ return counts.errors ? 1 : 0;
2924
+ } catch (err) {
2925
+ if (interrupted) return 130;
2926
+ throw err;
2927
+ } finally {
2928
+ process.removeListener("SIGINT", interrupt);
2929
+ process.removeListener("SIGTERM", interrupt);
2930
+ try {
2931
+ await loader.dispose();
2932
+ } finally {
2933
+ try {
2934
+ await output?.close();
2935
+ } finally {
2936
+ try {
2937
+ await source?.close();
2938
+ } finally {
2939
+ process.stderr.write(`classification summary: ${JSON.stringify(counts)}${interrupted ? " (interrupted)" : ""}
2940
+ `);
2941
+ }
2942
+ }
2943
+ }
2944
+ }
2945
+ }
2946
+
2353
2947
  // src/cli/doctor.ts
2354
2948
  init_errors();
2355
2949
  import { execFile as execFile3 } from "child_process";
2356
- import os3 from "os";
2950
+ import os4 from "os";
2357
2951
  import { promisify } from "util";
2358
- import fsp7 from "fs/promises";
2359
- import path8 from "path";
2952
+ import fsp10 from "fs/promises";
2953
+ import path9 from "path";
2360
2954
 
2361
2955
  // src/models/platform.ts
2362
2956
  init_errors();
2363
2957
  import { execFile as execFile2 } from "child_process";
2364
- import os2 from "os";
2958
+ import os3 from "os";
2365
2959
  function exec(cmd, args, timeoutMs) {
2366
2960
  return new Promise((resolve, reject) => {
2367
2961
  execFile2(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
@@ -2414,7 +3008,7 @@ async function detectPlatform() {
2414
3008
  const info = {
2415
3009
  platform,
2416
3010
  arch: process.arch,
2417
- totalMemBytes: os2.totalmem()
3011
+ totalMemBytes: os3.totalmem()
2418
3012
  };
2419
3013
  if (platform === "darwin") info.gpu = await gpuInfoDarwin();
2420
3014
  else if (platform === "linux") info.gpu = await gpuInfoLinux();
@@ -2508,16 +3102,16 @@ async function checkBinaryVersion(binaryPath) {
2508
3102
  }
2509
3103
  }
2510
3104
  async function detectMcpConfigs() {
2511
- const home = os3.homedir();
3105
+ const home = os4.homedir();
2512
3106
  const hints = [
2513
- { client: "Codex", path: path8.join(home, ".codex", "config.toml"), configured: false },
2514
- { client: "Claude Code (user)", path: path8.join(home, ".claude.json"), configured: false },
2515
- { client: "Claude Code (project)", path: path8.resolve(".mcp.json"), configured: false },
2516
- { client: "Cursor", path: path8.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
3107
+ { client: "Codex", path: path9.join(home, ".codex", "config.toml"), configured: false },
3108
+ { client: "Claude Code (user)", path: path9.join(home, ".claude.json"), configured: false },
3109
+ { client: "Claude Code (project)", path: path9.resolve(".mcp.json"), configured: false },
3110
+ { client: "Cursor", path: path9.join(process.cwd(), ".cursor", "mcp.json"), configured: false }
2517
3111
  ];
2518
3112
  for (const hint of hints) {
2519
3113
  try {
2520
- const content = await fsp7.readFile(hint.path, "utf8");
3114
+ const content = await fsp10.readFile(hint.path, "utf8");
2521
3115
  hint.configured = content.includes("clef-mcp");
2522
3116
  } catch {
2523
3117
  hint.configured = false;
@@ -2730,14 +3324,14 @@ init_errors();
2730
3324
  // src/models/llama-install.ts
2731
3325
  init_errors();
2732
3326
  import { execFile as execFile4 } from "child_process";
2733
- import fsp9 from "fs/promises";
2734
- import path10 from "path";
3327
+ import fsp12 from "fs/promises";
3328
+ import path11 from "path";
2735
3329
 
2736
3330
  // src/models/hf.ts
2737
3331
  init_errors();
2738
- import { createHash as createHash2 } from "crypto";
2739
- import fsp8 from "fs/promises";
2740
- import path9 from "path";
3332
+ import { createHash as createHash4 } from "crypto";
3333
+ import fsp11 from "fs/promises";
3334
+ import path10 from "path";
2741
3335
  var HF_BASE_URL = "https://huggingface.co";
2742
3336
  function resolveUrl(repo, revision, file) {
2743
3337
  return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
@@ -2794,8 +3388,8 @@ async function downloadOnce(url, dest, opts) {
2794
3388
  throw new Error(`HTTP ${res.status}`);
2795
3389
  }
2796
3390
  const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
2797
- const hash = createHash2("sha256");
2798
- const handle = await fsp8.open(dest, "w");
3391
+ const hash = createHash4("sha256");
3392
+ const handle = await fsp11.open(dest, "w");
2799
3393
  let bytes = 0;
2800
3394
  try {
2801
3395
  for await (const chunk of res.body) {
@@ -2810,7 +3404,7 @@ async function downloadOnce(url, dest, opts) {
2810
3404
  }
2811
3405
  const sha256 = hash.digest("hex");
2812
3406
  if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
2813
- await fsp8.rm(dest, { force: true });
3407
+ await fsp11.rm(dest, { force: true });
2814
3408
  throw new ClefError(
2815
3409
  ClefErrorCode.CHECKSUM_MISMATCH,
2816
3410
  `Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
@@ -2818,7 +3412,7 @@ async function downloadOnce(url, dest, opts) {
2818
3412
  );
2819
3413
  }
2820
3414
  if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
2821
- await fsp8.rm(dest, { force: true });
3415
+ await fsp11.rm(dest, { force: true });
2822
3416
  throw new ClefError(
2823
3417
  ClefErrorCode.DOWNLOAD_FAILED,
2824
3418
  `Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
@@ -2836,17 +3430,17 @@ async function installModel(opts) {
2836
3430
  return { manifest: existing, alreadyInstalled: true };
2837
3431
  }
2838
3432
  const info = await fetchFileInfo(modelSpec.repo, modelSpec.revision, quantSpec.file);
2839
- const finalPath = path9.join(path9.dirname(manifestPath2), quantSpec.file);
2840
- await fsp8.mkdir(path9.dirname(finalPath), { recursive: true });
2841
- const tmpPath = path9.join(path9.dirname(finalPath), `.download-${quantSpec.file}`);
2842
- await fsp8.rm(tmpPath, { force: true });
3433
+ const finalPath = path10.join(path10.dirname(manifestPath2), quantSpec.file);
3434
+ await fsp11.mkdir(path10.dirname(finalPath), { recursive: true });
3435
+ const tmpPath = path10.join(path10.dirname(finalPath), `.download-${quantSpec.file}`);
3436
+ await fsp11.rm(tmpPath, { force: true });
2843
3437
  opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
2844
3438
  const result = await downloadFile(info.downloadUrl, tmpPath, {
2845
3439
  expectedSha256: info.sha256,
2846
3440
  expectedSize: info.size ?? quantSpec.bytes,
2847
3441
  onProgress: opts.onProgress
2848
3442
  });
2849
- await fsp8.rename(tmpPath, finalPath);
3443
+ await fsp11.rename(tmpPath, finalPath);
2850
3444
  const manifest = {
2851
3445
  schemaVersion: 1,
2852
3446
  model: modelSpec.id,
@@ -2934,12 +3528,12 @@ async function findServerBinary(dir, platform) {
2934
3528
  const current = queue.shift();
2935
3529
  let entries;
2936
3530
  try {
2937
- entries = await fsp9.readdir(current, { withFileTypes: true });
3531
+ entries = await fsp12.readdir(current, { withFileTypes: true });
2938
3532
  } catch {
2939
3533
  continue;
2940
3534
  }
2941
3535
  for (const entry of entries) {
2942
- const full = path10.join(current, entry.name);
3536
+ const full = path11.join(current, entry.name);
2943
3537
  if (entry.isDirectory()) queue.push(full);
2944
3538
  else if (entry.name === target) return full;
2945
3539
  }
@@ -2950,14 +3544,14 @@ async function findServerBinary(dir, platform) {
2950
3544
  );
2951
3545
  }
2952
3546
  async function extractRelease(archivePath, destDir, platform) {
2953
- await fsp9.mkdir(destDir, { recursive: true });
3547
+ await fsp12.mkdir(destDir, { recursive: true });
2954
3548
  const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
2955
3549
  try {
2956
3550
  await execTar(args, destDir);
2957
3551
  } catch (err) {
2958
3552
  throw new ClefError(
2959
3553
  ClefErrorCode.DOWNLOAD_FAILED,
2960
- `Could not extract ${path10.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
3554
+ `Could not extract ${path11.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
2961
3555
  "Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
2962
3556
  );
2963
3557
  }
@@ -2967,7 +3561,7 @@ async function ensureLlamaRuntime(cfg, log) {
2967
3561
  const resolved = resolveLlamaServerBinary(cfg);
2968
3562
  if (resolved.source === "managed") {
2969
3563
  const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
2970
- const manifest = readRuntimeManifest(path10.join(platformDir, "runtime-manifest.json"));
3564
+ const manifest = readRuntimeManifest(path11.join(platformDir, "runtime-manifest.json"));
2971
3565
  return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
2972
3566
  }
2973
3567
  return { ...resolved, releaseTag: resolved.source, freshInstall: false };
@@ -2999,16 +3593,16 @@ async function downloadManagedLlama(cfg, log) {
2999
3593
  }
3000
3594
  const asset = pickAsset(release, platform, arch);
3001
3595
  log?.info("downloading llama.cpp runtime", { tag: release.tag_name, asset: asset.name });
3002
- const tagDir = path10.join(platformDir, release.tag_name);
3003
- await fsp9.mkdir(tagDir, { recursive: true });
3004
- const tmpArchive = path10.join(platformDir, `.download-${asset.name}`);
3005
- await fsp9.rm(tmpArchive, { force: true });
3596
+ const tagDir = path11.join(platformDir, release.tag_name);
3597
+ await fsp12.mkdir(tagDir, { recursive: true });
3598
+ const tmpArchive = path11.join(platformDir, `.download-${asset.name}`);
3599
+ await fsp12.rm(tmpArchive, { force: true });
3006
3600
  const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
3007
3601
  await extractRelease(tmpArchive, tagDir, platform);
3008
- await fsp9.rm(tmpArchive, { force: true });
3602
+ await fsp12.rm(tmpArchive, { force: true });
3009
3603
  const binaryPath = await findServerBinary(tagDir, platform);
3010
- if (platform !== "win32") await fsp9.chmod(binaryPath, 493);
3011
- await writeRuntimeManifest(path10.join(platformDir, "runtime-manifest.json"), {
3604
+ if (platform !== "win32") await fsp12.chmod(binaryPath, 493);
3605
+ await writeRuntimeManifest(path11.join(platformDir, "runtime-manifest.json"), {
3012
3606
  schemaVersion: 1,
3013
3607
  kind: "llama-cpp",
3014
3608
  releaseTag: release.tag_name,
@@ -3024,11 +3618,11 @@ async function removeManagedRuntime(cfg) {
3024
3618
  const { runtimeDir } = pathsFor(cfg.clefHome);
3025
3619
  const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
3026
3620
  try {
3027
- await fsp9.stat(dir);
3621
+ await fsp12.stat(dir);
3028
3622
  } catch {
3029
3623
  return false;
3030
3624
  }
3031
- await fsp9.rm(dir, { recursive: true, force: true });
3625
+ await fsp12.rm(dir, { recursive: true, force: true });
3032
3626
  return true;
3033
3627
  }
3034
3628
 
@@ -3036,7 +3630,7 @@ async function removeManagedRuntime(cfg) {
3036
3630
  init_prompt();
3037
3631
 
3038
3632
  // src/version.ts
3039
- var VERSION = "0.3.1";
3633
+ var VERSION = "0.4.0";
3040
3634
 
3041
3635
  // src/cli/install.ts
3042
3636
  var OS_NAMES = {
@@ -3225,7 +3819,7 @@ async function runStatus() {
3225
3819
 
3226
3820
  // src/cli/uninstall.ts
3227
3821
  init_errors();
3228
- import fsp11 from "fs/promises";
3822
+ import fsp14 from "fs/promises";
3229
3823
  init_prompt();
3230
3824
  async function runUninstall(opts) {
3231
3825
  const config = loadConfig();
@@ -3264,7 +3858,7 @@ async function runUninstall(opts) {
3264
3858
  return;
3265
3859
  }
3266
3860
  if (target) {
3267
- await fsp11.rm(target, { recursive: true, force: true });
3861
+ await fsp14.rm(target, { recursive: true, force: true });
3268
3862
  console.log(`Removed: ${target}`);
3269
3863
  }
3270
3864
  if (opts.runtime) {
@@ -3291,6 +3885,13 @@ program.action(async () => {
3291
3885
  handleCliError(err);
3292
3886
  }
3293
3887
  });
3888
+ program.command("classify <input>").description('Classify JSONL/CSV records using a YAML profile; uncertain results require review. Use "-" for stdin.').requiredOption("--profile <path>", "YAML classification profile").option("--output <path>", "JSONL output file (default: stdout; existing files are protected)").option("--format <format>", "input format: jsonl or csv (default: file extension, JSONL for stdin)").option("--model <id>", "override the profile model and CLEF_MODEL").option("--resume", "resume a matching file input/output checkpoint").action(async (input, opts) => {
3889
+ try {
3890
+ process.exit(await runClassify(input, opts));
3891
+ } catch (err) {
3892
+ handleCliError(err);
3893
+ }
3894
+ });
3294
3895
  program.command("decide").description("One-shot decision (no MCP session): JSON in, strict JSON result out. For hooks, CI and scripts.").option("--state <source>", 'state: file path, "-" for stdin, or inline JSON/text').option("--questions <source>", 'questions map: file path, "-" for stdin, or inline JSON').option("--model <id>", "model to use (default: CLEF_MODEL or clef-flash)").option("--daemon", "route through a running `clef-mcp daemon` (falls back to a cold run when none is reachable)").action(async (opts) => {
3295
3896
  try {
3296
3897
  await runDecide(opts);