@tuned-tensor/cli 0.9.0 → 0.9.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/dist/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ assertLocalRunInputReady,
4
+ loadLocalRunInput,
5
+ loadLocalRunnerConfig,
6
+ runLocalPipeline
7
+ } from "./chunk-DAP2Z7EZ.js";
2
8
 
3
9
  // src/cli.ts
4
10
  import { spawn as spawn4 } from "child_process";
@@ -235,6 +241,135 @@ function canonicalizeSpecBaseModel(body) {
235
241
  return body;
236
242
  }
237
243
 
244
+ // src/pipeline.ts
245
+ var PIPELINE_VERSION = 1;
246
+ var PIPELINE_TARGETS = ["local", "cloud"];
247
+ var PIPELINE_USES = ["train", "evaluate", "compare"];
248
+ var OUTPUTS = {
249
+ train: ["model"],
250
+ evaluate: ["report"],
251
+ compare: ["comparison"]
252
+ };
253
+ function isRecord(value) {
254
+ return value !== null && typeof value === "object" && !Array.isArray(value);
255
+ }
256
+ function ref(value) {
257
+ return isRecord(value) && typeof value.from === "string" ? { from: value.from } : void 0;
258
+ }
259
+ function parseRef(value) {
260
+ const match = /^([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_-]*)$/.exec(value.from);
261
+ return match ? { stepId: match[1], output: match[2] } : void 0;
262
+ }
263
+ function references(step) {
264
+ if (step.uses === "evaluate") return [{ field: "model", value: step.with?.model, expected: "model" }];
265
+ if (step.uses === "compare") return [
266
+ { field: "before", value: step.with?.before, expected: "report" },
267
+ { field: "after", value: step.with?.after, expected: "report" }
268
+ ];
269
+ return [];
270
+ }
271
+ function resolvedTarget(step, pipeline2) {
272
+ return step.target ?? pipeline2.target;
273
+ }
274
+ function validatePipeline(pipeline2) {
275
+ const errors = [];
276
+ if (!isRecord(pipeline2)) return ["Pipeline must be an object."];
277
+ if (pipeline2.version !== PIPELINE_VERSION) errors.push("Pipeline version must be 1.");
278
+ if (pipeline2.target !== void 0 && !PIPELINE_TARGETS.includes(pipeline2.target)) {
279
+ errors.push("Pipeline target must be local or cloud.");
280
+ }
281
+ const typedPipeline = pipeline2;
282
+ if (!Array.isArray(pipeline2.steps) || pipeline2.steps.length === 0) return [...errors, "Pipeline must contain at least one step."];
283
+ const seen = /* @__PURE__ */ new Map();
284
+ let trainCount = 0;
285
+ for (const rawStep of pipeline2.steps) {
286
+ if (!isRecord(rawStep)) {
287
+ errors.push("Each pipeline step must be an object.");
288
+ continue;
289
+ }
290
+ const step = rawStep;
291
+ if (typeof step.id !== "string" || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(step.id)) {
292
+ errors.push("Step id must start with a letter and contain only letters, numbers, _ or -.");
293
+ continue;
294
+ }
295
+ if (seen.has(step.id)) errors.push(`Duplicate step id: ${step.id}.`);
296
+ if (!PIPELINE_USES.includes(step.uses)) errors.push(`Step ${step.id} uses an unsupported component: ${String(step.uses)}.`);
297
+ if (step.uses === "train" && ++trainCount > 1) errors.push("Pipeline v1 supports at most one train step because local model artifacts are run-scoped.");
298
+ if (step.uses === "evaluate" && step.with?.evaluator !== void 0 && step.with.evaluator !== "behavior") {
299
+ errors.push(`Step ${step.id} evaluator must be behavior.`);
300
+ }
301
+ if (step.target !== void 0 && !PIPELINE_TARGETS.includes(step.target)) errors.push(`Step ${step.id} target must be local or cloud.`);
302
+ if (!resolvedTarget(step, typedPipeline)) errors.push(`Step ${step.id} needs a target or a pipeline default target.`);
303
+ for (const input of references(step)) {
304
+ if (step.uses === "evaluate" && input.value === "base") continue;
305
+ const source = ref(input.value);
306
+ if (!source) {
307
+ errors.push(`Step ${step.id} ${input.field} must be ${step.uses === "evaluate" ? "base or " : ""}a { from: "step.output" } reference.`);
308
+ continue;
309
+ }
310
+ const parsed = parseRef(source);
311
+ if (!parsed) {
312
+ errors.push(`Step ${step.id} has an invalid reference: ${source.from}.`);
313
+ continue;
314
+ }
315
+ const producer = seen.get(parsed.stepId);
316
+ if (!producer) {
317
+ const existsLater = pipeline2.steps.some((candidate) => isRecord(candidate) && candidate.id === parsed.stepId);
318
+ errors.push(`Step ${step.id} has a ${existsLater ? "forward" : "missing"} reference to ${parsed.stepId}.`);
319
+ } else if (!OUTPUTS[producer.uses]?.includes(parsed.output)) {
320
+ errors.push(`Step ${parsed.stepId} does not produce ${parsed.output} for step ${step.id}; it produces ${OUTPUTS[producer.uses]?.join(", ") ?? "no outputs"}.`);
321
+ } else if (parsed.output !== input.expected) {
322
+ errors.push(`Step ${step.id} ${input.field} requires a ${input.expected} reference, not ${source.from}.`);
323
+ }
324
+ }
325
+ seen.set(step.id, step);
326
+ }
327
+ return errors;
328
+ }
329
+ function canonicalPipeline(target = "local") {
330
+ return {
331
+ version: PIPELINE_VERSION,
332
+ name: `default-${target}`,
333
+ target,
334
+ steps: [
335
+ { id: "baseline", uses: "evaluate", target, with: { model: "base", evaluator: "behavior" } },
336
+ { id: "train", uses: "train", target },
337
+ { id: "candidate", uses: "evaluate", target, with: { model: { from: "train.model" }, evaluator: "behavior" } },
338
+ { id: "compare", uses: "compare", target, with: { before: { from: "baseline.report" }, after: { from: "candidate.report" } } }
339
+ ]
340
+ };
341
+ }
342
+ function createExecutionPlan(pipeline2, selection = {}) {
343
+ const errors = validatePipeline(pipeline2);
344
+ if (errors.length) throw new Error(`Invalid pipeline:
345
+ - ${errors.join("\n- ")}`);
346
+ const allIds = new Set(pipeline2.steps.map((step) => step.id));
347
+ for (const id of [...selection.only ?? [], ...selection.skip ?? []]) {
348
+ if (!allIds.has(id)) throw new Error(`Unknown pipeline step: ${id}.`);
349
+ }
350
+ const only = selection.only?.length ? new Set(selection.only) : void 0;
351
+ const skip = new Set(selection.skip ?? []);
352
+ const steps = pipeline2.steps.filter((step) => (!only || only.has(step.id)) && !skip.has(step.id));
353
+ if (!steps.length) throw new Error("Selection leaves no pipeline steps to run.");
354
+ const retained = new Map(steps.map((step) => [step.id, step]));
355
+ const resolved = steps.map((step) => {
356
+ const target = resolvedTarget(step, pipeline2);
357
+ const transfers = [];
358
+ for (const input of references(step)) {
359
+ const source = ref(input.value);
360
+ if (!source) continue;
361
+ const parsed = parseRef(source);
362
+ const producer = retained.get(parsed.stepId);
363
+ if (!producer) throw new Error(`Step ${step.id} has a dependency on ${parsed.stepId}, which was omitted by --only or --skip.`);
364
+ const fromTarget = resolvedTarget(producer, pipeline2);
365
+ if (fromTarget !== target) transfers.push({ from: source.from, from_target: fromTarget, to_target: target });
366
+ }
367
+ const normalized = step.uses === "evaluate" ? { ...step, with: { ...step.with, evaluator: step.with?.evaluator ?? "behavior" } } : step;
368
+ return { ...normalized, target, transfers };
369
+ });
370
+ return { version: PIPELINE_VERSION, ...pipeline2.name ? { name: pipeline2.name } : {}, steps: resolved };
371
+ }
372
+
238
373
  // src/agent-tools.ts
239
374
  var MAX_TOOL_OUTPUT = 32e3;
240
375
  var MAX_PREPARED_ACTION = 24e3;
@@ -283,6 +418,17 @@ var RunBody = Type.Object({
283
418
  var BaseModel = Type.Union(
284
419
  SUPPORTED_BASE_MODELS.map((model) => Type.Literal(model))
285
420
  );
421
+ var PipelineDocument = Type.Object({
422
+ version: Type.Literal(1),
423
+ name: Type.Optional(Type.String({ minLength: 1, maxLength: 255 })),
424
+ target: Type.Optional(Type.Union([Type.Literal("local"), Type.Literal("cloud")])),
425
+ steps: Type.Array(Type.Object({
426
+ id: Type.String({ minLength: 1, maxLength: 128 }),
427
+ uses: Type.Union([Type.Literal("train"), Type.Literal("evaluate"), Type.Literal("compare")]),
428
+ target: Type.Optional(Type.Union([Type.Literal("local"), Type.Literal("cloud")])),
429
+ with: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
430
+ }, { additionalProperties: false }), { minItems: 1, maxItems: 100 })
431
+ }, { additionalProperties: false });
286
432
  var Example = Type.Object({
287
433
  input: Type.String({ minLength: 1, maxLength: 1e5 }),
288
434
  output: Type.String({ minLength: 1, maxLength: 1e5 })
@@ -460,7 +606,18 @@ function createTunedTensorTools(api) {
460
606
  }, { additionalProperties: false }), async (p) => await api.get("/billing/transactions", {
461
607
  page: p.page ?? 1,
462
608
  per_page: p.per_page ?? 10
463
- }))
609
+ })),
610
+ define("describe_pipeline", "Describe pipeline", "Describe the built-in v1 pipeline contract and canonical recipe. This never executes anything.", Type.Object({
611
+ target: Type.Optional(Type.Union([Type.Literal("local"), Type.Literal("cloud")]))
612
+ }, { additionalProperties: false }), async (p) => ({
613
+ version: 1,
614
+ components: ["train", "evaluate", "compare"],
615
+ canonical: canonicalPipeline(p.target ?? "local")
616
+ })),
617
+ define("validate_pipeline", "Validate pipeline", "Validate an ordered pipeline and resolve targets/transfers without filesystem, network, or execution side effects.", Type.Object({ pipeline: PipelineDocument }, { additionalProperties: false }), async (p) => {
618
+ const errors = validatePipeline(p.pipeline);
619
+ return { valid: errors.length === 0, errors, ...errors.length ? {} : { plan: createExecutionPlan(p.pipeline) } };
620
+ })
464
621
  ];
465
622
  const mutations = [
466
623
  define("prepare_create_spec", "Prepare create spec", "Prepare creating a spec. This never mutates; /approve is required.", Type.Object({
@@ -480,6 +637,19 @@ function createTunedTensorTools(api) {
480
637
  changes: p.changes,
481
638
  expected_spec_updated_at: current.updated_at
482
639
  }));
640
+ }),
641
+ define("prepare_pipeline_run", "Prepare pipeline run", "Prepare a pipeline action for review only. It never executes a step, transfers an artifact, or reserves credits.", Type.Object({
642
+ pipeline: PipelineDocument,
643
+ dry_run: Type.Optional(Type.Boolean())
644
+ }, { additionalProperties: false }), async (p) => {
645
+ const errors = validatePipeline(p.pipeline);
646
+ if (errors.length) throw new Error(`Invalid pipeline:
647
+ - ${errors.join("\n- ")}`);
648
+ const plan = createExecutionPlan(p.pipeline);
649
+ return await api.propose(proposal("run_pipeline", "Run pipeline", "Review the resolved pipeline before any separate execution approval flow.", {
650
+ pipeline: p.pipeline,
651
+ dry_run: p.dry_run ?? true
652
+ }, { plan, execution: "not started" }));
483
653
  })
484
654
  ];
485
655
  return [...reads, ...mutations];
@@ -692,7 +862,7 @@ var DEFAULT_MAX_CLAIMS = 1e3;
692
862
  function assertThreadId(id) {
693
863
  if (!UUID.test(id)) throw new Error("Invalid local agent thread ID.");
694
864
  }
695
- function isRecord(value) {
865
+ function isRecord2(value) {
696
866
  return value !== null && typeof value === "object" && !Array.isArray(value);
697
867
  }
698
868
  function scrub(value, secrets) {
@@ -703,7 +873,7 @@ function scrub(value, secrets) {
703
873
  );
704
874
  }
705
875
  if (Array.isArray(value)) return value.map((entry) => scrub(entry, secrets));
706
- if (isRecord(value)) {
876
+ if (isRecord2(value)) {
707
877
  return Object.fromEntries(
708
878
  Object.entries(value).map(([key, entry]) => [key, scrub(entry, secrets)])
709
879
  );
@@ -852,7 +1022,7 @@ var LocalAgentStore = class {
852
1022
  throw new Error("Invalid local agent thread file.");
853
1023
  }
854
1024
  const parsed = JSON.parse(await readFile(path, "utf8"));
855
- if (!isRecord(parsed) || !isRecord(parsed.thread) || parsed.thread.id !== id || !Array.isArray(parsed.messages) || !Array.isArray(parsed.actions)) {
1025
+ if (!isRecord2(parsed) || !isRecord2(parsed.thread) || parsed.thread.id !== id || !Array.isArray(parsed.messages) || !Array.isArray(parsed.actions)) {
856
1026
  throw new Error("Local agent thread file is malformed.");
857
1027
  }
858
1028
  return parsed;
@@ -2218,7 +2388,7 @@ var StreamingTerminalMarkdown = class {
2218
2388
  var accent3 = chalk3.hex("#8B5CF6");
2219
2389
  var successMark2 = () => chalk3.green("\u2713");
2220
2390
  var errorMark2 = () => chalk3.red("\u2717");
2221
- function isRecord2(value) {
2391
+ function isRecord3(value) {
2222
2392
  return value !== null && typeof value === "object" && !Array.isArray(value);
2223
2393
  }
2224
2394
  function stringValue(value) {
@@ -2259,7 +2429,7 @@ function formatThread(thread, activeId) {
2259
2429
  return `${active} ${chalk3.dim(thread.id.slice(0, 8))} ${sanitizeTerminalText2(thread.title)} ${chalk3.dim(dateLabel)}`;
2260
2430
  }
2261
2431
  function actionFromPayload(payload) {
2262
- const value = isRecord2(payload.action) ? payload.action : isRecord2(payload.approval) ? payload.approval : payload;
2432
+ const value = isRecord3(payload.action) ? payload.action : isRecord3(payload.approval) ? payload.approval : payload;
2263
2433
  const id = stringValue(value.id);
2264
2434
  const title = stringValue(value.title);
2265
2435
  if (!id || !title) return null;
@@ -2392,7 +2562,7 @@ ${chalk3.yellow.bold("Approval required")} ${chalk3.dim(action.id.slice(0, 8))}
2392
2562
  if (event.type === "tool_call") {
2393
2563
  this.flushResponse();
2394
2564
  this.endOpenLine();
2395
- const name = stringValue(payload.name) ?? stringValue(isRecord2(payload.tool_call) ? payload.tool_call.name : null) ?? "tool";
2565
+ const name = stringValue(payload.name) ?? stringValue(isRecord3(payload.tool_call) ? payload.tool_call.name : null) ?? "tool";
2396
2566
  this.options.io.write(chalk3.dim(` \u25CB ${name}
2397
2567
  `));
2398
2568
  return;
@@ -2646,8 +2816,9 @@ ${chalk3.yellow.bold("Approval required")} ${chalk3.dim(action.id.slice(0, 8))}
2646
2816
  // src/local-runner.ts
2647
2817
  import { constants as osConstants } from "os";
2648
2818
  import { randomUUID as randomUUID4 } from "crypto";
2649
- import { createRequire } from "module";
2650
2819
  import { spawn } from "child_process";
2820
+ import { existsSync as existsSync2 } from "fs";
2821
+ import { fileURLToPath } from "url";
2651
2822
  import {
2652
2823
  readFile as readFile3,
2653
2824
  rm,
@@ -2820,7 +2991,7 @@ function shortId(id) {
2820
2991
  }
2821
2992
 
2822
2993
  // src/local-output.ts
2823
- function isRecord3(value) {
2994
+ function isRecord4(value) {
2824
2995
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2825
2996
  }
2826
2997
  function text(value) {
@@ -2834,10 +3005,10 @@ function number(value) {
2834
3005
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2835
3006
  }
2836
3007
  function record3(value) {
2837
- return isRecord3(value) ? value : void 0;
3008
+ return isRecord4(value) ? value : void 0;
2838
3009
  }
2839
3010
  function rows(value) {
2840
- return Array.isArray(value) ? value.filter(isRecord3) : [];
3011
+ return Array.isArray(value) ? value.filter(isRecord4) : [];
2841
3012
  }
2842
3013
  function displayId(value) {
2843
3014
  const id = text(value);
@@ -3125,19 +3296,19 @@ function renderServePlan(value) {
3125
3296
  function renderKnownJson(args2, value) {
3126
3297
  const command = args2[0];
3127
3298
  const subcommand = args2[1];
3128
- if (command === "doctor" && isRecord3(value)) {
3299
+ if (command === "doctor" && isRecord4(value)) {
3129
3300
  renderDoctor(value);
3130
3301
  return true;
3131
3302
  }
3132
- if (command === "init" && isRecord3(value)) {
3303
+ if (command === "init" && isRecord4(value)) {
3133
3304
  renderInit(value);
3134
3305
  return true;
3135
3306
  }
3136
- if (command === "validate" && isRecord3(value)) {
3307
+ if (command === "validate" && isRecord4(value)) {
3137
3308
  renderValidate(value);
3138
3309
  return true;
3139
3310
  }
3140
- if (command === "run" && isRecord3(value)) {
3311
+ if (command === "run" && isRecord4(value)) {
3141
3312
  renderRun(value);
3142
3313
  return true;
3143
3314
  }
@@ -3150,15 +3321,15 @@ function renderKnownJson(args2, value) {
3150
3321
  renderRunEvents(value);
3151
3322
  return true;
3152
3323
  }
3153
- if (subcommand === "get" && isRecord3(value)) {
3324
+ if (subcommand === "get" && isRecord4(value)) {
3154
3325
  renderRunRecord(value);
3155
3326
  return true;
3156
3327
  }
3157
- if (subcommand === "report" && isRecord3(value)) {
3328
+ if (subcommand === "report" && isRecord4(value)) {
3158
3329
  renderRunReport(value);
3159
3330
  return true;
3160
3331
  }
3161
- if (subcommand === "compare" && isRecord3(value)) {
3332
+ if (subcommand === "compare" && isRecord4(value)) {
3162
3333
  renderRunComparison(value);
3163
3334
  return true;
3164
3335
  }
@@ -3168,25 +3339,25 @@ function renderKnownJson(args2, value) {
3168
3339
  renderModelsList(value);
3169
3340
  return true;
3170
3341
  }
3171
- if ((subcommand === "get" || subcommand === "verify") && isRecord3(value)) {
3342
+ if ((subcommand === "get" || subcommand === "verify") && isRecord4(value)) {
3172
3343
  if (subcommand === "verify") printSuccess("Local model artifact is valid");
3173
3344
  renderModelRecord(value);
3174
3345
  return true;
3175
3346
  }
3176
- if ((subcommand === "prefetch" || subcommand === "verify-base") && isRecord3(value)) {
3347
+ if ((subcommand === "prefetch" || subcommand === "verify-base") && isRecord4(value)) {
3177
3348
  renderModelPrefetch(value);
3178
3349
  return true;
3179
3350
  }
3180
- if (["active", "activate", "rollback"].includes(subcommand ?? "") && isRecord3(value)) {
3351
+ if (["active", "activate", "rollback"].includes(subcommand ?? "") && isRecord4(value)) {
3181
3352
  renderActiveModel(value);
3182
3353
  return true;
3183
3354
  }
3184
- if (subcommand === "serve" && isRecord3(value)) {
3355
+ if (subcommand === "serve" && isRecord4(value)) {
3185
3356
  renderServePlan(value);
3186
3357
  return true;
3187
3358
  }
3188
3359
  }
3189
- if (command === "serve" && isRecord3(value)) {
3360
+ if (command === "serve" && isRecord4(value)) {
3190
3361
  renderServePlan(value);
3191
3362
  return true;
3192
3363
  }
@@ -3274,7 +3445,6 @@ function renderLocalOutput(payload, options = {}) {
3274
3445
  }
3275
3446
 
3276
3447
  // src/local-runner.ts
3277
- var localRequire = createRequire(import.meta.url);
3278
3448
  var DEFAULT_SPEC_NAME = "tunedtensor.json";
3279
3449
  var VALUE_OPTIONS = /* @__PURE__ */ new Set([
3280
3450
  "--config",
@@ -3460,14 +3630,13 @@ async function projectSpecArguments(originalArgs, cwd) {
3460
3630
  };
3461
3631
  }
3462
3632
  function resolveLocalCliEntrypoint() {
3463
- try {
3464
- return localRequire.resolve("@tuned-tensor/local");
3465
- } catch (error) {
3633
+ const entrypoint = fileURLToPath(new URL("./local-runtime.js", import.meta.url));
3634
+ if (!existsSync2(entrypoint)) {
3466
3635
  throw new Error(
3467
- "TT Local is not installed. Reinstall @tuned-tensor/cli with its local runtime dependency.",
3468
- { cause: error }
3636
+ "The bundled local runtime is missing. Rebuild or reinstall @tuned-tensor/cli."
3469
3637
  );
3470
3638
  }
3639
+ return entrypoint;
3471
3640
  }
3472
3641
  function localCommandStreamsStdout(args2) {
3473
3642
  const serves = args2[0] === "serve" || args2[0] === "models" && args2[1] === "serve";
@@ -4527,7 +4696,7 @@ Eval Results (${data3._evals.length}):`);
4527
4696
  }
4528
4697
 
4529
4698
  // src/commands/datasets.ts
4530
- import { existsSync as existsSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
4699
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
4531
4700
  var DOCUMENT_OCR_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
4532
4701
  function formatBytes(bytes) {
4533
4702
  if (bytes < 1024) return `${bytes} B`;
@@ -4783,7 +4952,7 @@ function registerDatasetsCommands(parent) {
4783
4952
  });
4784
4953
  datasets.command("upload").description("Upload a JSONL dataset file").argument("<file>", "Path to JSONL file").option("-n, --name <name>", "Dataset name (defaults to filename)").option("-d, --description <desc>", "Dataset description").option("--format <format>", "Dataset format: jsonl or document_ocr_jsonl").action(async (file, cmdOpts) => {
4785
4954
  const opts = parent.opts();
4786
- if (!existsSync2(file)) {
4955
+ if (!existsSync3(file)) {
4787
4956
  printError(`File not found: ${file}`);
4788
4957
  process.exit(1);
4789
4958
  }
@@ -4834,12 +5003,12 @@ function registerDatasetsCommands(parent) {
4834
5003
  }
4835
5004
 
4836
5005
  // src/commands/label.ts
4837
- import { existsSync as existsSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
5006
+ import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
4838
5007
  function formatCents2(cents) {
4839
5008
  return `$${(cents / 100).toFixed(2)}`;
4840
5009
  }
4841
5010
  function sleep(ms) {
4842
- return new Promise((resolve7) => setTimeout(resolve7, ms));
5011
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
4843
5012
  }
4844
5013
  function parseCsvHeader(line) {
4845
5014
  const fields = [];
@@ -5006,7 +5175,7 @@ function registerLabelCommands(parent) {
5006
5175
  const label = parent.command("label").description("Teacher-label real data into training datasets");
5007
5176
  label.command("upload").description("Upload unlabeled inputs (.jsonl or .csv) and start a labeling job").argument("<file>", 'Path to JSONL ({"input": ...} per line) or CSV file').requiredOption("-s, --spec <id>", "Behaviour spec the teacher labels under").option("-c, --input-column <name>", "CSV column that holds the input text").option("-n, --name <name>", "Job name (defaults to filename)").option("--watch", "Block and show progress until labeling finishes").action(async (file, cmdOpts) => {
5008
5177
  const opts = parent.opts();
5009
- if (!existsSync3(file)) {
5178
+ if (!existsSync4(file)) {
5010
5179
  printError(`File not found: ${file}`);
5011
5180
  process.exit(1);
5012
5181
  }
@@ -5235,7 +5404,7 @@ function registerLabelCommands(parent) {
5235
5404
  import { spawn as spawn3, execFileSync } from "child_process";
5236
5405
  import { createHash } from "crypto";
5237
5406
  import {
5238
- existsSync as existsSync5,
5407
+ existsSync as existsSync6,
5239
5408
  mkdirSync as mkdirSync3,
5240
5409
  statSync as statSync4,
5241
5410
  createWriteStream,
@@ -5251,7 +5420,7 @@ import { Readable, Transform } from "stream";
5251
5420
  import { pipeline } from "stream/promises";
5252
5421
 
5253
5422
  // src/commands/init.ts
5254
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
5423
+ import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
5255
5424
  import { resolve as resolve4 } from "path";
5256
5425
  var DEFAULT_SPEC_FILE = "tunedtensor.json";
5257
5426
  var SCAFFOLD = {
@@ -5272,7 +5441,7 @@ var SCAFFOLD = {
5272
5441
  function registerInitCommand(parent) {
5273
5442
  parent.command("init").description("Create a behaviour spec project file").option("-n, --name <name>", "Spec name").option("--model <model>", "Base model ID").option("-f, --file <path>", "Output file path", DEFAULT_SPEC_FILE).action(async (cmdOpts) => {
5274
5443
  const filePath = resolve4(cmdOpts.file);
5275
- if (existsSync4(filePath)) {
5444
+ if (existsSync5(filePath)) {
5276
5445
  if (isJsonMode()) {
5277
5446
  printJson({ created: false, path: filePath });
5278
5447
  return;
@@ -5301,7 +5470,7 @@ function registerInitCommand(parent) {
5301
5470
  }
5302
5471
  function loadSpec(filePath) {
5303
5472
  const resolved = resolve4(filePath || DEFAULT_SPEC_FILE);
5304
- if (!existsSync4(resolved)) {
5473
+ if (!existsSync5(resolved)) {
5305
5474
  printError(
5306
5475
  `Spec file not found: ${filePath || DEFAULT_SPEC_FILE}
5307
5476
  Run \`tt init\` to create one.`
@@ -5324,13 +5493,13 @@ function parsePositiveInteger(value, fallback) {
5324
5493
  return Number.isInteger(value) && value >= 0 ? value : fallback;
5325
5494
  }
5326
5495
  function sleep2(ms) {
5327
- return new Promise((resolve7) => setTimeout(resolve7, ms));
5496
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
5328
5497
  }
5329
5498
  function readRequestBody(req) {
5330
- return new Promise((resolve7, reject) => {
5499
+ return new Promise((resolve8, reject) => {
5331
5500
  const chunks = [];
5332
5501
  req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
5333
- req.on("end", () => resolve7(Buffer.concat(chunks)));
5502
+ req.on("end", () => resolve8(Buffer.concat(chunks)));
5334
5503
  req.on("error", reject);
5335
5504
  });
5336
5505
  }
@@ -5398,7 +5567,7 @@ function errorSummary(response, fallback) {
5398
5567
  return typeof message === "string" ? message : fallback;
5399
5568
  }
5400
5569
  async function allocatePort(host) {
5401
- return new Promise((resolve7, reject) => {
5570
+ return new Promise((resolve8, reject) => {
5402
5571
  const server = createNetServer();
5403
5572
  server.on("error", reject);
5404
5573
  server.listen(0, host, () => {
@@ -5408,7 +5577,7 @@ async function allocatePort(host) {
5408
5577
  return;
5409
5578
  }
5410
5579
  const port = address.port;
5411
- server.close(() => resolve7(port));
5580
+ server.close(() => resolve8(port));
5412
5581
  });
5413
5582
  });
5414
5583
  }
@@ -5448,11 +5617,11 @@ var ManagedModelServer = class {
5448
5617
  sendJson(res, 500, { error: { message: err instanceof Error ? err.message : String(err) } });
5449
5618
  });
5450
5619
  });
5451
- await new Promise((resolve7, reject) => {
5620
+ await new Promise((resolve8, reject) => {
5452
5621
  this.server?.once("error", reject);
5453
5622
  this.server?.listen(this.options.publicPort, this.options.publicHost, () => {
5454
5623
  this.server?.off("error", reject);
5455
- resolve7();
5624
+ resolve8();
5456
5625
  });
5457
5626
  });
5458
5627
  }
@@ -5462,15 +5631,15 @@ var ManagedModelServer = class {
5462
5631
  if (!this.server) return;
5463
5632
  const server = this.server;
5464
5633
  this.server = null;
5465
- await new Promise((resolve7, reject) => {
5466
- server.close((err) => err ? reject(err) : resolve7());
5634
+ await new Promise((resolve8, reject) => {
5635
+ server.close((err) => err ? reject(err) : resolve8());
5467
5636
  });
5468
5637
  }
5469
5638
  async run() {
5470
5639
  await this.start();
5471
- await new Promise((resolve7, reject) => {
5640
+ await new Promise((resolve8, reject) => {
5472
5641
  this.server?.once("error", reject);
5473
- this.server?.once("close", resolve7);
5642
+ this.server?.once("close", resolve8);
5474
5643
  });
5475
5644
  }
5476
5645
  async handle(req, res) {
@@ -5644,7 +5813,7 @@ var ManagedModelServer = class {
5644
5813
  try {
5645
5814
  child.kill("SIGTERM");
5646
5815
  await Promise.race([
5647
- new Promise((resolve7) => child.once("exit", () => resolve7())),
5816
+ new Promise((resolve8) => child.once("exit", () => resolve8())),
5648
5817
  sleep2(5e3).then(() => {
5649
5818
  if (child.exitCode == null && child.signalCode == null) child.kill("SIGKILL");
5650
5819
  })
@@ -6269,7 +6438,7 @@ if __name__ == "__main__":
6269
6438
  `;
6270
6439
  function resolveOutputPath(output, filename) {
6271
6440
  if (!output) return filename;
6272
- if (existsSync5(output) && statSync4(output).isDirectory()) {
6441
+ if (existsSync6(output) && statSync4(output).isDirectory()) {
6273
6442
  return join6(output, filename);
6274
6443
  }
6275
6444
  return output;
@@ -6390,7 +6559,7 @@ function runtimePythonPath(cacheDir) {
6390
6559
  function resolveServePython(cacheDir, explicitPython) {
6391
6560
  if (explicitPython) return explicitPython;
6392
6561
  const managedPython = runtimePythonPath(cacheDir);
6393
- return existsSync5(managedPython) ? managedPython : "python3";
6562
+ return existsSync6(managedPython) ? managedPython : "python3";
6394
6563
  }
6395
6564
  function commandExists(command) {
6396
6565
  try {
@@ -6469,7 +6638,7 @@ function safeSegment(value) {
6469
6638
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "model";
6470
6639
  }
6471
6640
  function directoryHasFiles(path) {
6472
- return existsSync5(path) && statSync4(path).isDirectory() && readdirSync(path).length > 0;
6641
+ return existsSync6(path) && statSync4(path).isDirectory() && readdirSync(path).length > 0;
6473
6642
  }
6474
6643
  function validateArchiveEntryPath(entry) {
6475
6644
  const normalized = normalize(entry).replace(/^[\\/]+/, "");
@@ -6490,7 +6659,7 @@ function validateArchiveEntries(archivePath) {
6490
6659
  function extractArchive(archivePath, targetDir, force) {
6491
6660
  if (directoryHasFiles(targetDir) && !force) return targetDir;
6492
6661
  validateArchiveEntries(archivePath);
6493
- if (existsSync5(targetDir)) rmSync(targetDir, { recursive: true, force: true });
6662
+ if (existsSync6(targetDir)) rmSync(targetDir, { recursive: true, force: true });
6494
6663
  mkdirSync3(targetDir, { recursive: true });
6495
6664
  execFileSync("tar", ["-xzf", archivePath, "-C", targetDir], { stdio: "inherit" });
6496
6665
  return targetDir;
@@ -6517,7 +6686,7 @@ function findSpecPath(explicitPath, modelPath) {
6517
6686
  const candidates = explicitPath ? [explicitPath] : [join6(process.cwd(), DEFAULT_SPEC_FILE), join6(modelPath, DEFAULT_SPEC_FILE)];
6518
6687
  for (const candidate of candidates) {
6519
6688
  const resolved = resolve5(candidate);
6520
- if (existsSync5(resolved) && statSync4(resolved).isFile()) return resolved;
6689
+ if (existsSync6(resolved) && statSync4(resolved).isFile()) return resolved;
6521
6690
  }
6522
6691
  return null;
6523
6692
  }
@@ -6527,7 +6696,7 @@ function loadSystemPromptFromSpec(specPath) {
6527
6696
  }
6528
6697
  function loadJsonSchemaForServe(schemaPath) {
6529
6698
  const resolved = resolve5(schemaPath);
6530
- if (!existsSync5(resolved) || !statSync4(resolved).isFile()) {
6699
+ if (!existsSync6(resolved) || !statSync4(resolved).isFile()) {
6531
6700
  throw new Error(`JSON schema file not found: ${schemaPath}`);
6532
6701
  }
6533
6702
  try {
@@ -6545,7 +6714,7 @@ function writeReferenceServerScript(cacheDir) {
6545
6714
  return scriptPath;
6546
6715
  }
6547
6716
  async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
6548
- if (existsSync5(target)) {
6717
+ if (existsSync6(target)) {
6549
6718
  const resolved = resolve5(target);
6550
6719
  const stats = statSync4(resolved);
6551
6720
  if (stats.isDirectory()) {
@@ -6571,7 +6740,7 @@ async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
6571
6740
  const modelCacheDir = join6(cacheDir, fullId);
6572
6741
  const archivePath = join6(modelCacheDir, data3.filename);
6573
6742
  const extractDir = join6(modelCacheDir, "model");
6574
- if (!existsSync5(archivePath) || forceDownload) {
6743
+ if (!existsSync6(archivePath) || forceDownload) {
6575
6744
  await downloadUrlToFile(data3.url, archivePath);
6576
6745
  }
6577
6746
  extractArchive(archivePath, extractDir, forceDownload);
@@ -6675,7 +6844,7 @@ function ollamaSlug(name) {
6675
6844
  }
6676
6845
  function firstExisting(candidates) {
6677
6846
  for (const candidate of candidates) {
6678
- if (candidate && existsSync5(candidate) && statSync4(candidate).isFile()) {
6847
+ if (candidate && existsSync6(candidate) && statSync4(candidate).isFile()) {
6679
6848
  return resolve5(candidate);
6680
6849
  }
6681
6850
  }
@@ -6687,7 +6856,7 @@ function llamaCppDir(explicit) {
6687
6856
  function resolveConvertScript(opts, required) {
6688
6857
  if (opts.convertScript) {
6689
6858
  const resolved = resolve5(opts.convertScript);
6690
- if (required && !existsSync5(resolved)) {
6859
+ if (required && !existsSync6(resolved)) {
6691
6860
  throw new Error(`Conversion script not found: ${opts.convertScript}`);
6692
6861
  }
6693
6862
  return resolved;
@@ -6706,7 +6875,7 @@ function resolveConvertScript(opts, required) {
6706
6875
  function resolveQuantizeBin(opts, required) {
6707
6876
  if (opts.quantizeBin) {
6708
6877
  const resolved = resolve5(opts.quantizeBin);
6709
- if (required && !existsSync5(resolved)) {
6878
+ if (required && !existsSync6(resolved)) {
6710
6879
  throw new Error(`llama-quantize binary not found: ${opts.quantizeBin}`);
6711
6880
  }
6712
6881
  return resolved;
@@ -6841,7 +7010,7 @@ function registerModelsCommands(parent) {
6841
7010
  opts
6842
7011
  );
6843
7012
  const outputPath = resolveOutputPath(cmdOpts.output, data3.filename);
6844
- if (existsSync5(outputPath) && !cmdOpts.force) {
7013
+ if (existsSync6(outputPath) && !cmdOpts.force) {
6845
7014
  throw new Error(`Output file already exists: ${outputPath}. Use --force to overwrite.`);
6846
7015
  }
6847
7016
  const bytes = await downloadUrlToFile(data3.url, outputPath);
@@ -6950,7 +7119,7 @@ function registerModelsCommands(parent) {
6950
7119
  ollama: ollamaInfo
6951
7120
  };
6952
7121
  if (printOnly) return printExportPlan(plan);
6953
- if (existsSync5(finalPath) && !cmdOpts.force) {
7122
+ if (existsSync6(finalPath) && !cmdOpts.force) {
6954
7123
  throw new Error(`Output already exists: ${finalPath}. Use --force to overwrite.`);
6955
7124
  }
6956
7125
  mkdirSync3(outputDir, { recursive: true });
@@ -6971,7 +7140,7 @@ function registerModelsCommands(parent) {
6971
7140
  finalPath,
6972
7141
  quantPlan.quantizeType
6973
7142
  ]);
6974
- if (!cmdOpts.keepIntermediate && intermediatePath && existsSync5(intermediatePath)) {
7143
+ if (!cmdOpts.keepIntermediate && intermediatePath && existsSync6(intermediatePath)) {
6975
7144
  rmSync(intermediatePath, { force: true });
6976
7145
  }
6977
7146
  }
@@ -7024,10 +7193,10 @@ function registerModelsCommands(parent) {
7024
7193
  ]);
7025
7194
  return;
7026
7195
  }
7027
- if (existsSync5(venvDir) && cmdOpts.force) {
7196
+ if (existsSync6(venvDir) && cmdOpts.force) {
7028
7197
  rmSync(venvDir, { recursive: true, force: true });
7029
7198
  }
7030
- if (!existsSync5(venvPython)) {
7199
+ if (!existsSync6(venvPython)) {
7031
7200
  mkdirSync3(cacheDir, { recursive: true });
7032
7201
  execFileSync(commands[0][0], commands[0].slice(1), { stdio: "inherit" });
7033
7202
  }
@@ -7639,6 +7808,81 @@ function registerAgentCommands(parent, options) {
7639
7808
  });
7640
7809
  }
7641
7810
 
7811
+ // src/commands/pipeline.ts
7812
+ import { existsSync as existsSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
7813
+ import { resolve as resolve7 } from "path";
7814
+ var DEFAULT_PIPELINE_FILE = "tunedtensor.pipeline.json";
7815
+ function parseList(value) {
7816
+ if (!value) return void 0;
7817
+ const ids = value.split(",").map((id) => id.trim()).filter(Boolean);
7818
+ if (!ids.length) throw new Error("Step selection must name at least one step.");
7819
+ return ids;
7820
+ }
7821
+ function loadPipeline(path) {
7822
+ const resolved = resolve7(path);
7823
+ if (!existsSync7(resolved)) throw new Error(`Pipeline file not found: ${path}. Run \`tt pipeline init\` to create one.`);
7824
+ try {
7825
+ return JSON.parse(readFileSync9(resolved, "utf8"));
7826
+ } catch (error) {
7827
+ throw new Error(`Pipeline file must be JSON: ${error.message}`);
7828
+ }
7829
+ }
7830
+ function outputPlan(plan) {
7831
+ if (isJsonMode()) return printJson(plan);
7832
+ const typed = plan;
7833
+ for (const step of typed.steps) {
7834
+ console.log(`${step.id.padEnd(16)} ${step.uses.padEnd(10)} ${step.target}`);
7835
+ for (const transfer of step.transfers) console.log(` transfer ${transfer.from}: ${transfer.from_target} -> ${transfer.to_target}`);
7836
+ }
7837
+ }
7838
+ function registerPipelineCommands(parent) {
7839
+ const pipeline2 = parent.command("pipeline").description("Create, validate, inspect, and safely dry-run ordered pipeline recipes");
7840
+ pipeline2.command("init").description("Write a canonical v1 pipeline recipe").option("-f, --file <path>", "Output file", DEFAULT_PIPELINE_FILE).option("--target <target>", "Default target: local or cloud", "local").action((options) => {
7841
+ if (!PIPELINE_TARGETS.includes(options.target)) throw new Error("--target must be local or cloud.");
7842
+ const path = resolve7(options.file);
7843
+ if (existsSync7(path)) throw new Error(`Pipeline file already exists: ${options.file}.`);
7844
+ const recipe = canonicalPipeline(options.target);
7845
+ writeFileSync5(path, `${JSON.stringify(recipe, null, 2)}
7846
+ `);
7847
+ if (isJsonMode()) return printJson({ created: true, path, pipeline: recipe });
7848
+ printSuccess(`Created ${options.file}`);
7849
+ });
7850
+ pipeline2.command("validate").description("Validate a pipeline without any execution or transfer").option("-f, --file <path>", "Pipeline file", DEFAULT_PIPELINE_FILE).action((options) => {
7851
+ const errors = validatePipeline(loadPipeline(options.file));
7852
+ const result2 = { valid: errors.length === 0, errors };
7853
+ if (isJsonMode()) return printJson(result2);
7854
+ if (errors.length) throw new Error(`Invalid pipeline:
7855
+ - ${errors.join("\n- ")}`);
7856
+ printSuccess("Pipeline is valid.");
7857
+ });
7858
+ pipeline2.command("plan").description("Resolve step targets and required artifact transfers").option("-f, --file <path>", "Pipeline file", DEFAULT_PIPELINE_FILE).option("--only <ids>", "Comma-separated step IDs to include").option("--skip <ids>", "Comma-separated step IDs to omit").action((options) => {
7859
+ outputPlan(createExecutionPlan(loadPipeline(options.file), { only: parseList(options.only), skip: parseList(options.skip) }));
7860
+ });
7861
+ pipeline2.command("run").description("Run an ordered local pipeline, or safely preview any pipeline").option("--dry-run", "Resolve and display only; never execute, transfer, or reserve credits").option("-f, --file <path>", "Pipeline file", DEFAULT_PIPELINE_FILE).option("--spec <path>", "Local behavior spec", "tunedtensor.json").option("--config <path>", "Local runtime config").option("--only <ids>", "Comma-separated step IDs to include").option("--skip <ids>", "Comma-separated step IDs to omit").action(async (options) => {
7862
+ const plan = createExecutionPlan(loadPipeline(options.file), { only: parseList(options.only), skip: parseList(options.skip) });
7863
+ if (options.dryRun) {
7864
+ if (isJsonMode()) return printJson({ dry_run: true, ...plan });
7865
+ console.log("Dry run only \u2014 no execution, artifact transfer, or credit reservation will occur.");
7866
+ return outputPlan(plan);
7867
+ }
7868
+ const remote = plan.steps.find((step) => step.target !== "local");
7869
+ if (remote) {
7870
+ throw new Error(`Step "${remote.id}" targets cloud execution. Hosted and mixed pipeline execution is not enabled by this CLI build; use --dry-run.`);
7871
+ }
7872
+ const input = await loadLocalRunInput(resolve7(options.spec));
7873
+ assertLocalRunInputReady(input.request);
7874
+ const config = await loadLocalRunnerConfig(options.config ? resolve7(options.config) : void 0);
7875
+ const localPipeline = {
7876
+ version: 1,
7877
+ ...plan.name ? { name: plan.name } : {},
7878
+ steps: plan.steps.map(({ transfers: _transfers, ...step }) => step)
7879
+ };
7880
+ const result2 = await runLocalPipeline({ request: input.request, config, pipeline: localPipeline });
7881
+ if (isJsonMode()) return printJson(result2);
7882
+ printSuccess(`Pipeline completed with status ${result2.status}.`);
7883
+ });
7884
+ }
7885
+
7642
7886
  // src/cli.ts
7643
7887
  async function createDefaultAgentClient(runtime, env, cloud) {
7644
7888
  const selection = getAgentSelection(env);
@@ -7681,7 +7925,16 @@ async function createDefaultAgentClient(runtime, env, cloud) {
7681
7925
  }
7682
7926
  function createLazyDefaultAgentClient(runtime, env, cloud) {
7683
7927
  let pending;
7684
- const client = () => pending ??= createDefaultAgentClient(runtime, env, cloud);
7928
+ const client = () => {
7929
+ if (!pending) {
7930
+ const attempt = createDefaultAgentClient(runtime, env, cloud);
7931
+ pending = attempt;
7932
+ attempt.catch(() => {
7933
+ if (pending === attempt) pending = void 0;
7934
+ });
7935
+ }
7936
+ return pending;
7937
+ };
7685
7938
  return {
7686
7939
  createThread: async () => await (await client()).createThread(),
7687
7940
  listThreads: async () => await (await client()).listThreads(),
@@ -7696,7 +7949,7 @@ async function runSelfCommand(args2, options = {}) {
7696
7949
  if (!entrypoint) {
7697
7950
  throw new Error("Cannot locate the tt CLI entrypoint.");
7698
7951
  }
7699
- return await new Promise((resolve7, reject) => {
7952
+ return await new Promise((resolve8, reject) => {
7700
7953
  const child = spawn4(process.execPath, [entrypoint, ...args2], {
7701
7954
  cwd: options.cwd,
7702
7955
  env: options.env ?? process.env,
@@ -7732,7 +7985,7 @@ async function runSelfCommand(args2, options = {}) {
7732
7985
  child.once("close", (exitCode, signal) => {
7733
7986
  cleanup();
7734
7987
  const signalNumber = signal ? osConstants2.signals[signal] : void 0;
7735
- resolve7({
7988
+ resolve8({
7736
7989
  exitCode: exitCode ?? (typeof signalNumber === "number" ? 128 + signalNumber : 1),
7737
7990
  signal
7738
7991
  });
@@ -7836,6 +8089,7 @@ function createProgram(version, runtime = {}) {
7836
8089
  registerInitCommand(program);
7837
8090
  registerEvalCommand(program);
7838
8091
  registerPushCommand(program);
8092
+ registerPipelineCommands(program);
7839
8093
  registerAgentCommands(program, {
7840
8094
  env,
7841
8095
  output: runtime.stdout ?? process.stdout,
@@ -7993,7 +8247,7 @@ async function runCli(version, runtime = {}) {
7993
8247
  }
7994
8248
 
7995
8249
  // src/index.ts
7996
- runCli("0.9.0").catch((err) => {
8250
+ runCli("0.9.1").catch((err) => {
7997
8251
  reportError(err);
7998
8252
  process.exit(1);
7999
8253
  });