@tuned-tensor/cli 0.4.24 → 0.5.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/index.js CHANGED
@@ -1,7 +1,162 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
3
+ // src/cli.ts
4
+ import { spawn as spawn4 } from "child_process";
5
+ import { constants as osConstants2 } from "os";
4
6
  import { Command } from "commander";
7
+ import chalk7 from "chalk";
8
+
9
+ // src/local-runner.ts
10
+ import { constants as osConstants } from "os";
11
+ import { randomUUID } from "crypto";
12
+ import { createRequire } from "module";
13
+ import { spawn } from "child_process";
14
+ import {
15
+ readFile,
16
+ rm,
17
+ stat,
18
+ writeFile
19
+ } from "fs/promises";
20
+ import {
21
+ basename as basename2,
22
+ dirname,
23
+ isAbsolute,
24
+ join as join2,
25
+ resolve
26
+ } from "path";
27
+
28
+ // src/base-models.ts
29
+ var SUPPORTED_BASE_MODELS = [
30
+ "google/gemma-4-E2B-it",
31
+ "google/gemma-4-E4B-it",
32
+ "Qwen/Qwen3.5-2B",
33
+ "Qwen/Qwen3-VL-2B-Instruct",
34
+ "Qwen/Qwen3.5-4B",
35
+ "meta-llama/Llama-3.2-3B-Instruct",
36
+ "microsoft/Phi-4-mini-instruct",
37
+ "ibm-granite/granite-3.3-2b-instruct",
38
+ "bigcode/starcoder2-3b"
39
+ ];
40
+ var BASE_MODEL_ALIASES = /* @__PURE__ */ new Map();
41
+ for (const model of SUPPORTED_BASE_MODELS) {
42
+ BASE_MODEL_ALIASES.set(model.toLowerCase(), model);
43
+ }
44
+ for (const [alias, model] of [
45
+ ["google/gemma-4-E2B", "google/gemma-4-E2B-it"],
46
+ ["google/gemma-4-e2b", "google/gemma-4-E2B-it"],
47
+ ["google/gemma-4-e2b-it", "google/gemma-4-E2B-it"],
48
+ ["google/gemma-4-E4B", "google/gemma-4-E4B-it"],
49
+ ["google/gemma-4-e4b", "google/gemma-4-E4B-it"],
50
+ ["google/gemma-4-e4b-it", "google/gemma-4-E4B-it"],
51
+ ["qwen/qwen3.5-2b", "Qwen/Qwen3.5-2B"],
52
+ ["Qwen/Qwen3.5-2B-Base", "Qwen/Qwen3.5-2B"],
53
+ ["qwen/qwen3.5-2b-base", "Qwen/Qwen3.5-2B"],
54
+ ["qwen/qwen3-vl-2b-instruct", "Qwen/Qwen3-VL-2B-Instruct"],
55
+ ["Qwen/Qwen3-VL-2B", "Qwen/Qwen3-VL-2B-Instruct"],
56
+ ["qwen/qwen3-vl-2b", "Qwen/Qwen3-VL-2B-Instruct"],
57
+ ["qwen/qwen3.5-4b", "Qwen/Qwen3.5-4B"],
58
+ ["Qwen/Qwen3.5-4B-Base", "Qwen/Qwen3.5-4B"],
59
+ ["qwen/qwen3.5-4b-base", "Qwen/Qwen3.5-4B"],
60
+ ["meta-llama/llama-3.2-3b-instruct", "meta-llama/Llama-3.2-3B-Instruct"],
61
+ ["meta-llama/Llama-3.2-3B", "meta-llama/Llama-3.2-3B-Instruct"],
62
+ ["meta-llama/llama-3.2-3b", "meta-llama/Llama-3.2-3B-Instruct"],
63
+ ["microsoft/phi-4-mini-instruct", "microsoft/Phi-4-mini-instruct"],
64
+ ["phi-4-mini-instruct", "microsoft/Phi-4-mini-instruct"],
65
+ ["ibm-granite/granite-3.3-2b-instruct", "ibm-granite/granite-3.3-2b-instruct"],
66
+ ["granite-3.3-2b-instruct", "ibm-granite/granite-3.3-2b-instruct"],
67
+ ["bigcode/starcoder2-3b", "bigcode/starcoder2-3b"],
68
+ ["starcoder2-3b", "bigcode/starcoder2-3b"]
69
+ ]) {
70
+ BASE_MODEL_ALIASES.set(alias.toLowerCase(), model);
71
+ }
72
+ var UnsupportedBaseModelError = class extends Error {
73
+ constructor(model) {
74
+ const supported = SUPPORTED_BASE_MODELS.join(", ");
75
+ super(
76
+ `Unsupported base_model "${String(model)}". Supported base models: ${supported}`
77
+ );
78
+ this.name = "UnsupportedBaseModelError";
79
+ }
80
+ };
81
+ function canonicalizeBaseModel(model) {
82
+ if (typeof model !== "string" || !model.trim()) {
83
+ throw new UnsupportedBaseModelError(model);
84
+ }
85
+ const canonical = BASE_MODEL_ALIASES.get(model.trim().toLowerCase());
86
+ if (!canonical) {
87
+ throw new UnsupportedBaseModelError(model);
88
+ }
89
+ return canonical;
90
+ }
91
+ function canonicalizeSpecBaseModel(body) {
92
+ if ("base_model" in body && body.base_model !== void 0) {
93
+ return { ...body, base_model: canonicalizeBaseModel(body.base_model) };
94
+ }
95
+ return body;
96
+ }
97
+
98
+ // src/project-spec.ts
99
+ var CLOUD_SPEC_KEYS = [
100
+ "name",
101
+ "description",
102
+ "base_model",
103
+ "system_prompt",
104
+ "guidelines",
105
+ "constraints",
106
+ "examples",
107
+ "eval_cases"
108
+ ];
109
+ var LOCAL_ONLY_SPEC_KEYS = [
110
+ "hyperparameters",
111
+ "dataset_prebuilt"
112
+ ];
113
+ var LOCAL_SPEC_KEYS = [
114
+ "id",
115
+ "name",
116
+ "description",
117
+ "system_prompt",
118
+ "guidelines",
119
+ "constraints",
120
+ "base_model",
121
+ "examples",
122
+ ...LOCAL_ONLY_SPEC_KEYS
123
+ ];
124
+ var CLOUD_SPEC_KEY_SET = new Set(CLOUD_SPEC_KEYS);
125
+ var LOCAL_ONLY_SPEC_KEY_SET = new Set(LOCAL_ONLY_SPEC_KEYS);
126
+ var LOCAL_SPEC_KEY_SET = new Set(LOCAL_SPEC_KEYS);
127
+ var KNOWN_PROJECT_SPEC_KEY_SET = /* @__PURE__ */ new Set([
128
+ ...CLOUD_SPEC_KEYS,
129
+ ...LOCAL_SPEC_KEYS
130
+ ]);
131
+ function projectSpec(raw, supportedKeys) {
132
+ const body = {};
133
+ const droppedKeys = [];
134
+ for (const [key, value] of Object.entries(raw)) {
135
+ if (supportedKeys.has(key)) {
136
+ body[key] = value;
137
+ } else {
138
+ droppedKeys.push(key);
139
+ }
140
+ }
141
+ return {
142
+ body: canonicalizeSpecBaseModel(body),
143
+ droppedKeys
144
+ };
145
+ }
146
+ function projectCloudSpec(raw) {
147
+ return projectSpec(raw, CLOUD_SPEC_KEY_SET);
148
+ }
149
+ function projectLocalSpec(raw) {
150
+ return projectSpec(raw, LOCAL_SPEC_KEY_SET);
151
+ }
152
+ function hasLocalOnlySpecFields(raw) {
153
+ return Object.keys(raw).some((key) => LOCAL_ONLY_SPEC_KEY_SET.has(key));
154
+ }
155
+ function unknownProjectSpecKeys(raw) {
156
+ return Object.keys(raw).filter(
157
+ (key) => !KNOWN_PROJECT_SPEC_KEY_SET.has(key)
158
+ );
159
+ }
5
160
 
6
161
  // src/output.ts
7
162
  import chalk from "chalk";
@@ -12,7 +167,13 @@ import { readFileSync as readFileSync2, statSync } from "fs";
12
167
  import { basename } from "path";
13
168
 
14
169
  // src/config.ts
15
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
170
+ import {
171
+ chmodSync,
172
+ existsSync,
173
+ mkdirSync,
174
+ readFileSync,
175
+ writeFileSync
176
+ } from "fs";
16
177
  import { homedir } from "os";
17
178
  import { join } from "path";
18
179
  var CONFIG_DIR_NAME = "tuned-tensor";
@@ -36,8 +197,13 @@ function readConfig() {
36
197
  }
37
198
  function writeConfig(config) {
38
199
  const dir = getConfigDir();
39
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
40
- writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n");
200
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
201
+ chmodSync(dir, 448);
202
+ const path = getConfigPath();
203
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", {
204
+ mode: 384
205
+ });
206
+ chmodSync(path, 384);
41
207
  }
42
208
  function updateConfig(partial) {
43
209
  const current = readConfig();
@@ -45,7 +211,7 @@ function updateConfig(partial) {
45
211
  }
46
212
  function clearConfig() {
47
213
  const path = getConfigPath();
48
- if (existsSync(path)) writeFileSync(path, "{}\n");
214
+ if (existsSync(path)) writeConfig({});
49
215
  }
50
216
  var DEFAULT_BASE_URL = "https://tunedtensor.com";
51
217
  function getBaseUrl(opts) {
@@ -57,9 +223,9 @@ function getApiKey(opts) {
57
223
 
58
224
  // src/client.ts
59
225
  var ApiError = class extends Error {
60
- constructor(status, code, message) {
226
+ constructor(status2, code, message) {
61
227
  super(message);
62
- this.status = status;
228
+ this.status = status2;
63
229
  this.code = code;
64
230
  this.name = "ApiError";
65
231
  }
@@ -140,12 +306,12 @@ function isJsonMode() {
140
306
  function printJson(data) {
141
307
  console.log(JSON.stringify(data, null, 2));
142
308
  }
143
- function printTable(headers, rows, meta) {
309
+ function printTable(headers, rows2, meta) {
144
310
  const table = new Table({
145
311
  head: headers.map((h) => chalk.bold.cyan(h)),
146
312
  style: { head: [], border: [] }
147
313
  });
148
- for (const row of rows) {
314
+ for (const row of rows2) {
149
315
  table.push(row);
150
316
  }
151
317
  console.log(table.toString());
@@ -158,103 +324,1782 @@ Page ${meta.page}/${totalPages} (${meta.total} total)`
158
324
  )
159
325
  );
160
326
  }
161
- }
162
- function printDetail(fields) {
163
- const maxLabel = Math.max(...fields.map(([l]) => l.length));
164
- for (const [label, value] of fields) {
165
- const paddedLabel = label.padEnd(maxLabel);
166
- console.log(`${chalk.bold.cyan(paddedLabel)} ${value ?? chalk.dim("\u2014")}`);
327
+ }
328
+ function printDetail(fields) {
329
+ const maxLabel = Math.max(...fields.map(([l]) => l.length));
330
+ for (const [label, value] of fields) {
331
+ const paddedLabel = label.padEnd(maxLabel);
332
+ console.log(`${chalk.bold.cyan(paddedLabel)} ${value ?? chalk.dim("\u2014")}`);
333
+ }
334
+ }
335
+ function printSuccess(message) {
336
+ console.log(chalk.green("\u2713") + " " + message);
337
+ }
338
+ function printWarning(message) {
339
+ console.log(chalk.yellow("!") + " " + message);
340
+ }
341
+ function printError(message) {
342
+ console.error(chalk.red("\u2717") + " " + message);
343
+ }
344
+ function reportError(err) {
345
+ const isApi = err instanceof ApiError;
346
+ const message = err?.message || String(err);
347
+ if (jsonMode) {
348
+ printJson({
349
+ error: {
350
+ status: isApi ? err.status : null,
351
+ code: isApi ? err.code : "CLI_ERROR",
352
+ message
353
+ }
354
+ });
355
+ } else if (isApi) {
356
+ printError(`[${err.status}] ${message}`);
357
+ } else {
358
+ printError(message);
359
+ }
360
+ }
361
+ function formatDate(iso) {
362
+ if (!iso) return "\u2014";
363
+ return new Date(iso).toLocaleString();
364
+ }
365
+ function formatStatus(status2) {
366
+ const map = {
367
+ completed: chalk.green,
368
+ running: chalk.blue,
369
+ training: chalk.blue,
370
+ evaluating: chalk.blue,
371
+ preparing: chalk.yellow,
372
+ pending: chalk.yellow,
373
+ uploading: chalk.yellow,
374
+ failed: chalk.red,
375
+ cancelled: chalk.dim,
376
+ validated: chalk.green,
377
+ invalid: chalk.red
378
+ };
379
+ const colorFn = map[status2] || chalk.white;
380
+ return colorFn(status2);
381
+ }
382
+ function truncate(str, max) {
383
+ if (str.length <= max) return str;
384
+ return str.slice(0, max - 1) + "\u2026";
385
+ }
386
+ function shortId(id) {
387
+ return id.slice(0, 8);
388
+ }
389
+
390
+ // src/local-output.ts
391
+ function isRecord(value) {
392
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
393
+ }
394
+ function text(value) {
395
+ if (typeof value === "string") return value;
396
+ if (typeof value === "number" || typeof value === "boolean") {
397
+ return String(value);
398
+ }
399
+ return void 0;
400
+ }
401
+ function number(value) {
402
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
403
+ }
404
+ function record(value) {
405
+ return isRecord(value) ? value : void 0;
406
+ }
407
+ function rows(value) {
408
+ return Array.isArray(value) ? value.filter(isRecord) : [];
409
+ }
410
+ function displayId(value) {
411
+ const id = text(value);
412
+ return id ? shortId(id) : "\u2014";
413
+ }
414
+ function displayNumber(value, digits = 4) {
415
+ const parsed = number(value);
416
+ if (parsed === void 0) return void 0;
417
+ return parsed.toFixed(digits).replace(/\.?0+$/, "");
418
+ }
419
+ function displayBytes(value) {
420
+ const bytes = number(value);
421
+ if (bytes === void 0 || bytes < 0) return void 0;
422
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
423
+ let amount = bytes;
424
+ let unit = 0;
425
+ while (amount >= 1024 && unit < units.length - 1) {
426
+ amount /= 1024;
427
+ unit += 1;
428
+ }
429
+ const precision = amount >= 10 || unit === 0 ? 0 : 1;
430
+ return `${amount.toFixed(precision)} ${units[unit]}`;
431
+ }
432
+ function displayCommand(value) {
433
+ if (!Array.isArray(value) || !value.every((part) => typeof part === "string")) {
434
+ return void 0;
435
+ }
436
+ return value.map(
437
+ (part) => /[\s"'\\]/.test(part) ? JSON.stringify(part) : part
438
+ ).join(" ");
439
+ }
440
+ function status(value) {
441
+ const parsed = text(value) ?? "unknown";
442
+ return formatStatus(parsed);
443
+ }
444
+ function gateFrom(value) {
445
+ return record(record(value)?.general_regression);
446
+ }
447
+ function renderGate(value) {
448
+ const gate = gateFrom(value);
449
+ if (!gate) {
450
+ printWarning(
451
+ "General regression was not run; this model cannot be activated yet."
452
+ );
453
+ return;
454
+ }
455
+ if (gate.passed === true) {
456
+ printSuccess("General regression gate passed");
457
+ return;
458
+ }
459
+ printWarning(
460
+ "The local run completed, but the general regression gate failed."
461
+ );
462
+ const failures = Array.isArray(gate.failures) ? gate.failures.filter(
463
+ (failure) => typeof failure === "string"
464
+ ) : [];
465
+ for (const failure of failures) printWarning(failure);
466
+ }
467
+ function renderDoctor(value) {
468
+ const checks = rows(value.checks);
469
+ printTable(
470
+ ["Check", "Status", "Details"],
471
+ checks.map((check) => [
472
+ text(check.name) ?? "unknown",
473
+ check.ok === true ? formatStatus("completed") : formatStatus("failed"),
474
+ truncate(text(check.message) ?? text(check.detail) ?? "\u2014", 100)
475
+ ])
476
+ );
477
+ if (value.ok === true) {
478
+ printSuccess("Local environment is ready");
479
+ } else {
480
+ printError("Local environment needs attention");
481
+ }
482
+ if (value.config_path) {
483
+ printDetail([["Config", text(value.config_path)]]);
484
+ }
485
+ }
486
+ function renderInit(value) {
487
+ printSuccess("Created local Tuned Tensor project");
488
+ printDetail([
489
+ ["Path", text(value.path)],
490
+ ["Name", text(value.name)],
491
+ ["ID", text(value.id)],
492
+ ["Base model", text(value.base_model)],
493
+ ["Config", text(value.config_path)]
494
+ ]);
495
+ }
496
+ function renderValidate(value) {
497
+ printSuccess("Local behavior spec is valid");
498
+ printDetail([
499
+ ["Input", text(value.input_path)],
500
+ ["Config", text(value.config_path)],
501
+ ["Spec", text(value.behavior_spec_id)],
502
+ ["Base model", text(value.base_model)],
503
+ ["Dataset", text(value.dataset_format)],
504
+ ["Artifacts", text(value.artifact_root)],
505
+ ["Store", text(value.store_root)],
506
+ ["Dry run", text(value.dry_run)]
507
+ ]);
508
+ }
509
+ function renderRun(value) {
510
+ const runStatus = text(value.status) ?? "unknown";
511
+ if (runStatus === "completed") {
512
+ printSuccess("Local run completed");
513
+ } else {
514
+ printWarning(`Local run finished with status ${runStatus}`);
515
+ }
516
+ const comparison = record(value.comparison);
517
+ printDetail([
518
+ ["Run", text(value.run_id)],
519
+ ["Status", status(runStatus)],
520
+ ["Model", text(value.model_id) ?? text(value.fine_tuned_model_id)],
521
+ ["Report", text(value.report_path)],
522
+ ["Artifacts", text(value.artifact_dir)],
523
+ ["Score delta", displayNumber(comparison?.avg_score_delta)],
524
+ ["Pass-rate delta", displayNumber(comparison?.pass_rate_delta)]
525
+ ]);
526
+ renderGate(value);
527
+ }
528
+ function renderRunsList(value) {
529
+ const runRows = rows(value);
530
+ if (runRows.length === 0) {
531
+ printWarning("No local runs found.");
532
+ return;
533
+ }
534
+ printTable(
535
+ ["Run", "Spec", "Status", "Stage", "Model", "Updated"],
536
+ runRows.map((run) => [
537
+ displayId(run.id),
538
+ truncate(text(run.spec_name) ?? displayId(run.behavior_spec_id), 28),
539
+ status(run.status),
540
+ text(run.current_stage) ?? "\u2014",
541
+ displayId(run.model_id),
542
+ formatDate(text(run.updated_at))
543
+ ])
544
+ );
545
+ }
546
+ function renderRunEvents(value) {
547
+ const eventRows = rows(value);
548
+ if (eventRows.length === 0) {
549
+ printWarning("No local run events found.");
550
+ return;
551
+ }
552
+ printTable(
553
+ ["Time", "Stage", "Status", "Message"],
554
+ eventRows.map((event) => [
555
+ formatDate(text(event.occurred_at)),
556
+ text(event.stage) ?? "\u2014",
557
+ status(event.status),
558
+ truncate(text(event.message) ?? "\u2014", 80)
559
+ ])
560
+ );
561
+ }
562
+ function renderRunRecord(value) {
563
+ printDetail([
564
+ ["Run", text(value.id) ?? text(value.run_id)],
565
+ ["Spec", text(value.spec_name) ?? text(value.behavior_spec_id)],
566
+ ["Status", status(value.status)],
567
+ ["Stage", text(value.current_stage)],
568
+ ["Model", text(value.model_id) ?? text(value.fine_tuned_model_id)],
569
+ ["Base model", text(value.base_model)],
570
+ ["Report", text(value.report_path)],
571
+ ["Artifacts", text(value.artifact_dir)],
572
+ ["Updated", formatDate(text(value.updated_at) ?? text(value.created_at))]
573
+ ]);
574
+ }
575
+ function renderRunReport(value) {
576
+ const baseline = record(value.baseline);
577
+ const candidate = record(value.candidate);
578
+ const comparison = record(value.comparison);
579
+ const metadata = record(value.run_metadata);
580
+ printDetail([
581
+ ["Run", text(value.run_id)],
582
+ ["Status", status(value.status)],
583
+ ["Base model", text(value.base_model)],
584
+ ["Tuned model", text(value.fine_tuned_model_id)],
585
+ ["Training examples", text(metadata?.training_example_count)],
586
+ ["Evaluation examples", text(metadata?.eval_examples_used)],
587
+ ["Baseline score", displayNumber(baseline?.avg_score)],
588
+ ["Candidate score", displayNumber(candidate?.avg_score)],
589
+ ["Score delta", displayNumber(comparison?.avg_score_delta)],
590
+ ["Pass-rate delta", displayNumber(comparison?.pass_rate_delta)],
591
+ ["Regressions", text(comparison?.regressions)],
592
+ ["Improvements", text(comparison?.improvements)]
593
+ ]);
594
+ renderGate(value);
595
+ }
596
+ function renderRunComparison(value) {
597
+ const runA = record(value.run_a);
598
+ const runB = record(value.run_b);
599
+ const shared = record(value.shared);
600
+ const sharedA = record(shared?.run_a);
601
+ const sharedB = record(shared?.run_b);
602
+ printTable(
603
+ ["Run", "Candidate score", "Token F1", "Examples"],
604
+ [
605
+ [
606
+ displayId(runA?.run_id),
607
+ displayNumber(sharedA?.candidate_avg_score) ?? "\u2014",
608
+ displayNumber(sharedA?.candidate_avg_token_f1) ?? "\u2014",
609
+ text(shared?.examples) ?? "0"
610
+ ],
611
+ [
612
+ displayId(runB?.run_id),
613
+ displayNumber(sharedB?.candidate_avg_score) ?? "\u2014",
614
+ displayNumber(sharedB?.candidate_avg_token_f1) ?? "\u2014",
615
+ text(shared?.examples) ?? "0"
616
+ ]
617
+ ]
618
+ );
619
+ const notes = Array.isArray(value.notes) ? value.notes.filter((note) => typeof note === "string") : [];
620
+ for (const note of notes) printWarning(note);
621
+ }
622
+ function renderModelsList(value) {
623
+ const modelRows = rows(value);
624
+ if (modelRows.length === 0) {
625
+ printWarning("No local models found.");
626
+ return;
627
+ }
628
+ printTable(
629
+ ["Model", "Base model", "Run", "Provider", "Created"],
630
+ modelRows.map((model) => [
631
+ displayId(model.id),
632
+ truncate(text(model.base_model) ?? "\u2014", 32),
633
+ displayId(model.run_id),
634
+ text(model.provider) ?? "\u2014",
635
+ formatDate(text(model.created_at))
636
+ ])
637
+ );
638
+ }
639
+ function renderModelRecord(value) {
640
+ const model = record(value.model) ?? value;
641
+ const artifact = record(value.artifact);
642
+ const integrity = record(value.integrity);
643
+ printDetail([
644
+ ["Model", text(model.id) ?? text(value.active)],
645
+ ["Base model", text(model.base_model)],
646
+ ["Run", text(model.run_id)],
647
+ ["Provider", text(model.provider)],
648
+ ["Artifact", text(model.artifact_uri) ?? text(artifact?.path)],
649
+ ["Manifest", text(value.manifest_path)],
650
+ ["Verified files", text(integrity?.checked)],
651
+ ["Created", formatDate(text(model.created_at))]
652
+ ]);
653
+ }
654
+ function renderModelPrefetch(value) {
655
+ printSuccess(
656
+ value.local_files_only === true ? "Verified the local base-model snapshot" : "Base model is ready locally"
657
+ );
658
+ printDetail([
659
+ ["Base model", text(value.base_model)],
660
+ ["Revision", text(value.snapshot_revision)],
661
+ ["Snapshot", text(value.snapshot_path)],
662
+ ["Files", text(value.file_count)],
663
+ ["Size", displayBytes(value.size_bytes)],
664
+ ["Cache", text(value.model_cache) ?? text(value.hf_home)]
665
+ ]);
666
+ }
667
+ function renderActiveModel(value) {
668
+ const active = text(value.active) ?? "base";
669
+ const pointer = record(value.pointer);
670
+ if (pointer?.action === "activate") {
671
+ printSuccess(`Activated local model ${active}`);
672
+ } else if (pointer?.action === "rollback") {
673
+ printSuccess(`Rolled back to ${active}`);
674
+ }
675
+ printDetail([
676
+ ["Active", active],
677
+ ["Run", text(pointer?.run_id)],
678
+ ["Previous", text(pointer?.previous_model_id) ?? "base"],
679
+ ["Changed", formatDate(text(pointer?.activated_at))],
680
+ ["Manifest", text(value.manifest_path)]
681
+ ]);
682
+ }
683
+ function renderServePlan(value) {
684
+ printSuccess("Local serving plan is valid");
685
+ printDetail([
686
+ ["Model", text(value.model_id) ?? text(value.base_model)],
687
+ ["URL", text(value.url)],
688
+ ["Artifact", text(value.artifact_path)],
689
+ ["Manifest", text(value.manifest_path)],
690
+ ["Command", displayCommand(value.command)]
691
+ ]);
692
+ }
693
+ function renderKnownJson(args, value) {
694
+ const command = args[0];
695
+ const subcommand = args[1];
696
+ if (command === "doctor" && isRecord(value)) {
697
+ renderDoctor(value);
698
+ return true;
699
+ }
700
+ if (command === "init" && isRecord(value)) {
701
+ renderInit(value);
702
+ return true;
703
+ }
704
+ if (command === "validate" && isRecord(value)) {
705
+ renderValidate(value);
706
+ return true;
707
+ }
708
+ if (command === "run" && isRecord(value)) {
709
+ renderRun(value);
710
+ return true;
711
+ }
712
+ if (command === "runs") {
713
+ if ((subcommand === void 0 || subcommand === "list") && Array.isArray(value)) {
714
+ renderRunsList(value);
715
+ return true;
716
+ }
717
+ if (subcommand === "events" && Array.isArray(value)) {
718
+ renderRunEvents(value);
719
+ return true;
720
+ }
721
+ if (subcommand === "get" && isRecord(value)) {
722
+ renderRunRecord(value);
723
+ return true;
724
+ }
725
+ if (subcommand === "report" && isRecord(value)) {
726
+ renderRunReport(value);
727
+ return true;
728
+ }
729
+ if (subcommand === "compare" && isRecord(value)) {
730
+ renderRunComparison(value);
731
+ return true;
732
+ }
733
+ }
734
+ if (command === "models") {
735
+ if ((subcommand === void 0 || subcommand === "list") && Array.isArray(value)) {
736
+ renderModelsList(value);
737
+ return true;
738
+ }
739
+ if ((subcommand === "get" || subcommand === "verify") && isRecord(value)) {
740
+ if (subcommand === "verify") printSuccess("Local model artifact is valid");
741
+ renderModelRecord(value);
742
+ return true;
743
+ }
744
+ if ((subcommand === "prefetch" || subcommand === "verify-base") && isRecord(value)) {
745
+ renderModelPrefetch(value);
746
+ return true;
747
+ }
748
+ if (["active", "activate", "rollback"].includes(subcommand ?? "") && isRecord(value)) {
749
+ renderActiveModel(value);
750
+ return true;
751
+ }
752
+ if (subcommand === "serve" && isRecord(value)) {
753
+ renderServePlan(value);
754
+ return true;
755
+ }
756
+ }
757
+ if (command === "serve" && isRecord(value)) {
758
+ renderServePlan(value);
759
+ return true;
760
+ }
761
+ return false;
762
+ }
763
+ function lastNonEmptyLine(value) {
764
+ return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1);
765
+ }
766
+ function adaptLocalCliText(value) {
767
+ return value.replace(
768
+ /(?<![.@/\w])tuned-tensor-local(?=$|[\s:`\]])/g,
769
+ "tt local"
770
+ ).replace(/(?<![.@/\w])tt-local(?=$|[\s:`\]])/g, "tt local");
771
+ }
772
+ function localCliErrorEnvelope(payload) {
773
+ const rawMessage = payload.errorMessage ?? (payload.stderr.trim() || void 0) ?? lastNonEmptyLine(payload.stdout) ?? `tt local exited with code ${payload.exitCode}.`;
774
+ return {
775
+ error: {
776
+ status: null,
777
+ code: "LOCAL_CLI_ERROR",
778
+ message: adaptLocalCliText(rawMessage),
779
+ exit_code: payload.exitCode,
780
+ signal: payload.signal
781
+ }
782
+ };
783
+ }
784
+ function localCliTextEnvelope(payload) {
785
+ const output = adaptLocalCliText(payload.stdout).trim();
786
+ if (payload.args[0] === "info") {
787
+ const lines = output.split(/\r?\n/);
788
+ const heading = lines[0]?.match(/^([^:]+):\s*(.+)$/);
789
+ const fields = Object.fromEntries(
790
+ lines.slice(1).flatMap((line) => {
791
+ const match = line.match(/^([^:]+):\s*(.*)$/);
792
+ return match ? [[match[1].trim().toLowerCase().replaceAll(" ", "_"), match[2].trim()]] : [];
793
+ })
794
+ );
795
+ if (heading) {
796
+ return {
797
+ data: {
798
+ name: heading[1],
799
+ description: heading[2],
800
+ ...fields
801
+ }
802
+ };
803
+ }
804
+ }
805
+ return { data: { output } };
806
+ }
807
+ function renderLocalOutput(payload, options = {}) {
808
+ if (payload.streamingStdout && payload.exitCode === 0 && !payload.errorMessage) {
809
+ return;
810
+ }
811
+ const output = options.stdout ?? process.stdout;
812
+ if (options.jsonMode) {
813
+ if (payload.hasJson) {
814
+ output.write(payload.stdout);
815
+ return;
816
+ }
817
+ if (payload.exitCode !== 0 || payload.errorMessage) {
818
+ output.write(`${JSON.stringify(localCliErrorEnvelope(payload), null, 2)}
819
+ `);
820
+ return;
821
+ }
822
+ output.write(`${JSON.stringify(localCliTextEnvelope(payload), null, 2)}
823
+ `);
824
+ return;
825
+ }
826
+ if (payload.droppedSpecKeys.length > 0) {
827
+ printWarning(
828
+ `Ignored project field(s) unsupported by the local runtime: ${payload.droppedSpecKeys.join(", ")}.`
829
+ );
830
+ }
831
+ if (payload.hasJson && renderKnownJson(payload.args, payload.json)) return;
832
+ if (payload.hasJson) {
833
+ printJson(payload.json);
834
+ return;
835
+ }
836
+ if (payload.stdout) output.write(adaptLocalCliText(payload.stdout));
837
+ if (payload.exitCode !== 0 && !payload.stderr && !payload.stdout) {
838
+ printError(
839
+ payload.errorMessage ? adaptLocalCliText(payload.errorMessage) : `tt local exited with code ${payload.exitCode}.`
840
+ );
841
+ }
842
+ }
843
+
844
+ // src/local-runner.ts
845
+ var localRequire = createRequire(import.meta.url);
846
+ var DEFAULT_SPEC_NAME = "tunedtensor.json";
847
+ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
848
+ "--config",
849
+ "--name",
850
+ "--model",
851
+ "--output",
852
+ "--profile",
853
+ "--host",
854
+ "--port",
855
+ "--device",
856
+ "--max-tokens",
857
+ "--temperature",
858
+ "--top-p",
859
+ "--max-concurrent-requests",
860
+ "--spec",
861
+ "--api-key-env"
862
+ ]);
863
+ function isOptionWithInlineValue(value) {
864
+ const equals = value.indexOf("=");
865
+ return equals !== -1 && VALUE_OPTIONS.has(value.slice(0, equals));
866
+ }
867
+ function positionalIndices(args, start) {
868
+ const indices = [];
869
+ let optionsEnded = false;
870
+ for (let index = start; index < args.length; index += 1) {
871
+ const value = args[index];
872
+ if (!optionsEnded && value === "--") {
873
+ optionsEnded = true;
874
+ continue;
875
+ }
876
+ if (!optionsEnded && value.startsWith("-")) {
877
+ if (!isOptionWithInlineValue(value) && VALUE_OPTIONS.has(value)) index += 1;
878
+ continue;
879
+ }
880
+ indices.push(index);
881
+ }
882
+ return indices;
883
+ }
884
+ function optionValueArgument(args, name) {
885
+ for (let index = 0; index < args.length; index += 1) {
886
+ const value = args[index];
887
+ if (value === name) {
888
+ const path = args[index + 1];
889
+ if (!path) return null;
890
+ return {
891
+ path,
892
+ replace(projectedPath) {
893
+ args[index + 1] = projectedPath;
894
+ }
895
+ };
896
+ }
897
+ if (value.startsWith(`${name}=`)) {
898
+ const path = value.slice(name.length + 1);
899
+ if (!path) return null;
900
+ return {
901
+ path,
902
+ replace(projectedPath) {
903
+ args[index] = `${name}=${projectedPath}`;
904
+ }
905
+ };
906
+ }
907
+ }
908
+ return null;
909
+ }
910
+ function positionalSpecArgument(args, start, defaultPath) {
911
+ const positional = positionalIndices(args, start)[0];
912
+ if (positional !== void 0) {
913
+ return {
914
+ path: args[positional],
915
+ replace(projectedPath) {
916
+ args[positional] = projectedPath;
917
+ }
918
+ };
919
+ }
920
+ return {
921
+ path: defaultPath,
922
+ replace(projectedPath) {
923
+ args.splice(start, 0, projectedPath);
924
+ }
925
+ };
926
+ }
927
+ function selectedSpecArgument(args, cwd) {
928
+ const command = args[0];
929
+ if (["doctor", "validate", "run"].includes(command ?? "")) {
930
+ return positionalSpecArgument(args, 1, join2(cwd, DEFAULT_SPEC_NAME));
931
+ }
932
+ if (command === "models" && ["prefetch", "verify-base"].includes(args[1] ?? "")) {
933
+ return positionalSpecArgument(args, 2, join2(cwd, DEFAULT_SPEC_NAME));
934
+ }
935
+ const serves = command === "serve" || command === "models" && args[1] === "serve";
936
+ if (!serves) return null;
937
+ const explicit = optionValueArgument(args, "--spec");
938
+ if (explicit) return explicit;
939
+ return null;
940
+ }
941
+ async function pathIsFile(path) {
942
+ return (await stat(path).catch(() => null))?.isFile() ?? false;
943
+ }
944
+ function resolveFromCwd(path, cwd) {
945
+ if (isAbsolute(path)) return path;
946
+ return resolve(cwd, path);
947
+ }
948
+ async function projectSpecArguments(originalArgs, cwd) {
949
+ const args = [...originalArgs];
950
+ if (args.includes("--help") || args.includes("-h")) {
951
+ return {
952
+ args,
953
+ droppedSpecKeys: [],
954
+ pathReplacements: [],
955
+ cleanup: async () => {
956
+ }
957
+ };
958
+ }
959
+ const selected = selectedSpecArgument(args, cwd);
960
+ if (!selected) {
961
+ return {
962
+ args,
963
+ droppedSpecKeys: [],
964
+ pathReplacements: [],
965
+ cleanup: async () => {
966
+ }
967
+ };
968
+ }
969
+ const sourcePath = resolveFromCwd(selected.path, cwd);
970
+ if (!await pathIsFile(sourcePath)) {
971
+ return {
972
+ args,
973
+ droppedSpecKeys: [],
974
+ pathReplacements: [],
975
+ cleanup: async () => {
976
+ }
977
+ };
978
+ }
979
+ const raw = JSON.parse(await readFile(sourcePath, "utf8"));
980
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
981
+ throw new Error(`${sourcePath} must contain a JSON object.`);
982
+ }
983
+ const body = raw;
984
+ const runRequestKeys = [
985
+ "run_id",
986
+ "behavior_spec_id",
987
+ "run_number",
988
+ "spec_snapshot"
989
+ ].filter((key) => key in body);
990
+ if ("spec_snapshot" in body || runRequestKeys.length >= 2) {
991
+ return {
992
+ args,
993
+ droppedSpecKeys: [],
994
+ pathReplacements: [],
995
+ cleanup: async () => {
996
+ }
997
+ };
998
+ }
999
+ const projected = projectLocalSpec(body);
1000
+ if (projected.droppedKeys.length === 0) {
1001
+ return {
1002
+ args,
1003
+ droppedSpecKeys: [],
1004
+ pathReplacements: [],
1005
+ cleanup: async () => {
1006
+ }
1007
+ };
1008
+ }
1009
+ const temporaryPath = join2(
1010
+ dirname(sourcePath),
1011
+ `.${basename2(sourcePath)}.tt-local-${process.pid}-${randomUUID()}.json`
1012
+ );
1013
+ await writeFile(
1014
+ temporaryPath,
1015
+ `${JSON.stringify(projected.body, null, 2)}
1016
+ `,
1017
+ { encoding: "utf8", flag: "wx", mode: 384 }
1018
+ );
1019
+ selected.replace(temporaryPath);
1020
+ return {
1021
+ args,
1022
+ droppedSpecKeys: projected.droppedKeys,
1023
+ pathReplacements: [{
1024
+ sourcePath,
1025
+ projectedPath: temporaryPath
1026
+ }],
1027
+ cleanup: () => rm(temporaryPath, { force: true })
1028
+ };
1029
+ }
1030
+ function resolveLocalCliEntrypoint() {
1031
+ try {
1032
+ return localRequire.resolve("@tuned-tensor/local");
1033
+ } catch (error) {
1034
+ throw new Error(
1035
+ "TT Local is not installed. Reinstall @tuned-tensor/cli with its local runtime dependency.",
1036
+ { cause: error }
1037
+ );
1038
+ }
1039
+ }
1040
+ function localCommandStreamsStdout(args) {
1041
+ const serves = args[0] === "serve" || args[0] === "models" && args[1] === "serve";
1042
+ return serves && !args.includes("--print-command") && !args.includes("--help") && !args.includes("-h");
1043
+ }
1044
+ function parseJsonOutput(stdout2) {
1045
+ if (!stdout2.trim()) return { hasJson: false, json: void 0 };
1046
+ try {
1047
+ return { hasJson: true, json: JSON.parse(stdout2) };
1048
+ } catch {
1049
+ return { hasJson: false, json: void 0 };
1050
+ }
1051
+ }
1052
+ function signalExitCode(signal) {
1053
+ if (!signal) return 1;
1054
+ const signalNumber = osConstants.signals[signal];
1055
+ return typeof signalNumber === "number" ? 128 + signalNumber : 1;
1056
+ }
1057
+ function writeStream(stream, chunk) {
1058
+ stream.write(chunk);
1059
+ }
1060
+ function normalizeProjectedPaths(value, replacements) {
1061
+ let normalized = value;
1062
+ for (const replacement of replacements) {
1063
+ const encodedProjectedPath = JSON.stringify(
1064
+ replacement.projectedPath
1065
+ ).slice(1, -1);
1066
+ const encodedSourcePath = JSON.stringify(
1067
+ replacement.sourcePath
1068
+ ).slice(1, -1);
1069
+ normalized = normalized.replaceAll(encodedProjectedPath, encodedSourcePath).replaceAll(replacement.projectedPath, replacement.sourcePath);
1070
+ }
1071
+ return normalized;
1072
+ }
1073
+ async function spawnLocalCli(args) {
1074
+ const output = args.stdout ?? process.stdout;
1075
+ const errors = args.stderr ?? process.stderr;
1076
+ const pipeInput = Boolean(args.stdin && args.stdin !== process.stdin);
1077
+ const pipeStreamingOutput = args.streamingStdout && Boolean(args.stdout && args.stdout !== process.stdout);
1078
+ const child = spawn(
1079
+ args.nodeExecutable,
1080
+ [args.entrypoint, ...args.commandArgs],
1081
+ {
1082
+ cwd: args.cwd,
1083
+ env: args.env,
1084
+ stdio: [
1085
+ pipeInput ? "pipe" : "inherit",
1086
+ args.streamingStdout && !pipeStreamingOutput ? "inherit" : "pipe",
1087
+ "pipe"
1088
+ ],
1089
+ detached: process.platform !== "win32"
1090
+ }
1091
+ );
1092
+ if (pipeInput && args.stdin && child.stdin) args.stdin.pipe(child.stdin);
1093
+ let stdout2 = "";
1094
+ let stderr = "";
1095
+ child.stdout?.setEncoding("utf8");
1096
+ child.stderr?.setEncoding("utf8");
1097
+ child.stdout?.on("data", (chunk) => {
1098
+ const normalized = normalizeProjectedPaths(
1099
+ chunk,
1100
+ args.pathReplacements
1101
+ );
1102
+ if (args.streamingStdout) writeStream(output, normalized);
1103
+ else stdout2 += normalized;
1104
+ });
1105
+ child.stderr?.on("data", (chunk) => {
1106
+ const normalized = normalizeProjectedPaths(
1107
+ chunk,
1108
+ args.pathReplacements
1109
+ );
1110
+ stderr += normalized;
1111
+ writeStream(
1112
+ errors,
1113
+ args.jsonMode ? normalized : adaptLocalCliText(normalized)
1114
+ );
1115
+ });
1116
+ return new Promise((resolveChild) => {
1117
+ let spawnError;
1118
+ const forwardSignal = (signal) => {
1119
+ const childSignal = signal === "SIGHUP" ? "SIGTERM" : signal;
1120
+ if (child.pid && process.platform !== "win32") {
1121
+ try {
1122
+ process.kill(-child.pid, childSignal);
1123
+ return;
1124
+ } catch {
1125
+ }
1126
+ }
1127
+ child.kill(childSignal);
1128
+ };
1129
+ const onSigint = () => forwardSignal("SIGINT");
1130
+ const onSigterm = () => forwardSignal("SIGTERM");
1131
+ const onSighup = () => forwardSignal("SIGHUP");
1132
+ const cleanup = () => {
1133
+ process.off("SIGINT", onSigint);
1134
+ process.off("SIGTERM", onSigterm);
1135
+ if (process.platform !== "win32") process.off("SIGHUP", onSighup);
1136
+ };
1137
+ process.on("SIGINT", onSigint);
1138
+ process.on("SIGTERM", onSigterm);
1139
+ if (process.platform !== "win32") process.on("SIGHUP", onSighup);
1140
+ child.once("error", (error) => {
1141
+ spawnError = error;
1142
+ });
1143
+ child.once("close", (code, signal) => {
1144
+ cleanup();
1145
+ resolveChild({
1146
+ exitCode: spawnError ? 1 : code ?? signalExitCode(signal),
1147
+ signal,
1148
+ stdout: stdout2,
1149
+ stderr,
1150
+ ...spawnError ? { errorMessage: spawnError.message } : {}
1151
+ });
1152
+ });
1153
+ });
1154
+ }
1155
+ function setupFailure(originalArgs, error) {
1156
+ const message = error instanceof Error ? error.message : String(error);
1157
+ return {
1158
+ args: [...originalArgs],
1159
+ projectedArgs: [...originalArgs],
1160
+ entrypoint: null,
1161
+ exitCode: 1,
1162
+ signal: null,
1163
+ stdout: "",
1164
+ stderr: "",
1165
+ hasJson: false,
1166
+ json: void 0,
1167
+ streamingStdout: false,
1168
+ droppedSpecKeys: [],
1169
+ errorMessage: message
1170
+ };
1171
+ }
1172
+ async function runLocalCommand(args, options = {}) {
1173
+ const cwd = resolve(options.cwd ?? process.cwd());
1174
+ let projected;
1175
+ try {
1176
+ const entrypoint = options.entrypoint ?? resolveLocalCliEntrypoint();
1177
+ projected = await projectSpecArguments(args, cwd);
1178
+ const streamingStdout = localCommandStreamsStdout(projected.args);
1179
+ const result = await spawnLocalCli({
1180
+ entrypoint,
1181
+ commandArgs: projected.args,
1182
+ cwd,
1183
+ env: options.env ?? process.env,
1184
+ nodeExecutable: options.nodeExecutable ?? process.execPath,
1185
+ streamingStdout,
1186
+ jsonMode: options.jsonMode ?? false,
1187
+ stdin: options.stdin,
1188
+ stdout: options.stdout,
1189
+ stderr: options.stderr,
1190
+ pathReplacements: projected.pathReplacements
1191
+ });
1192
+ const parsed = parseJsonOutput(result.stdout);
1193
+ return {
1194
+ args: [...args],
1195
+ projectedArgs: projected.args,
1196
+ entrypoint,
1197
+ ...result,
1198
+ ...parsed,
1199
+ streamingStdout,
1200
+ droppedSpecKeys: projected.droppedSpecKeys
1201
+ };
1202
+ } catch (error) {
1203
+ return setupFailure(args, error);
1204
+ } finally {
1205
+ await projected?.cleanup().catch(() => void 0);
1206
+ }
1207
+ }
1208
+ async function executeLocalCommand(args, options = {}) {
1209
+ const result = await runLocalCommand(args, options);
1210
+ renderLocalOutput(result, {
1211
+ jsonMode: options.jsonMode,
1212
+ stdout: options.stdout
1213
+ });
1214
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
1215
+ return result;
1216
+ }
1217
+
1218
+ // src/shell.ts
1219
+ import chalk2 from "chalk";
1220
+ import { stat as stat3 } from "fs/promises";
1221
+ import { homedir as homedir3 } from "os";
1222
+ import { isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
1223
+ import { createInterface } from "readline";
1224
+
1225
+ // src/command-catalog.ts
1226
+ var CLOUD = ["cloud"];
1227
+ var LOCAL = ["local"];
1228
+ var BOTH = ["cloud", "local"];
1229
+ var COMMAND_CATALOG = [
1230
+ { path: "init", description: "Create a behaviour-spec project.", group: "Workflow", modes: BOTH },
1231
+ { path: "eval", description: "Validate a managed-service behaviour spec.", group: "Workflow", modes: CLOUD },
1232
+ { path: "push", description: "Create or update the cloud behaviour spec.", group: "Workflow", modes: CLOUD },
1233
+ { path: "validate", description: "Validate a local fine-tuning project.", group: "Workflow", modes: LOCAL },
1234
+ { path: "doctor", description: "Check the local host and run prerequisites.", group: "Workflow", modes: LOCAL },
1235
+ { path: "run", description: "Run the local fine-tuning and evaluation workflow.", group: "Workflow", modes: LOCAL },
1236
+ { path: "runs list", description: "List runs.", group: "Inspect", modes: BOTH },
1237
+ { path: "runs get", description: "Show a run.", group: "Inspect", modes: BOTH },
1238
+ { path: "runs report", description: "Show a run report.", group: "Inspect", modes: BOTH },
1239
+ { path: "runs compare", description: "Compare two local run reports.", group: "Inspect", modes: LOCAL },
1240
+ { path: "runs events", description: "Show local run progress events.", group: "Inspect", modes: LOCAL },
1241
+ { path: "runs estimate", description: "Estimate managed run cost and duration.", group: "Workflow", modes: CLOUD },
1242
+ { path: "runs start", description: "Start a managed fine-tuning run.", group: "Workflow", modes: CLOUD },
1243
+ { path: "runs watch", description: "Watch a managed run.", group: "Inspect", modes: CLOUD },
1244
+ { path: "runs diagnose", description: "Show managed run diagnostics.", group: "Inspect", modes: CLOUD },
1245
+ { path: "runs cancel", description: "Cancel a managed run.", group: "Workflow", modes: CLOUD },
1246
+ { path: "models list", description: "List models.", group: "Inspect", modes: BOTH },
1247
+ { path: "models get", description: "Show a model.", group: "Inspect", modes: BOTH },
1248
+ { path: "models base", description: "List managed-service base models.", group: "Inspect", modes: CLOUD },
1249
+ { path: "models download", description: "Download a managed model artifact.", group: "Serving", modes: CLOUD },
1250
+ { path: "models export", description: "Export a managed model to GGUF or Ollama.", group: "Serving", modes: CLOUD },
1251
+ { path: "models setup-runtime", description: "Install the managed model-serving runtime.", group: "Serving", modes: CLOUD },
1252
+ { path: "models serve", description: "Serve a model through an OpenAI-compatible API.", group: "Serving", modes: BOTH },
1253
+ { path: "models delete", description: "Delete a managed model.", group: "Workflow", modes: CLOUD },
1254
+ { path: "models verify", description: "Verify a local model artifact.", group: "Serving", modes: LOCAL },
1255
+ { path: "models prefetch", description: "Download the local base-model snapshot.", group: "Serving", modes: LOCAL },
1256
+ { path: "models verify-base", description: "Verify the local base-model snapshot.", group: "Serving", modes: LOCAL },
1257
+ { path: "models active", description: "Show the active local model.", group: "Serving", modes: LOCAL },
1258
+ { path: "models activate", description: "Activate a verified local model.", group: "Serving", modes: LOCAL },
1259
+ { path: "models rollback", description: "Roll back the active local model.", group: "Serving", modes: LOCAL },
1260
+ { path: "serve", description: "Serve a local adapter, active model, or base model.", group: "Serving", modes: LOCAL },
1261
+ { path: "specs list", description: "List cloud behaviour specs.", group: "Inspect", modes: CLOUD },
1262
+ { path: "specs get", description: "Show a cloud behaviour spec.", group: "Inspect", modes: CLOUD },
1263
+ { path: "specs create", description: "Create a cloud behaviour spec.", group: "Workflow", modes: CLOUD },
1264
+ { path: "specs update", description: "Update a cloud behaviour spec.", group: "Workflow", modes: CLOUD },
1265
+ { path: "specs delete", description: "Delete a cloud behaviour spec.", group: "Workflow", modes: CLOUD },
1266
+ { path: "datasets list", description: "List cloud datasets.", group: "Data", modes: CLOUD },
1267
+ { path: "datasets get", description: "Show a cloud dataset.", group: "Data", modes: CLOUD },
1268
+ { path: "datasets upload", description: "Upload a cloud dataset.", group: "Data", modes: CLOUD },
1269
+ { path: "datasets delete", description: "Delete a cloud dataset.", group: "Data", modes: CLOUD },
1270
+ { path: "label upload", description: "Start a cloud teacher-labeling job.", group: "Data", modes: CLOUD },
1271
+ { path: "label watch", description: "Watch a cloud labeling job.", group: "Data", modes: CLOUD },
1272
+ { path: "label list", description: "List cloud labeling jobs.", group: "Data", modes: CLOUD },
1273
+ { path: "label status", description: "Show cloud labeling-job status.", group: "Data", modes: CLOUD },
1274
+ { path: "label rows", description: "Review labeled rows.", group: "Data", modes: CLOUD },
1275
+ { path: "label accept", description: "Accept labeled rows.", group: "Data", modes: CLOUD },
1276
+ { path: "label reject", description: "Reject labeled rows.", group: "Data", modes: CLOUD },
1277
+ { path: "label edit", description: "Edit a labeled row.", group: "Data", modes: CLOUD },
1278
+ { path: "label promote", description: "Promote reviewed rows to a dataset.", group: "Data", modes: CLOUD },
1279
+ { path: "label cancel", description: "Cancel a labeling job.", group: "Data", modes: CLOUD },
1280
+ { path: "auth login", description: "Store a cloud API key.", group: "Account", modes: CLOUD },
1281
+ { path: "auth logout", description: "Remove stored cloud credentials.", group: "Account", modes: CLOUD },
1282
+ { path: "auth status", description: "Show cloud authentication status.", group: "Account", modes: CLOUD },
1283
+ { path: "balance", description: "Show cloud credit balance.", group: "Account", modes: CLOUD },
1284
+ { path: "topup", description: "Open a cloud credit checkout.", group: "Account", modes: CLOUD },
1285
+ { path: "info", description: "Show TT Local package information.", group: "Inspect", modes: LOCAL }
1286
+ ];
1287
+ var SLASH_COMMANDS = [
1288
+ { path: "/help", description: "Show commands; add a word to filter." },
1289
+ { path: "/status", description: "Show lightweight workflow status." },
1290
+ { path: "/context", description: "Show the current project and backend context." },
1291
+ { path: "/mode cloud", description: "Use the managed cloud workflow." },
1292
+ { path: "/mode local", description: "Use the local GPU workflow." },
1293
+ { path: "/cd", description: "Change the shell's working directory." },
1294
+ { path: "/clear", description: "Clear the terminal." },
1295
+ { path: "/exit", description: "Exit the TT shell." }
1296
+ ];
1297
+ function catalogForMode(mode) {
1298
+ return COMMAND_CATALOG.filter((command) => command.modes.includes(mode));
1299
+ }
1300
+ function commandPathsForMode(mode) {
1301
+ return [...new Set(catalogForMode(mode).map((command) => command.path))].sort((left, right) => left.localeCompare(right));
1302
+ }
1303
+ function groupedCatalog(mode, query) {
1304
+ const normalizedQuery = query?.trim().toLowerCase();
1305
+ const matches = catalogForMode(mode).filter((command) => {
1306
+ if (!normalizedQuery) return true;
1307
+ return command.path.toLowerCase().includes(normalizedQuery) || command.description.toLowerCase().includes(normalizedQuery) || command.group.toLowerCase().includes(normalizedQuery);
1308
+ });
1309
+ const groups = /* @__PURE__ */ new Map();
1310
+ for (const command of matches) {
1311
+ const group = groups.get(command.group) ?? [];
1312
+ group.push(command);
1313
+ groups.set(command.group, group);
1314
+ }
1315
+ return groups;
1316
+ }
1317
+ function completionTarget(line, activeMode) {
1318
+ const leadingWhitespace = line.match(/^\s*/)?.[0] ?? "";
1319
+ const content = line.slice(leadingWhitespace.length);
1320
+ const override = content.match(/^(cloud|local)(?:\s+|$)/);
1321
+ if (!override) {
1322
+ return { prefix: leadingWhitespace, mode: activeMode, fragment: content };
1323
+ }
1324
+ const mode = override[1];
1325
+ const prefix = `${leadingWhitespace}${mode} `;
1326
+ return {
1327
+ prefix,
1328
+ mode,
1329
+ fragment: content.slice(override[0].length)
1330
+ };
1331
+ }
1332
+ function createCommandCompleter(getMode) {
1333
+ return (line) => {
1334
+ const trimmed = line.trimStart();
1335
+ if (trimmed.startsWith("/")) {
1336
+ const candidates = SLASH_COMMANDS.map((command) => command.path);
1337
+ const matches2 = candidates.filter((candidate) => candidate.startsWith(trimmed));
1338
+ return [matches2.length > 0 ? matches2 : candidates, line];
1339
+ }
1340
+ if (trimmed.startsWith("?")) {
1341
+ return [["?"], line];
1342
+ }
1343
+ const { prefix, mode, fragment } = completionTarget(line, getMode());
1344
+ const paths = commandPathsForMode(mode);
1345
+ const targetCandidates = paths.map((path) => `${prefix}${path}`);
1346
+ if (!prefix.trim()) targetCandidates.unshift("cloud ", "local ");
1347
+ const normalizedLine = line.trimStart().toLowerCase();
1348
+ const normalizedFragment = fragment.toLowerCase();
1349
+ const matches = targetCandidates.filter((candidate) => {
1350
+ if (prefix.trim()) return candidate.toLowerCase().startsWith(`${prefix.toLowerCase()}${normalizedFragment}`);
1351
+ return candidate.toLowerCase().startsWith(normalizedLine);
1352
+ });
1353
+ return [matches.length > 0 ? matches : targetCandidates, line];
1354
+ };
1355
+ }
1356
+
1357
+ // src/shell-context.ts
1358
+ import { readFile as readFile2, readdir, stat as stat2 } from "fs/promises";
1359
+ import { homedir as homedir2 } from "os";
1360
+ import { basename as basename3, dirname as dirname2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "path";
1361
+ async function readJsonObject(path) {
1362
+ try {
1363
+ const parsed = JSON.parse(await readFile2(path, "utf8"));
1364
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1365
+ return { found: true, invalid: true };
1366
+ }
1367
+ return {
1368
+ found: true,
1369
+ value: parsed,
1370
+ invalid: false
1371
+ };
1372
+ } catch (error) {
1373
+ if (error.code === "ENOENT") {
1374
+ return { found: false, invalid: false };
1375
+ }
1376
+ return { found: true, invalid: true };
1377
+ }
1378
+ }
1379
+ function stringField(value, key) {
1380
+ const field = value?.[key];
1381
+ return typeof field === "string" && field.trim() ? field : void 0;
1382
+ }
1383
+ function expandPath(value, baseDirectory, homeDirectory) {
1384
+ if (value === "~") return homeDirectory;
1385
+ if (value.startsWith("~/")) return resolve2(homeDirectory, value.slice(2));
1386
+ return isAbsolute2(value) ? resolve2(value) : resolve2(baseDirectory, value);
1387
+ }
1388
+ function environmentHome(env) {
1389
+ return env.HOME ? resolve2(env.HOME) : homedir2();
1390
+ }
1391
+ function cloudConfigPath(env, homeDirectory) {
1392
+ const configHome = env.XDG_CONFIG_HOME ? resolve2(env.XDG_CONFIG_HOME) : join3(homeDirectory, ".config");
1393
+ return join3(configHome, "tuned-tensor", "config.json");
1394
+ }
1395
+ function redactApiKey(key) {
1396
+ if (!key) return void 0;
1397
+ if (key.length <= 4) return "\u2026";
1398
+ const prefixLength = key.length > 8 ? 8 : Math.max(1, Math.floor(key.length / 2));
1399
+ return `${key.slice(0, prefixLength)}\u2026`;
1400
+ }
1401
+ function targetFromEnvironment(env) {
1402
+ const value = env.TT_TARGET?.trim().toLowerCase();
1403
+ return value === "cloud" || value === "local" ? value : void 0;
1404
+ }
1405
+ async function adjacentFile(path) {
1406
+ try {
1407
+ return (await stat2(path)).isFile();
1408
+ } catch {
1409
+ return false;
1410
+ }
1411
+ }
1412
+ async function readActiveModelId(storeRoot) {
1413
+ const result = await readJsonObject(join3(storeRoot, "active-model.json"));
1414
+ if (!result.found || result.invalid) return void 0;
1415
+ const modelId = result.value?.model_id;
1416
+ if (modelId === null) return "base";
1417
+ return typeof modelId === "string" && modelId ? modelId : void 0;
1418
+ }
1419
+ async function readLatestRun(storeRoot) {
1420
+ const runsRoot = join3(storeRoot, "runs");
1421
+ let names;
1422
+ try {
1423
+ names = (await readdir(runsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1424
+ } catch {
1425
+ return void 0;
1426
+ }
1427
+ const states = await Promise.all(names.map(async (name) => {
1428
+ const result = await readJsonObject(join3(runsRoot, name, "state.json"));
1429
+ if (!result.found || result.invalid || !result.value) return void 0;
1430
+ const id = stringField(result.value, "id") ?? name;
1431
+ const state = {
1432
+ id,
1433
+ status: stringField(result.value, "status"),
1434
+ specName: stringField(result.value, "spec_name"),
1435
+ updatedAt: stringField(result.value, "updated_at")
1436
+ };
1437
+ return state;
1438
+ }));
1439
+ return states.filter((state) => Boolean(state)).sort((left, right) => {
1440
+ const leftUpdated = left.updatedAt ?? "";
1441
+ const rightUpdated = right.updatedAt ?? "";
1442
+ if (leftUpdated !== rightUpdated) {
1443
+ return rightUpdated.localeCompare(leftUpdated);
1444
+ }
1445
+ return right.id.localeCompare(left.id);
1446
+ })[0];
1447
+ }
1448
+ async function discoverShellContext(options = {}) {
1449
+ const env = options.env ?? process.env;
1450
+ const cwd = resolve2(options.cwd ?? process.cwd());
1451
+ const homeDirectory = environmentHome(env);
1452
+ const warnings = [];
1453
+ const specPath = join3(cwd, "tunedtensor.json");
1454
+ const specJson = await readJsonObject(specPath);
1455
+ let spec;
1456
+ if (specJson.found) {
1457
+ if (specJson.invalid || !specJson.value) {
1458
+ spec = { path: specPath, parseError: true };
1459
+ warnings.push("tunedtensor.json could not be parsed.");
1460
+ } else {
1461
+ const examples = specJson.value.examples;
1462
+ spec = {
1463
+ path: specPath,
1464
+ id: stringField(specJson.value, "id"),
1465
+ name: stringField(specJson.value, "name"),
1466
+ baseModel: stringField(specJson.value, "base_model"),
1467
+ exampleCount: Array.isArray(examples) ? examples.length : void 0,
1468
+ parseError: false
1469
+ };
1470
+ }
1471
+ }
1472
+ const localConfigPath = join3(cwd, "local-runner.json");
1473
+ const hasAdjacentLocalConfig = await adjacentFile(localConfigPath);
1474
+ const localConfigJson = hasAdjacentLocalConfig ? await readJsonObject(localConfigPath) : { found: false, invalid: false };
1475
+ if (localConfigJson.invalid) {
1476
+ warnings.push("local-runner.json could not be parsed.");
1477
+ }
1478
+ const localConfigDirectory = hasAdjacentLocalConfig ? dirname2(localConfigPath) : cwd;
1479
+ const configuredArtifactRoot = stringField(localConfigJson.value, "artifactRoot");
1480
+ const configuredStoreRoot = stringField(localConfigJson.value, "storeRoot");
1481
+ const artifactRoot = configuredArtifactRoot ? expandPath(configuredArtifactRoot, localConfigDirectory, homeDirectory) : resolve2(cwd, ".tt-local", "artifacts");
1482
+ const storeRoot = configuredStoreRoot ? expandPath(configuredStoreRoot, localConfigDirectory, homeDirectory) : env.TT_LOCAL_HOME ? expandPath(env.TT_LOCAL_HOME, cwd, homeDirectory) : join3(homeDirectory, ".tuned-tensor-local");
1483
+ const cloudPath = cloudConfigPath(env, homeDirectory);
1484
+ const cloudConfigJson = await readJsonObject(cloudPath);
1485
+ if (cloudConfigJson.invalid) {
1486
+ warnings.push("The Tuned Tensor cloud config could not be parsed.");
1487
+ }
1488
+ const environmentApiKey = env.TUNED_TENSOR_API_KEY?.trim();
1489
+ const storedApiKey = stringField(cloudConfigJson.value, "api_key");
1490
+ const apiKey = environmentApiKey || storedApiKey;
1491
+ const baseUrl = env.TUNED_TENSOR_URL?.trim() || stringField(cloudConfigJson.value, "base_url") || "https://tunedtensor.com";
1492
+ const environmentTarget = targetFromEnvironment(env);
1493
+ if (env.TT_TARGET && !environmentTarget) {
1494
+ warnings.push(`Ignoring invalid TT_TARGET=${JSON.stringify(env.TT_TARGET)}; use cloud or local.`);
1495
+ }
1496
+ const inferredTarget = environmentTarget ?? (hasAdjacentLocalConfig ? "local" : "cloud");
1497
+ const targetSource = environmentTarget ? "environment" : hasAdjacentLocalConfig ? "adjacent-config" : "default-cloud";
1498
+ const [activeModelId, latestRun] = await Promise.all([
1499
+ readActiveModelId(storeRoot),
1500
+ readLatestRun(storeRoot)
1501
+ ]);
1502
+ return {
1503
+ cwd,
1504
+ projectName: basename3(cwd) || cwd,
1505
+ inferredTarget,
1506
+ targetSource,
1507
+ spec,
1508
+ cloud: {
1509
+ authenticated: Boolean(apiKey),
1510
+ keyPrefix: redactApiKey(apiKey),
1511
+ baseUrl,
1512
+ configPath: cloudPath,
1513
+ configFound: cloudConfigJson.found
1514
+ },
1515
+ local: {
1516
+ configPath: hasAdjacentLocalConfig ? localConfigPath : void 0,
1517
+ artifactRoot,
1518
+ storeRoot,
1519
+ activeModelId,
1520
+ latestRun
1521
+ },
1522
+ warnings
1523
+ };
1524
+ }
1525
+ function valueOrDash(value) {
1526
+ return value === void 0 || value === "" ? "\u2014" : String(value);
1527
+ }
1528
+ function targetSourceLabel(source) {
1529
+ switch (source) {
1530
+ case "environment":
1531
+ return "TT_TARGET";
1532
+ case "adjacent-config":
1533
+ return "adjacent local-runner.json";
1534
+ default:
1535
+ return "default";
1536
+ }
1537
+ }
1538
+ function formatShellContext(context, selectedTarget, targetSource) {
1539
+ const specLabel = context.spec ? context.spec.parseError ? `${context.spec.path} (invalid JSON)` : context.spec.path : "not found";
1540
+ const sourceLabel = targetSource === "session" ? "session" : targetSource === "command-option" ? "--target" : targetSourceLabel(targetSource);
1541
+ const lines = [
1542
+ `Target ${selectedTarget} (${sourceLabel})`,
1543
+ `Project ${context.projectName}`,
1544
+ `Directory ${context.cwd}`,
1545
+ `Spec ${specLabel}`,
1546
+ `Spec ID ${valueOrDash(context.spec?.id)}`,
1547
+ `Base model ${valueOrDash(context.spec?.baseModel)}`,
1548
+ `Examples ${valueOrDash(context.spec?.exampleCount)}`,
1549
+ `Cloud endpoint ${context.cloud.baseUrl}`,
1550
+ `Cloud auth ${context.cloud.authenticated ? `yes (${context.cloud.keyPrefix})` : "no"}`,
1551
+ `Local config ${context.local.configPath ?? "not found"}`,
1552
+ `Artifact root ${context.local.artifactRoot}`,
1553
+ `Store root ${context.local.storeRoot}`
1554
+ ];
1555
+ for (const warning of context.warnings) lines.push(`Warning ${warning}`);
1556
+ return lines;
1557
+ }
1558
+ function formatShellStatus(context, selectedTarget) {
1559
+ const lines = [
1560
+ `Workflow ${selectedTarget}`,
1561
+ `Spec ${context.spec?.name ?? (context.spec ? "unnamed" : "not found")}`
1562
+ ];
1563
+ if (selectedTarget === "cloud") {
1564
+ lines.push(
1565
+ `Authentication ${context.cloud.authenticated ? `ready (${context.cloud.keyPrefix})` : "not configured"}`,
1566
+ `Endpoint ${context.cloud.baseUrl}`,
1567
+ "Remote status not queried"
1568
+ );
1569
+ } else {
1570
+ lines.push(
1571
+ `Local config ${context.local.configPath ?? "not found"}`,
1572
+ `Active model ${context.local.activeModelId ?? "base"}`,
1573
+ `Latest run ${context.local.latestRun ? `${context.local.latestRun.id} (${context.local.latestRun.status ?? "unknown"})` : "none"}`,
1574
+ "Host checks not run (use doctor)"
1575
+ );
1576
+ }
1577
+ return lines;
1578
+ }
1579
+
1580
+ // src/shell.ts
1581
+ var ShellParseError = class extends Error {
1582
+ constructor(message) {
1583
+ super(message);
1584
+ this.name = "ShellParseError";
1585
+ }
1586
+ };
1587
+ function operatorError(operator) {
1588
+ return new ShellParseError(
1589
+ `Shell operator ${JSON.stringify(operator)} is not supported. Run TT commands directly; pipes, redirects, and command substitution are disabled.`
1590
+ );
1591
+ }
1592
+ function tokenizeShellInput(input) {
1593
+ if (input.includes("\0")) {
1594
+ throw new ShellParseError("Command input must not contain NUL bytes.");
1595
+ }
1596
+ if (/[\r\n]/.test(input)) {
1597
+ throw new ShellParseError("Enter one command at a time.");
1598
+ }
1599
+ if (input.trimStart().startsWith("!")) {
1600
+ throw new ShellParseError("Shell escapes are disabled; enter a TT command instead.");
1601
+ }
1602
+ const tokens = [];
1603
+ let token = "";
1604
+ let tokenStarted = false;
1605
+ let quote = "none";
1606
+ const finishToken = () => {
1607
+ if (!tokenStarted) return;
1608
+ tokens.push(token);
1609
+ token = "";
1610
+ tokenStarted = false;
1611
+ };
1612
+ for (let index = 0; index < input.length; index += 1) {
1613
+ const character = input[index];
1614
+ if (quote === "single") {
1615
+ if (character === "'") {
1616
+ quote = "none";
1617
+ } else {
1618
+ token += character;
1619
+ }
1620
+ continue;
1621
+ }
1622
+ if (quote === "double") {
1623
+ if (character === '"') {
1624
+ quote = "none";
1625
+ continue;
1626
+ }
1627
+ if (character === "\\") {
1628
+ const escaped = input[index + 1];
1629
+ if (escaped === void 0) {
1630
+ throw new ShellParseError("Command ends with an unfinished escape.");
1631
+ }
1632
+ token += escaped;
1633
+ tokenStarted = true;
1634
+ index += 1;
1635
+ continue;
1636
+ }
1637
+ if (character === "`") throw operatorError("`\u2026`");
1638
+ if (character === "$" && input[index + 1] === "(") {
1639
+ throw operatorError("$(\u2026)");
1640
+ }
1641
+ token += character;
1642
+ tokenStarted = true;
1643
+ continue;
1644
+ }
1645
+ if (/\s/.test(character)) {
1646
+ finishToken();
1647
+ continue;
1648
+ }
1649
+ if (character === "'") {
1650
+ quote = "single";
1651
+ tokenStarted = true;
1652
+ continue;
1653
+ }
1654
+ if (character === '"') {
1655
+ quote = "double";
1656
+ tokenStarted = true;
1657
+ continue;
1658
+ }
1659
+ if (character === "\\") {
1660
+ const escaped = input[index + 1];
1661
+ if (escaped === void 0) {
1662
+ throw new ShellParseError("Command ends with an unfinished escape.");
1663
+ }
1664
+ token += escaped;
1665
+ tokenStarted = true;
1666
+ index += 1;
1667
+ continue;
1668
+ }
1669
+ if (character === "`") throw operatorError("`\u2026`");
1670
+ if (character === "$" && input[index + 1] === "(") {
1671
+ throw operatorError("$(\u2026)");
1672
+ }
1673
+ if (character === "|" || character === ">" || character === "<" || character === ";" || character === "&") {
1674
+ const doubled = input[index + 1] === character && (character === "|" || character === "&");
1675
+ throw operatorError(doubled ? character.repeat(2) : character);
1676
+ }
1677
+ token += character;
1678
+ tokenStarted = true;
1679
+ }
1680
+ if (quote !== "none") {
1681
+ throw new ShellParseError(
1682
+ `Command has an unterminated ${quote === "single" ? "single" : "double"} quote.`
1683
+ );
1684
+ }
1685
+ finishToken();
1686
+ return tokens;
1687
+ }
1688
+ function routeShellCommand(input, activeMode) {
1689
+ let args = tokenizeShellInput(input);
1690
+ if (args.length === 0) return null;
1691
+ if (args[0] === "tt") {
1692
+ args = args.slice(1);
1693
+ if (args.length === 0) {
1694
+ throw new ShellParseError("The TT shell is already open; enter a command such as `runs list`.");
1695
+ }
1696
+ }
1697
+ if (args[0]?.startsWith("/")) {
1698
+ throw new ShellParseError("Slash commands are session commands and cannot be routed to a workflow.");
1699
+ }
1700
+ const override = args[0];
1701
+ if (override === "cloud" || override === "local") {
1702
+ if (args.length === 1) {
1703
+ throw new ShellParseError(`Add a ${override} command, or use \`/mode ${override}\` to switch workflows.`);
1704
+ }
1705
+ return { target: override, args: args.slice(1) };
1706
+ }
1707
+ return { target: activeMode, args };
1708
+ }
1709
+ var SLASH_NAMES = /* @__PURE__ */ new Set([
1710
+ "help",
1711
+ "status",
1712
+ "context",
1713
+ "mode",
1714
+ "clear",
1715
+ "cd",
1716
+ "exit"
1717
+ ]);
1718
+ function parseSlashCommand(input) {
1719
+ const trimmed = input.trim();
1720
+ if (!trimmed) return null;
1721
+ if (trimmed === "?") return { name: "help", args: [] };
1722
+ if (trimmed.startsWith("? ")) {
1723
+ return {
1724
+ name: "help",
1725
+ args: tokenizeShellInput(trimmed.slice(1))
1726
+ };
1727
+ }
1728
+ if (!trimmed.startsWith("/")) return null;
1729
+ if (trimmed === "/") return { name: "palette", args: [] };
1730
+ const tokens = tokenizeShellInput(trimmed);
1731
+ const rawName = tokens[0].slice(1).toLowerCase();
1732
+ if (!SLASH_NAMES.has(rawName)) {
1733
+ throw new ShellParseError(`Unknown session command: /${rawName}. Use /help to see available commands.`);
1734
+ }
1735
+ return {
1736
+ name: rawName,
1737
+ args: tokens.slice(1)
1738
+ };
1739
+ }
1740
+ function errorMessage(error) {
1741
+ return error instanceof Error ? error.message : String(error);
1742
+ }
1743
+ function assertNoArgs(command, args) {
1744
+ if (args.length > 0) {
1745
+ throw new ShellParseError(`/${command} does not accept arguments.`);
1746
+ }
1747
+ }
1748
+ function expandDirectory(value, cwd, env) {
1749
+ const home = env.HOME ? resolve3(env.HOME) : homedir3();
1750
+ if (value === "~") return home;
1751
+ if (value.startsWith("~/")) return resolve3(home, value.slice(2));
1752
+ return isAbsolute3(value) ? resolve3(value) : resolve3(cwd, value);
1753
+ }
1754
+ function helpText(mode, query, palette = false) {
1755
+ const groups = groupedCatalog(mode, query);
1756
+ const lines = [];
1757
+ lines.push(palette ? `Commands for ${mode} \u2014 type a command or use cloud/local as a one-shot prefix` : `TT ${mode} commands${query ? ` matching ${JSON.stringify(query)}` : ""}`);
1758
+ if (groups.size === 0) {
1759
+ lines.push(" No matching commands.");
1760
+ } else {
1761
+ for (const [group, commands] of groups) {
1762
+ lines.push(`
1763
+ ${group}`);
1764
+ for (const command of commands) {
1765
+ lines.push(` ${command.path.padEnd(22)} ${command.description}`);
1766
+ }
1767
+ }
1768
+ }
1769
+ if (!query) {
1770
+ lines.push("\nSession");
1771
+ for (const command of SLASH_COMMANDS) {
1772
+ lines.push(` ${command.path.padEnd(22)} ${command.description}`);
1773
+ }
1774
+ lines.push(" ? Alias for /help.");
1775
+ lines.push("\nUse Tab to complete commands. Shell operators and shell escapes are disabled.");
1776
+ }
1777
+ return `${lines.join("\n")}
1778
+ `;
1779
+ }
1780
+ function renderShellBanner(snapshot) {
1781
+ const spec = snapshot.context.spec?.name ?? snapshot.context.spec?.path ?? "no spec";
1782
+ return [
1783
+ chalk2.bold.cyan("Tuned Tensor"),
1784
+ ` ${chalk2.bold(snapshot.mode)} \xB7 ${snapshot.context.projectName} \xB7 ${spec}`,
1785
+ chalk2.dim(" /help for commands \xB7 /mode cloud|local to switch workflows"),
1786
+ ""
1787
+ ].join("\n");
1788
+ }
1789
+ function renderShellPrompt(snapshot) {
1790
+ return `${chalk2.cyan("tt")} ${chalk2.bold(snapshot.mode)} ${chalk2.dim(snapshot.context.projectName)} \u203A `;
1791
+ }
1792
+ var TunedTensorShellSession = class _TunedTensorShellSession {
1793
+ constructor(runner, io, env, contextProvider, initialContext) {
1794
+ this.runner = runner;
1795
+ this.io = io;
1796
+ this.env = env;
1797
+ this.contextProvider = contextProvider;
1798
+ this.mode = initialContext.inferredTarget;
1799
+ this.modeSource = initialContext.targetSource;
1800
+ this.cwd = initialContext.cwd;
1801
+ this.context = initialContext;
1802
+ }
1803
+ mode;
1804
+ modeSource;
1805
+ cwd;
1806
+ context;
1807
+ static async create(options) {
1808
+ const env = options.env ?? process.env;
1809
+ const cwd = resolve3(options.cwd ?? process.cwd());
1810
+ const contextProvider = options.contextProvider ?? ((input) => discoverShellContext(input));
1811
+ const initialContext = await contextProvider({ cwd, env });
1812
+ return new _TunedTensorShellSession(
1813
+ options.runner,
1814
+ options.io,
1815
+ env,
1816
+ contextProvider,
1817
+ initialContext
1818
+ );
1819
+ }
1820
+ snapshot() {
1821
+ return {
1822
+ mode: this.mode,
1823
+ modeSource: this.modeSource,
1824
+ cwd: this.cwd,
1825
+ context: this.context
1826
+ };
1827
+ }
1828
+ prompt() {
1829
+ return renderShellPrompt(this.snapshot());
1830
+ }
1831
+ banner() {
1832
+ return renderShellBanner(this.snapshot());
1833
+ }
1834
+ async refreshContext() {
1835
+ this.context = await this.contextProvider({ cwd: this.cwd, env: this.env });
1836
+ this.cwd = this.context.cwd;
1837
+ if (this.modeSource !== "session" && this.modeSource !== "environment") {
1838
+ this.mode = this.context.inferredTarget;
1839
+ this.modeSource = this.context.targetSource;
1840
+ }
1841
+ }
1842
+ writeLines(lines) {
1843
+ this.io.write(`${lines.join("\n")}
1844
+ `);
1845
+ }
1846
+ async handleSlash(command) {
1847
+ switch (command.name) {
1848
+ case "palette":
1849
+ this.io.write(helpText(this.mode, void 0, true));
1850
+ return "continue";
1851
+ case "help":
1852
+ this.io.write(helpText(this.mode, command.args.join(" ") || void 0));
1853
+ return "continue";
1854
+ case "status":
1855
+ assertNoArgs("status", command.args);
1856
+ await this.refreshContext();
1857
+ this.writeLines(formatShellStatus(this.context, this.mode));
1858
+ return "continue";
1859
+ case "context":
1860
+ assertNoArgs("context", command.args);
1861
+ await this.refreshContext();
1862
+ this.writeLines(formatShellContext(this.context, this.mode, this.modeSource));
1863
+ return "continue";
1864
+ case "mode": {
1865
+ if (command.args.length === 0) {
1866
+ this.io.write(`Current workflow: ${this.mode}
1867
+ `);
1868
+ return "continue";
1869
+ }
1870
+ if (command.args.length !== 1 || !["cloud", "local"].includes(command.args[0])) {
1871
+ throw new ShellParseError("Usage: /mode cloud|local");
1872
+ }
1873
+ this.mode = command.args[0];
1874
+ this.modeSource = "session";
1875
+ this.io.write(`Workflow switched to ${this.mode}.
1876
+ `);
1877
+ return "continue";
1878
+ }
1879
+ case "clear":
1880
+ assertNoArgs("clear", command.args);
1881
+ this.io.clear();
1882
+ return "continue";
1883
+ case "cd": {
1884
+ if (command.args.length === 0) {
1885
+ this.io.write(`${this.cwd}
1886
+ `);
1887
+ return "continue";
1888
+ }
1889
+ if (command.args.length !== 1) {
1890
+ throw new ShellParseError("Usage: /cd <directory>; quote paths containing spaces.");
1891
+ }
1892
+ const nextDirectory = expandDirectory(command.args[0], this.cwd, this.env);
1893
+ const metadata = await stat3(nextDirectory).catch(() => null);
1894
+ if (!metadata?.isDirectory()) {
1895
+ throw new ShellParseError(`Not a directory: ${nextDirectory}`);
1896
+ }
1897
+ this.cwd = nextDirectory;
1898
+ await this.refreshContext();
1899
+ this.io.write(`Directory: ${this.cwd}
1900
+ `);
1901
+ return "continue";
1902
+ }
1903
+ case "exit":
1904
+ assertNoArgs("exit", command.args);
1905
+ return "exit";
1906
+ }
1907
+ }
1908
+ async handleLine(input) {
1909
+ try {
1910
+ const slash = parseSlashCommand(input);
1911
+ if (slash) return await this.handleSlash(slash);
1912
+ const routed = routeShellCommand(input, this.mode);
1913
+ if (!routed) return "continue";
1914
+ await this.runner({
1915
+ target: routed.target,
1916
+ args: [...routed.args],
1917
+ cwd: this.cwd
1918
+ });
1919
+ await this.refreshContext();
1920
+ return "continue";
1921
+ } catch (error) {
1922
+ this.io.writeError(`${chalk2.red("Error:")} ${errorMessage(error)}
1923
+ `);
1924
+ return "continue";
1925
+ }
167
1926
  }
1927
+ };
1928
+ async function createShellSession(options) {
1929
+ return TunedTensorShellSession.create(options);
168
1930
  }
169
- function printSuccess(message) {
170
- console.log(chalk.green("\u2713") + " " + message);
1931
+ function streamIsTTY(stream) {
1932
+ return Boolean(stream.isTTY);
171
1933
  }
172
- function printWarning(message) {
173
- console.log(chalk.yellow("!") + " " + message);
1934
+ async function withForegroundSignalHandoff(control, run) {
1935
+ const signals = control.signals ?? process;
1936
+ const restoreRawMode = control.input.isRaw === true && typeof control.input.setRawMode === "function";
1937
+ const keepParentAlive = () => {
1938
+ };
1939
+ control.pauseReadline();
1940
+ if (restoreRawMode) control.input.setRawMode(false);
1941
+ signals.on("SIGINT", keepParentAlive);
1942
+ try {
1943
+ return await run();
1944
+ } finally {
1945
+ signals.off("SIGINT", keepParentAlive);
1946
+ if (restoreRawMode) control.input.setRawMode(true);
1947
+ control.resumeReadline();
1948
+ }
174
1949
  }
175
- function printError(message) {
176
- console.error(chalk.red("\u2717") + " " + message);
1950
+ function streamIO(output, error) {
1951
+ return {
1952
+ write(text2) {
1953
+ output.write(text2);
1954
+ },
1955
+ writeError(text2) {
1956
+ error.write(text2);
1957
+ },
1958
+ clear() {
1959
+ output.write("\x1B[2J\x1B[H");
1960
+ }
1961
+ };
177
1962
  }
178
- function reportError(err) {
179
- const isApi = err instanceof ApiError;
180
- const message = err?.message || String(err);
181
- if (jsonMode) {
182
- printJson({
183
- error: {
184
- status: isApi ? err.status : null,
185
- code: isApi ? err.code : "CLI_ERROR",
186
- message
1963
+ async function startInteractiveShell(options) {
1964
+ const input = options.input ?? process.stdin;
1965
+ const output = options.output ?? process.stdout;
1966
+ const error = options.error ?? process.stderr;
1967
+ if (options.requireTTY !== false && (!streamIsTTY(input) || !streamIsTTY(output))) {
1968
+ throw new Error("The interactive TT shell requires a TTY.");
1969
+ }
1970
+ let readline;
1971
+ const terminalInput = input;
1972
+ const foregroundRunner = (request2) => withForegroundSignalHandoff(
1973
+ {
1974
+ input: terminalInput,
1975
+ pauseReadline: () => readline?.pause(),
1976
+ resumeReadline: () => readline?.resume()
1977
+ },
1978
+ () => options.runner(request2)
1979
+ );
1980
+ const session = await createShellSession({
1981
+ runner: foregroundRunner,
1982
+ io: streamIO(output, error),
1983
+ cwd: options.cwd,
1984
+ env: options.env
1985
+ });
1986
+ const completer = createCommandCompleter(() => session.snapshot().mode);
1987
+ readline = createInterface({
1988
+ input,
1989
+ output,
1990
+ terminal: true,
1991
+ historySize: 100,
1992
+ removeHistoryDuplicates: true,
1993
+ completer
1994
+ });
1995
+ readline.on("SIGINT", () => {
1996
+ output.write("\n");
1997
+ readline?.setPrompt(session.prompt());
1998
+ readline?.write(null, { ctrl: true, name: "u" });
1999
+ });
2000
+ output.write(session.banner());
2001
+ readline.setPrompt(session.prompt());
2002
+ readline.prompt();
2003
+ try {
2004
+ for await (const line of readline) {
2005
+ const action = await session.handleLine(line);
2006
+ if (action === "exit") {
2007
+ readline.close();
2008
+ break;
187
2009
  }
188
- });
189
- } else if (isApi) {
190
- printError(`[${err.status}] ${message}`);
191
- } else {
192
- printError(message);
2010
+ readline.setPrompt(session.prompt());
2011
+ readline.prompt();
2012
+ }
2013
+ } finally {
2014
+ readline.close();
193
2015
  }
194
2016
  }
195
- function formatDate(iso) {
196
- if (!iso) return "\u2014";
197
- return new Date(iso).toLocaleString();
198
- }
199
- function formatStatus(status) {
200
- const map = {
201
- completed: chalk.green,
202
- running: chalk.blue,
203
- training: chalk.blue,
204
- evaluating: chalk.blue,
205
- preparing: chalk.yellow,
206
- pending: chalk.yellow,
207
- uploading: chalk.yellow,
208
- failed: chalk.red,
209
- cancelled: chalk.dim,
210
- validated: chalk.green,
211
- invalid: chalk.red
212
- };
213
- const colorFn = map[status] || chalk.white;
214
- return colorFn(status);
215
- }
216
- function truncate(str, max) {
217
- if (str.length <= max) return str;
218
- return str.slice(0, max - 1) + "\u2026";
2017
+ function ciEnabled(value) {
2018
+ if (!value) return false;
2019
+ return !["0", "false", "no", "off"].includes(value.trim().toLowerCase());
219
2020
  }
220
- function shortId(id) {
221
- return id.slice(0, 8);
2021
+ function shouldStartInteractiveShell(options) {
2022
+ const env = options.env ?? process.env;
2023
+ return options.args.length === 0 && options.stdinIsTTY === true && options.stdoutIsTTY === true && !ciEnabled(env.CI) && env.TERM !== "dumb";
222
2024
  }
223
2025
 
224
2026
  // src/commands/auth.ts
225
- import { createInterface } from "readline/promises";
2027
+ import { createInterface as createInterface2 } from "readline/promises";
226
2028
  import { stdin, stdout } from "process";
227
- import chalk2 from "chalk";
2029
+ import { Writable } from "stream";
2030
+ import chalk3 from "chalk";
2031
+ async function promptForApiKey(input = stdin, output = stdout) {
2032
+ if (input.isTTY !== true || output.isTTY !== true) {
2033
+ throw new Error(
2034
+ "An API key is required in non-interactive mode. Pass it to `tt auth login <key>` or set TUNED_TENSOR_API_KEY."
2035
+ );
2036
+ }
2037
+ let muted = false;
2038
+ const maskedOutput = new Writable({
2039
+ write(chunk, _encoding, callback) {
2040
+ if (!muted) output.write(chunk);
2041
+ callback();
2042
+ }
2043
+ });
2044
+ const rl = createInterface2({
2045
+ input,
2046
+ output: maskedOutput,
2047
+ terminal: true
2048
+ });
2049
+ try {
2050
+ const pending = rl.question("Enter your API key (tt_...): ");
2051
+ muted = true;
2052
+ return await pending;
2053
+ } finally {
2054
+ muted = false;
2055
+ output.write("\n");
2056
+ rl.close();
2057
+ }
2058
+ }
228
2059
  function registerAuthCommands(parent) {
229
2060
  const auth = parent.command("auth").description("Manage authentication");
230
2061
  auth.command("login").description("Store an API key for authentication").argument("[key]", "API key (tt_...). If omitted, you will be prompted.").action(async (key) => {
231
2062
  let apiKey = key;
232
2063
  if (!apiKey) {
2064
+ if (isJsonMode()) {
2065
+ throw new Error(
2066
+ "An API key argument is required in JSON mode. Use `tt --json auth login <key>`."
2067
+ );
2068
+ }
233
2069
  const settingsUrl = `${DEFAULT_BASE_URL}/dashboard/settings`;
234
2070
  console.log();
235
2071
  console.log(
236
- ` To get your API key, go to ${chalk2.bold("Settings \u2192 API Keys")} in the Tuned Tensor dashboard:`
2072
+ ` To get your API key, go to ${chalk3.bold("Settings \u2192 API Keys")} in the Tuned Tensor dashboard:`
237
2073
  );
238
- console.log(` ${chalk2.cyan.underline(settingsUrl)}`);
2074
+ console.log(` ${chalk3.cyan.underline(settingsUrl)}`);
239
2075
  console.log();
240
- const rl = createInterface({ input: stdin, output: stdout });
241
- apiKey = await rl.question("Enter your API key (tt_...): ");
242
- rl.close();
2076
+ apiKey = await promptForApiKey();
243
2077
  }
244
2078
  apiKey = apiKey.trim();
245
2079
  if (!apiKey.startsWith("tt_") || apiKey.length !== 51) {
246
- printError(
2080
+ throw new Error(
247
2081
  "Invalid API key format. Keys start with tt_ and are 51 characters long."
248
2082
  );
249
- process.exit(1);
250
2083
  }
251
2084
  updateConfig({ api_key: apiKey });
2085
+ if (isJsonMode()) {
2086
+ printJson({
2087
+ authenticated: true,
2088
+ key_prefix: `${apiKey.slice(0, 8)}...`,
2089
+ base_url: getBaseUrl(parent.opts())
2090
+ });
2091
+ return;
2092
+ }
252
2093
  printSuccess(
253
2094
  `API key stored (${apiKey.slice(0, 8)}...). You're ready to go.`
254
2095
  );
255
2096
  });
256
2097
  auth.command("logout").description("Remove stored credentials").action(() => {
257
2098
  clearConfig();
2099
+ if (isJsonMode()) {
2100
+ printJson({ authenticated: false });
2101
+ return;
2102
+ }
258
2103
  printSuccess("Credentials removed.");
259
2104
  });
260
2105
  auth.command("status").description("Show current authentication status").action(() => {
@@ -382,87 +2227,7 @@ function resolveLabelingJobId(prefix, opts) {
382
2227
  );
383
2228
  }
384
2229
 
385
- // src/base-models.ts
386
- var SUPPORTED_BASE_MODELS = [
387
- "google/gemma-4-E2B-it",
388
- "google/gemma-4-E4B-it",
389
- "Qwen/Qwen3.5-2B",
390
- "Qwen/Qwen3-VL-2B-Instruct",
391
- "Qwen/Qwen3.5-4B",
392
- "meta-llama/Llama-3.2-3B-Instruct",
393
- "microsoft/Phi-4-mini-instruct",
394
- "ibm-granite/granite-3.3-2b-instruct",
395
- "bigcode/starcoder2-3b"
396
- ];
397
- var BASE_MODEL_ALIASES = /* @__PURE__ */ new Map();
398
- for (const model of SUPPORTED_BASE_MODELS) {
399
- BASE_MODEL_ALIASES.set(model.toLowerCase(), model);
400
- }
401
- for (const [alias, model] of [
402
- ["google/gemma-4-E2B", "google/gemma-4-E2B-it"],
403
- ["google/gemma-4-e2b", "google/gemma-4-E2B-it"],
404
- ["google/gemma-4-e2b-it", "google/gemma-4-E2B-it"],
405
- ["google/gemma-4-E4B", "google/gemma-4-E4B-it"],
406
- ["google/gemma-4-e4b", "google/gemma-4-E4B-it"],
407
- ["google/gemma-4-e4b-it", "google/gemma-4-E4B-it"],
408
- ["qwen/qwen3.5-2b", "Qwen/Qwen3.5-2B"],
409
- ["Qwen/Qwen3.5-2B-Base", "Qwen/Qwen3.5-2B"],
410
- ["qwen/qwen3.5-2b-base", "Qwen/Qwen3.5-2B"],
411
- ["qwen/qwen3-vl-2b-instruct", "Qwen/Qwen3-VL-2B-Instruct"],
412
- ["Qwen/Qwen3-VL-2B", "Qwen/Qwen3-VL-2B-Instruct"],
413
- ["qwen/qwen3-vl-2b", "Qwen/Qwen3-VL-2B-Instruct"],
414
- ["qwen/qwen3.5-4b", "Qwen/Qwen3.5-4B"],
415
- ["Qwen/Qwen3.5-4B-Base", "Qwen/Qwen3.5-4B"],
416
- ["qwen/qwen3.5-4b-base", "Qwen/Qwen3.5-4B"],
417
- ["meta-llama/llama-3.2-3b-instruct", "meta-llama/Llama-3.2-3B-Instruct"],
418
- ["meta-llama/Llama-3.2-3B", "meta-llama/Llama-3.2-3B-Instruct"],
419
- ["meta-llama/llama-3.2-3b", "meta-llama/Llama-3.2-3B-Instruct"],
420
- ["microsoft/phi-4-mini-instruct", "microsoft/Phi-4-mini-instruct"],
421
- ["phi-4-mini-instruct", "microsoft/Phi-4-mini-instruct"],
422
- ["ibm-granite/granite-3.3-2b-instruct", "ibm-granite/granite-3.3-2b-instruct"],
423
- ["granite-3.3-2b-instruct", "ibm-granite/granite-3.3-2b-instruct"],
424
- ["bigcode/starcoder2-3b", "bigcode/starcoder2-3b"],
425
- ["starcoder2-3b", "bigcode/starcoder2-3b"]
426
- ]) {
427
- BASE_MODEL_ALIASES.set(alias.toLowerCase(), model);
428
- }
429
- var UnsupportedBaseModelError = class extends Error {
430
- constructor(model) {
431
- const supported = SUPPORTED_BASE_MODELS.join(", ");
432
- super(
433
- `Unsupported base_model "${String(model)}". Supported base models: ${supported}`
434
- );
435
- this.name = "UnsupportedBaseModelError";
436
- }
437
- };
438
- function canonicalizeBaseModel(model) {
439
- if (typeof model !== "string" || !model.trim()) {
440
- throw new UnsupportedBaseModelError(model);
441
- }
442
- const canonical = BASE_MODEL_ALIASES.get(model.trim().toLowerCase());
443
- if (!canonical) {
444
- throw new UnsupportedBaseModelError(model);
445
- }
446
- return canonical;
447
- }
448
- function canonicalizeSpecBaseModel(body) {
449
- if ("base_model" in body && body.base_model !== void 0) {
450
- return { ...body, base_model: canonicalizeBaseModel(body.base_model) };
451
- }
452
- return body;
453
- }
454
-
455
2230
  // src/commands/specs.ts
456
- var SPEC_BODY_KEYS = /* @__PURE__ */ new Set([
457
- "name",
458
- "description",
459
- "base_model",
460
- "system_prompt",
461
- "guidelines",
462
- "constraints",
463
- "examples",
464
- "eval_cases"
465
- ]);
466
2231
  var RUN_INPUT_KEYS = ["run_id", "behavior_spec_id", "spec_snapshot", "run_number"];
467
2232
  var SpecBodyError = class extends Error {
468
2233
  };
@@ -493,15 +2258,13 @@ function loadSpecBody(filePath, mode) {
493
2258
  `${filePath} is missing required field "name" (string).`
494
2259
  );
495
2260
  }
496
- const unknown = Object.keys(body).filter(
497
- (k) => !SPEC_BODY_KEYS.has(k) && k !== "id" && k !== "eval_cases"
498
- );
2261
+ const unknown = unknownProjectSpecKeys(body);
499
2262
  if (unknown.length && !isJsonMode()) {
500
2263
  printWarning(
501
- `Unknown spec field(s) in ${filePath}: ${unknown.join(", ")}. They will be sent but may be rejected by the API.`
2264
+ `Unknown spec field(s) in ${filePath}: ${unknown.join(", ")}. They will be ignored by the cloud API.`
502
2265
  );
503
2266
  }
504
- return canonicalizeSpecBaseModel(body);
2267
+ return projectCloudSpec(body).body;
505
2268
  }
506
2269
  function registerSpecsCommands(parent) {
507
2270
  const specs = parent.command("specs").description("Manage behaviour specs");
@@ -1195,18 +2958,18 @@ function findControlChar(value) {
1195
2958
  function formatCodepoint(code) {
1196
2959
  return `U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
1197
2960
  }
1198
- function detectRowFormat(record) {
1199
- const input = record.input;
1200
- if (input && typeof input === "object" && typeof input.prompt === "string" && Array.isArray(input.assets) && input.assets.length > 0 && typeof record.output === "string") {
2961
+ function detectRowFormat(record2) {
2962
+ const input = record2.input;
2963
+ if (input && typeof input === "object" && typeof input.prompt === "string" && Array.isArray(input.assets) && input.assets.length > 0 && typeof record2.output === "string") {
1201
2964
  return "document_ocr_jsonl";
1202
2965
  }
1203
- if (typeof record.input === "string" || typeof record.output === "string" || !("input" in record) || !("output" in record)) {
2966
+ if (typeof record2.input === "string" || typeof record2.output === "string" || !("input" in record2) || !("output" in record2)) {
1204
2967
  return "jsonl";
1205
2968
  }
1206
2969
  return null;
1207
2970
  }
1208
- function validateDocumentOcrRow(record, rowNumber, errors) {
1209
- const input = record.input;
2971
+ function validateDocumentOcrRow(record2, rowNumber, errors) {
2972
+ const input = record2.input;
1210
2973
  const assets = input.assets;
1211
2974
  if (typeof input.prompt !== "string" || input.prompt.trim().length === 0) {
1212
2975
  errors.push(`Row ${rowNumber}: OCR input.prompt must be a non-empty string`);
@@ -1218,10 +2981,10 @@ function validateDocumentOcrRow(record, rowNumber, errors) {
1218
2981
  );
1219
2982
  }
1220
2983
  }
1221
- if (typeof record.output !== "string" || record.output.trim().length === 0) {
2984
+ if (typeof record2.output !== "string" || record2.output.trim().length === 0) {
1222
2985
  errors.push(`Row ${rowNumber}: OCR output must be a non-empty string`);
1223
2986
  } else {
1224
- const hit = findControlChar(record.output);
2987
+ const hit = findControlChar(record2.output);
1225
2988
  if (hit) {
1226
2989
  errors.push(
1227
2990
  `Row ${rowNumber}: OCR output contains invisible control character ${formatCodepoint(hit.codepoint)} (${hit.name}) at offset ${hit.offset} \u2014 strip or escape before uploading; some training paths split on it`
@@ -1290,14 +3053,14 @@ function validateDatasetFile(file, requestedFormat) {
1290
3053
  errors.push(`Row ${rowNumber}: expected an object with "input" and "output" fields`);
1291
3054
  continue;
1292
3055
  }
1293
- const record = row;
1294
- if ("messages" in record && !("input" in record) && !("output" in record)) {
3056
+ const record2 = row;
3057
+ if ("messages" in record2 && !("input" in record2) && !("output" in record2)) {
1295
3058
  errors.push(
1296
3059
  `Row ${rowNumber}: found OpenAI SFT-style "messages"; Tuned Tensor datasets require flat "input" and "output" strings`
1297
3060
  );
1298
3061
  continue;
1299
3062
  }
1300
- const rowFormat = detectRowFormat(record);
3063
+ const rowFormat = detectRowFormat(record2);
1301
3064
  if (!rowFormat) {
1302
3065
  errors.push(
1303
3066
  `Row ${rowNumber}: expected text {"input": string, "output": string} or OCR {"input":{"prompt": string, "assets": [...]}, "output": string}`
@@ -1314,17 +3077,17 @@ function validateDatasetFile(file, requestedFormat) {
1314
3077
  continue;
1315
3078
  }
1316
3079
  if (rowFormat === "document_ocr_jsonl") {
1317
- validateDocumentOcrRow(record, rowNumber, errors);
3080
+ validateDocumentOcrRow(record2, rowNumber, errors);
1318
3081
  continue;
1319
3082
  }
1320
- if (typeof record.input !== "string") {
3083
+ if (typeof record2.input !== "string") {
1321
3084
  errors.push(`Row ${rowNumber}: missing string "input" field`);
1322
3085
  }
1323
- if (typeof record.output !== "string") {
3086
+ if (typeof record2.output !== "string") {
1324
3087
  errors.push(`Row ${rowNumber}: missing string "output" field`);
1325
3088
  }
1326
3089
  for (const field of ["input", "output"]) {
1327
- const value = record[field];
3090
+ const value = record2[field];
1328
3091
  if (typeof value !== "string") continue;
1329
3092
  const hit = findControlChar(value);
1330
3093
  if (hit) {
@@ -1452,7 +3215,7 @@ function formatCents2(cents) {
1452
3215
  return `$${(cents / 100).toFixed(2)}`;
1453
3216
  }
1454
3217
  function sleep(ms) {
1455
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3218
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
1456
3219
  }
1457
3220
  function parseCsvHeader(line) {
1458
3221
  const fields = [];
@@ -1845,7 +3608,7 @@ function registerLabelCommands(parent) {
1845
3608
  }
1846
3609
 
1847
3610
  // src/commands/models.ts
1848
- import { spawn as spawn2, execFileSync } from "child_process";
3611
+ import { spawn as spawn3, execFileSync } from "child_process";
1849
3612
  import { createHash } from "crypto";
1850
3613
  import {
1851
3614
  existsSync as existsSync5,
@@ -1858,14 +3621,14 @@ import {
1858
3621
  readdirSync,
1859
3622
  rmSync
1860
3623
  } from "fs";
1861
- import { homedir as homedir2 } from "os";
1862
- import { dirname as dirname2, join as join2, resolve as resolve2, basename as basename2, normalize, isAbsolute } from "path";
3624
+ import { homedir as homedir4 } from "os";
3625
+ import { dirname as dirname4, join as join4, resolve as resolve5, basename as basename4, normalize, isAbsolute as isAbsolute4 } from "path";
1863
3626
  import { Readable, Transform } from "stream";
1864
3627
  import { pipeline } from "stream/promises";
1865
3628
 
1866
3629
  // src/commands/init.ts
1867
3630
  import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1868
- import { resolve } from "path";
3631
+ import { resolve as resolve4 } from "path";
1869
3632
  var DEFAULT_SPEC_FILE = "tunedtensor.json";
1870
3633
  var SCAFFOLD = {
1871
3634
  name: "My Agent",
@@ -1875,30 +3638,45 @@ var SCAFFOLD = {
1875
3638
  guidelines: [],
1876
3639
  constraints: [],
1877
3640
  examples: [
1878
- { input: "Hello", output: "Hi! How can I help you today?" }
1879
- ],
1880
- eval_cases: []
3641
+ { input: "Hello", output: "Hi! How can I help you today?" },
3642
+ {
3643
+ input: "Summarize this update: The launch moved to Friday.",
3644
+ output: "The launch is now scheduled for Friday."
3645
+ }
3646
+ ]
1881
3647
  };
1882
3648
  function registerInitCommand(parent) {
1883
- parent.command("init").description("Create a local behaviour spec 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) => {
1884
- const filePath = resolve(cmdOpts.file);
3649
+ 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) => {
3650
+ const filePath = resolve4(cmdOpts.file);
1885
3651
  if (existsSync4(filePath)) {
3652
+ if (isJsonMode()) {
3653
+ printJson({ created: false, path: filePath });
3654
+ return;
3655
+ }
1886
3656
  printWarning(`${cmdOpts.file} already exists. Use tt eval to validate it or edit it directly.`);
1887
3657
  return;
1888
3658
  }
1889
- const spec = { ...SCAFFOLD };
3659
+ const spec = {
3660
+ ...SCAFFOLD,
3661
+ examples: SCAFFOLD.examples.map((example) => ({ ...example }))
3662
+ };
1890
3663
  if (cmdOpts.name) spec.name = cmdOpts.name;
1891
3664
  if (cmdOpts.model) spec.base_model = canonicalizeBaseModel(cmdOpts.model);
1892
3665
  writeFileSync2(filePath, JSON.stringify(spec, null, 2) + "\n");
3666
+ if (isJsonMode()) {
3667
+ printJson({ created: true, path: filePath, spec });
3668
+ return;
3669
+ }
1893
3670
  printSuccess(`Created ${cmdOpts.file}`);
1894
3671
  console.log("\nNext steps:");
1895
3672
  console.log(" 1. Edit the spec: system_prompt, guidelines, examples");
1896
- console.log(" 2. Validate spec: tt eval");
1897
- console.log(" 3. Push to remote: tt push");
3673
+ console.log(" 2. Validate for cloud: tt eval");
3674
+ console.log(" 3. Push to cloud: tt push");
3675
+ console.log(" Or validate locally: tt local validate");
1898
3676
  });
1899
3677
  }
1900
3678
  function loadSpec(filePath) {
1901
- const resolved = resolve(filePath || DEFAULT_SPEC_FILE);
3679
+ const resolved = resolve4(filePath || DEFAULT_SPEC_FILE);
1902
3680
  if (!existsSync4(resolved)) {
1903
3681
  printError(
1904
3682
  `Spec file not found: ${filePath || DEFAULT_SPEC_FILE}
@@ -1911,30 +3689,30 @@ Run \`tt init\` to create one.`
1911
3689
  }
1912
3690
 
1913
3691
  // src/serve-manager.ts
1914
- import { spawn } from "child_process";
1915
- import { randomUUID } from "crypto";
3692
+ import { spawn as spawn2 } from "child_process";
3693
+ import { randomUUID as randomUUID2 } from "crypto";
1916
3694
  import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
1917
3695
  import { createServer } from "http";
1918
3696
  import { createServer as createNetServer } from "net";
1919
- import { dirname } from "path";
3697
+ import { dirname as dirname3 } from "path";
1920
3698
  var COMPLETION_ROUTES = /* @__PURE__ */ new Set(["/v1/chat/completions", "/chat/completions"]);
1921
3699
  function parsePositiveInteger(value, fallback) {
1922
3700
  return Number.isInteger(value) && value >= 0 ? value : fallback;
1923
3701
  }
1924
3702
  function sleep2(ms) {
1925
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3703
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
1926
3704
  }
1927
3705
  function readRequestBody(req) {
1928
- return new Promise((resolve4, reject) => {
3706
+ return new Promise((resolve7, reject) => {
1929
3707
  const chunks = [];
1930
3708
  req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1931
- req.on("end", () => resolve4(Buffer.concat(chunks)));
3709
+ req.on("end", () => resolve7(Buffer.concat(chunks)));
1932
3710
  req.on("error", reject);
1933
3711
  });
1934
3712
  }
1935
- function sendJson(res, status, payload) {
3713
+ function sendJson(res, status2, payload) {
1936
3714
  const body = Buffer.from(JSON.stringify(payload), "utf8");
1937
- res.writeHead(status, {
3715
+ res.writeHead(status2, {
1938
3716
  "content-type": "application/json",
1939
3717
  "content-length": String(body.byteLength),
1940
3718
  "access-control-allow-origin": "*",
@@ -1996,7 +3774,7 @@ function errorSummary(response, fallback) {
1996
3774
  return typeof message === "string" ? message : fallback;
1997
3775
  }
1998
3776
  async function allocatePort(host) {
1999
- return new Promise((resolve4, reject) => {
3777
+ return new Promise((resolve7, reject) => {
2000
3778
  const server = createNetServer();
2001
3779
  server.on("error", reject);
2002
3780
  server.listen(0, host, () => {
@@ -2006,14 +3784,14 @@ async function allocatePort(host) {
2006
3784
  return;
2007
3785
  }
2008
3786
  const port = address.port;
2009
- server.close(() => resolve4(port));
3787
+ server.close(() => resolve7(port));
2010
3788
  });
2011
3789
  });
2012
3790
  }
2013
3791
  var ManagedModelServer = class {
2014
3792
  constructor(options) {
2015
3793
  this.options = options;
2016
- this.spawnModelServer = options.spawnModelServer ?? spawn;
3794
+ this.spawnModelServer = options.spawnModelServer ?? spawn2;
2017
3795
  this.idleTimeoutMs = parsePositiveInteger(options.idleTimeoutSeconds, 300) * 1e3;
2018
3796
  this.restartAfterRequests = parsePositiveInteger(options.restartAfterRequests, 100);
2019
3797
  this.logRecordOverride = options.logRecord;
@@ -2046,11 +3824,11 @@ var ManagedModelServer = class {
2046
3824
  sendJson(res, 500, { error: { message: err instanceof Error ? err.message : String(err) } });
2047
3825
  });
2048
3826
  });
2049
- await new Promise((resolve4, reject) => {
3827
+ await new Promise((resolve7, reject) => {
2050
3828
  this.server?.once("error", reject);
2051
3829
  this.server?.listen(this.options.publicPort, this.options.publicHost, () => {
2052
3830
  this.server?.off("error", reject);
2053
- resolve4();
3831
+ resolve7();
2054
3832
  });
2055
3833
  });
2056
3834
  }
@@ -2060,15 +3838,15 @@ var ManagedModelServer = class {
2060
3838
  if (!this.server) return;
2061
3839
  const server = this.server;
2062
3840
  this.server = null;
2063
- await new Promise((resolve4, reject) => {
2064
- server.close((err) => err ? reject(err) : resolve4());
3841
+ await new Promise((resolve7, reject) => {
3842
+ server.close((err) => err ? reject(err) : resolve7());
2065
3843
  });
2066
3844
  }
2067
3845
  async run() {
2068
3846
  await this.start();
2069
- await new Promise((resolve4, reject) => {
3847
+ await new Promise((resolve7, reject) => {
2070
3848
  this.server?.once("error", reject);
2071
- this.server?.once("close", resolve4);
3849
+ this.server?.once("close", resolve7);
2072
3850
  });
2073
3851
  }
2074
3852
  async handle(req, res) {
@@ -2136,20 +3914,20 @@ var ManagedModelServer = class {
2136
3914
  });
2137
3915
  }
2138
3916
  async processGeneration(route, body, res, receivedAt) {
2139
- const requestId = `ttreq-${randomUUID()}`;
3917
+ const requestId = `ttreq-${randomUUID2()}`;
2140
3918
  const startedAt = Date.now();
2141
3919
  const queuedMs = startedAt - receivedAt;
2142
3920
  this.activeRequests += 1;
2143
3921
  this.clearIdleTimer();
2144
- let status = 500;
3922
+ let status2 = 500;
2145
3923
  let parsed = null;
2146
3924
  let failure = null;
2147
3925
  try {
2148
3926
  await this.ensureModelReady();
2149
3927
  const forwarded = await this.forwardGeneration(route, body);
2150
- status = forwarded.status;
3928
+ status2 = forwarded.status;
2151
3929
  parsed = forwarded.parsed;
2152
- res.writeHead(status, {
3930
+ res.writeHead(status2, {
2153
3931
  ...copyResponseHeaders(forwarded.headers),
2154
3932
  "content-length": String(Buffer.byteLength(forwarded.body))
2155
3933
  });
@@ -2158,8 +3936,8 @@ var ManagedModelServer = class {
2158
3936
  failure = errorSummary(parsed, null);
2159
3937
  } catch (err) {
2160
3938
  failure = err instanceof Error ? err.message : String(err);
2161
- status = 502;
2162
- sendJson(res, status, { error: { message: failure } });
3939
+ status2 = 502;
3940
+ sendJson(res, status2, { error: { message: failure } });
2163
3941
  } finally {
2164
3942
  const finishedAt = Date.now();
2165
3943
  this.activeRequests -= 1;
@@ -2172,13 +3950,13 @@ var ManagedModelServer = class {
2172
3950
  path: this.options.modelPath
2173
3951
  },
2174
3952
  request_bytes: body.byteLength,
2175
- response_status: status,
3953
+ response_status: status2,
2176
3954
  latency_ms: finishedAt - receivedAt,
2177
3955
  queued_ms: queuedMs,
2178
3956
  prompt_tokens: usageNumber(parsed, "prompt_tokens"),
2179
3957
  completion_tokens: usageNumber(parsed, "completion_tokens"),
2180
3958
  total_tokens: usageNumber(parsed, "total_tokens"),
2181
- schema_validity: status < 400 ? true : status === 422 ? false : null,
3959
+ schema_validity: status2 < 400 ? true : status2 === 422 ? false : null,
2182
3960
  gate_field: this.options.gateField,
2183
3961
  gate_result: this.extractGateResult(parsed),
2184
3962
  restart_count: this.restartCount,
@@ -2242,7 +4020,7 @@ var ManagedModelServer = class {
2242
4020
  try {
2243
4021
  child.kill("SIGTERM");
2244
4022
  await Promise.race([
2245
- new Promise((resolve4) => child.once("exit", () => resolve4())),
4023
+ new Promise((resolve7) => child.once("exit", () => resolve7())),
2246
4024
  sleep2(5e3).then(() => {
2247
4025
  if (child.exitCode == null && child.signalCode == null) child.kill("SIGKILL");
2248
4026
  })
@@ -2259,8 +4037,8 @@ var ManagedModelServer = class {
2259
4037
  const response = await fetch(`http://127.0.0.1:${this.internalPort}/health`, {
2260
4038
  signal: controller.signal
2261
4039
  });
2262
- const text = await response.text();
2263
- return { ok: response.ok, body: tryJsonParse(text) };
4040
+ const text2 = await response.text();
4041
+ return { ok: response.ok, body: tryJsonParse(text2) };
2264
4042
  } catch {
2265
4043
  return { ok: false, body: null };
2266
4044
  } finally {
@@ -2284,12 +4062,12 @@ var ManagedModelServer = class {
2284
4062
  headers: { "content-type": "application/json" },
2285
4063
  body
2286
4064
  });
2287
- const text = await response.text();
4065
+ const text2 = await response.text();
2288
4066
  return {
2289
4067
  status: response.status,
2290
4068
  headers: response.headers,
2291
- body: text,
2292
- parsed: tryJsonParse(text)
4069
+ body: text2,
4070
+ parsed: tryJsonParse(text2)
2293
4071
  };
2294
4072
  }
2295
4073
  extractGateResult(response) {
@@ -2312,15 +4090,15 @@ var ManagedModelServer = class {
2312
4090
  clearTimeout(this.idleTimer);
2313
4091
  this.idleTimer = null;
2314
4092
  }
2315
- writeLog(record) {
4093
+ writeLog(record2) {
2316
4094
  if (this.logRecordOverride) {
2317
- this.logRecordOverride(record);
4095
+ this.logRecordOverride(record2);
2318
4096
  return;
2319
4097
  }
2320
- const line = `${JSON.stringify(record)}
4098
+ const line = `${JSON.stringify(record2)}
2321
4099
  `;
2322
4100
  if (this.options.logFile) {
2323
- mkdirSync2(dirname(this.options.logFile), { recursive: true });
4101
+ mkdirSync2(dirname3(this.options.logFile), { recursive: true });
2324
4102
  appendFileSync(this.options.logFile, line, "utf8");
2325
4103
  return;
2326
4104
  }
@@ -2868,7 +4646,7 @@ if __name__ == "__main__":
2868
4646
  function resolveOutputPath(output, filename) {
2869
4647
  if (!output) return filename;
2870
4648
  if (existsSync5(output) && statSync4(output).isDirectory()) {
2871
- return join2(output, filename);
4649
+ return join4(output, filename);
2872
4650
  }
2873
4651
  return output;
2874
4652
  }
@@ -2944,7 +4722,7 @@ async function downloadUrlToFile(url, outputPath) {
2944
4722
  if (!res.body) {
2945
4723
  throw new Error("Download failed: response body was empty");
2946
4724
  }
2947
- mkdirSync3(dirname2(outputPath), { recursive: true });
4725
+ mkdirSync3(dirname4(outputPath), { recursive: true });
2948
4726
  const file = createWriteStream(outputPath);
2949
4727
  const contentLength = parseContentLength(res.headers.get("content-length"));
2950
4728
  const progress = createDownloadProgress(
@@ -2975,15 +4753,15 @@ async function downloadUrlToFile(url, outputPath) {
2975
4753
  return contentLength;
2976
4754
  }
2977
4755
  function defaultCacheDir() {
2978
- const root = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
2979
- return join2(root, "tuned-tensor", "models");
4756
+ const root = process.env.XDG_CACHE_HOME || join4(homedir4(), ".cache");
4757
+ return join4(root, "tuned-tensor", "models");
2980
4758
  }
2981
4759
  function runtimeDir(cacheDir) {
2982
- return join2(cacheDir, "_runtime");
4760
+ return join4(cacheDir, "_runtime");
2983
4761
  }
2984
4762
  function runtimePythonPath(cacheDir) {
2985
4763
  const dir = runtimeDir(cacheDir);
2986
- return process.platform === "win32" ? join2(dir, "Scripts", "python.exe") : join2(dir, "bin", "python");
4764
+ return process.platform === "win32" ? join4(dir, "Scripts", "python.exe") : join4(dir, "bin", "python");
2987
4765
  }
2988
4766
  function resolveServePython(cacheDir, explicitPython) {
2989
4767
  if (explicitPython) return explicitPython;
@@ -3056,7 +4834,7 @@ print(json.dumps({"missing": missing}))
3056
4834
  }
3057
4835
  }
3058
4836
  function setupRuntimeCommands(python, venvDir, deps) {
3059
- const venvPython = process.platform === "win32" ? join2(venvDir, "Scripts", "python.exe") : join2(venvDir, "bin", "python");
4837
+ const venvPython = process.platform === "win32" ? join4(venvDir, "Scripts", "python.exe") : join4(venvDir, "bin", "python");
3060
4838
  return [
3061
4839
  [python, "-m", "venv", venvDir],
3062
4840
  [venvPython, "-m", "pip", "install", "--upgrade", "pip"],
@@ -3071,7 +4849,7 @@ function directoryHasFiles(path) {
3071
4849
  }
3072
4850
  function validateArchiveEntryPath(entry) {
3073
4851
  const normalized = normalize(entry).replace(/^[\\/]+/, "");
3074
- if (!entry.trim() || isAbsolute(entry) || normalized === ".." || normalized.startsWith(`..${"/"}`) || normalized.startsWith(`..${"\\"}`)) {
4852
+ if (!entry.trim() || isAbsolute4(entry) || normalized === ".." || normalized.startsWith(`..${"/"}`) || normalized.startsWith(`..${"\\"}`)) {
3075
4853
  throw new Error(`Unsafe archive entry path: ${entry}`);
3076
4854
  }
3077
4855
  return normalized;
@@ -3094,9 +4872,9 @@ function extractArchive(archivePath, targetDir, force) {
3094
4872
  return targetDir;
3095
4873
  }
3096
4874
  function localArchiveCacheDir(archivePath, cacheDir) {
3097
- const resolved = resolve2(archivePath);
4875
+ const resolved = resolve5(archivePath);
3098
4876
  const digest = createHash("sha256").update(resolved).digest("hex").slice(0, 12);
3099
- return join2(cacheDir, `local-${safeSegment(basename2(archivePath))}-${digest}`);
4877
+ return join4(cacheDir, `local-${safeSegment(basename4(archivePath))}-${digest}`);
3100
4878
  }
3101
4879
  function buildSystemPrompt(spec) {
3102
4880
  const parts = [];
@@ -3112,9 +4890,9 @@ ${spec.constraints.map((c) => `- ${c}`).join("\n")}`);
3112
4890
  return parts.join("\n\n");
3113
4891
  }
3114
4892
  function findSpecPath(explicitPath, modelPath) {
3115
- const candidates = explicitPath ? [explicitPath] : [join2(process.cwd(), DEFAULT_SPEC_FILE), join2(modelPath, DEFAULT_SPEC_FILE)];
4893
+ const candidates = explicitPath ? [explicitPath] : [join4(process.cwd(), DEFAULT_SPEC_FILE), join4(modelPath, DEFAULT_SPEC_FILE)];
3116
4894
  for (const candidate of candidates) {
3117
- const resolved = resolve2(candidate);
4895
+ const resolved = resolve5(candidate);
3118
4896
  if (existsSync5(resolved) && statSync4(resolved).isFile()) return resolved;
3119
4897
  }
3120
4898
  return null;
@@ -3124,7 +4902,7 @@ function loadSystemPromptFromSpec(specPath) {
3124
4902
  return buildSystemPrompt(spec);
3125
4903
  }
3126
4904
  function loadJsonSchemaForServe(schemaPath) {
3127
- const resolved = resolve2(schemaPath);
4905
+ const resolved = resolve5(schemaPath);
3128
4906
  if (!existsSync5(resolved) || !statSync4(resolved).isFile()) {
3129
4907
  throw new Error(`JSON schema file not found: ${schemaPath}`);
3130
4908
  }
@@ -3136,28 +4914,28 @@ function loadJsonSchemaForServe(schemaPath) {
3136
4914
  }
3137
4915
  }
3138
4916
  function writeReferenceServerScript(cacheDir) {
3139
- const scriptDir = join2(cacheDir, "_server");
4917
+ const scriptDir = join4(cacheDir, "_server");
3140
4918
  mkdirSync3(scriptDir, { recursive: true });
3141
- const scriptPath = join2(scriptDir, "openai_reference_server.py");
4919
+ const scriptPath = join4(scriptDir, "openai_reference_server.py");
3142
4920
  writeFileSync3(scriptPath, REFERENCE_SERVER_SCRIPT, "utf8");
3143
4921
  return scriptPath;
3144
4922
  }
3145
4923
  async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
3146
4924
  if (existsSync5(target)) {
3147
- const resolved = resolve2(target);
4925
+ const resolved = resolve5(target);
3148
4926
  const stats = statSync4(resolved);
3149
4927
  if (stats.isDirectory()) {
3150
4928
  return {
3151
4929
  modelPath: resolved,
3152
- modelName: basename2(resolved),
4930
+ modelName: basename4(resolved),
3153
4931
  source: "local-directory"
3154
4932
  };
3155
4933
  }
3156
4934
  if (stats.isFile() && (resolved.endsWith(".tar.gz") || resolved.endsWith(".tgz"))) {
3157
- const extractDir2 = join2(localArchiveCacheDir(resolved, cacheDir), "model");
4935
+ const extractDir2 = join4(localArchiveCacheDir(resolved, cacheDir), "model");
3158
4936
  return {
3159
4937
  modelPath: extractArchive(resolved, extractDir2, forceDownload),
3160
- modelName: basename2(resolved).replace(/\.t(ar\.)?gz$/, ""),
4938
+ modelName: basename4(resolved).replace(/\.t(ar\.)?gz$/, ""),
3161
4939
  source: "local-archive"
3162
4940
  };
3163
4941
  }
@@ -3166,9 +4944,9 @@ async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
3166
4944
  const fullId = await resolveModelId(target, opts);
3167
4945
  const { data: model } = await get(`/models/${fullId}`, void 0, opts);
3168
4946
  const { data } = await get(`/models/${fullId}/download`, void 0, opts);
3169
- const modelCacheDir = join2(cacheDir, fullId);
3170
- const archivePath = join2(modelCacheDir, data.filename);
3171
- const extractDir = join2(modelCacheDir, "model");
4947
+ const modelCacheDir = join4(cacheDir, fullId);
4948
+ const archivePath = join4(modelCacheDir, data.filename);
4949
+ const extractDir = join4(modelCacheDir, "model");
3172
4950
  if (!existsSync5(archivePath) || forceDownload) {
3173
4951
  await downloadUrlToFile(data.url, archivePath);
3174
4952
  }
@@ -3274,7 +5052,7 @@ function ollamaSlug(name) {
3274
5052
  function firstExisting(candidates) {
3275
5053
  for (const candidate of candidates) {
3276
5054
  if (candidate && existsSync5(candidate) && statSync4(candidate).isFile()) {
3277
- return resolve2(candidate);
5055
+ return resolve5(candidate);
3278
5056
  }
3279
5057
  }
3280
5058
  return null;
@@ -3284,14 +5062,14 @@ function llamaCppDir(explicit) {
3284
5062
  }
3285
5063
  function resolveConvertScript(opts, required) {
3286
5064
  if (opts.convertScript) {
3287
- const resolved = resolve2(opts.convertScript);
5065
+ const resolved = resolve5(opts.convertScript);
3288
5066
  if (required && !existsSync5(resolved)) {
3289
5067
  throw new Error(`Conversion script not found: ${opts.convertScript}`);
3290
5068
  }
3291
5069
  return resolved;
3292
5070
  }
3293
5071
  const dir = llamaCppDir(opts.llamaCpp);
3294
- const candidates = dir ? [join2(dir, "convert_hf_to_gguf.py"), join2(dir, "convert-hf-to-gguf.py")] : [];
5072
+ const candidates = dir ? [join4(dir, "convert_hf_to_gguf.py"), join4(dir, "convert-hf-to-gguf.py")] : [];
3295
5073
  const found = firstExisting(candidates);
3296
5074
  if (found) return found;
3297
5075
  if (required) {
@@ -3299,11 +5077,11 @@ function resolveConvertScript(opts, required) {
3299
5077
  "Could not find convert_hf_to_gguf.py. Pass --llama-cpp <dir> (a llama.cpp checkout) or --convert-script <path>. Get it from https://github.com/ggml-org/llama.cpp."
3300
5078
  );
3301
5079
  }
3302
- return dir ? join2(dir, "convert_hf_to_gguf.py") : "convert_hf_to_gguf.py";
5080
+ return dir ? join4(dir, "convert_hf_to_gguf.py") : "convert_hf_to_gguf.py";
3303
5081
  }
3304
5082
  function resolveQuantizeBin(opts, required) {
3305
5083
  if (opts.quantizeBin) {
3306
- const resolved = resolve2(opts.quantizeBin);
5084
+ const resolved = resolve5(opts.quantizeBin);
3307
5085
  if (required && !existsSync5(resolved)) {
3308
5086
  throw new Error(`llama-quantize binary not found: ${opts.quantizeBin}`);
3309
5087
  }
@@ -3312,10 +5090,10 @@ function resolveQuantizeBin(opts, required) {
3312
5090
  const dir = llamaCppDir(opts.llamaCpp);
3313
5091
  if (dir) {
3314
5092
  const candidates = [
3315
- join2(dir, "build", "bin", "llama-quantize"),
3316
- join2(dir, "build", "bin", "quantize"),
3317
- join2(dir, "llama-quantize"),
3318
- join2(dir, "quantize")
5093
+ join4(dir, "build", "bin", "llama-quantize"),
5094
+ join4(dir, "build", "bin", "quantize"),
5095
+ join4(dir, "llama-quantize"),
5096
+ join4(dir, "quantize")
3319
5097
  ];
3320
5098
  const found = firstExisting(candidates);
3321
5099
  if (found) return found;
@@ -3463,7 +5241,7 @@ function registerModelsCommands(parent) {
3463
5241
  }
3464
5242
  const quantPlan = planQuant(String(cmdOpts.quant));
3465
5243
  const printOnly = Boolean(cmdOpts.printCommand);
3466
- const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
5244
+ const cacheDir = resolve5(cmdOpts.cacheDir || defaultCacheDir());
3467
5245
  mkdirSync3(cacheDir, { recursive: true });
3468
5246
  const serveTarget = await resolveServeTarget(
3469
5247
  target,
@@ -3472,9 +5250,9 @@ function registerModelsCommands(parent) {
3472
5250
  Boolean(cmdOpts.forceDownload)
3473
5251
  );
3474
5252
  const slug = ollamaSlug(serveTarget.modelName);
3475
- const outputDir = resolve2(cmdOpts.output || join2(process.cwd(), `${slug}-gguf`));
3476
- const finalPath = join2(outputDir, `${slug}.${quantPlan.quant}.gguf`);
3477
- const intermediatePath = quantPlan.requiresQuantize ? join2(outputDir, `${slug}.f16.gguf`) : void 0;
5253
+ const outputDir = resolve5(cmdOpts.output || join4(process.cwd(), `${slug}-gguf`));
5254
+ const finalPath = join4(outputDir, `${slug}.${quantPlan.quant}.gguf`);
5255
+ const intermediatePath = quantPlan.requiresQuantize ? join4(outputDir, `${slug}.f16.gguf`) : void 0;
3478
5256
  const convertScript = resolveConvertScript(cmdOpts, !printOnly);
3479
5257
  const convertOutfile = intermediatePath ?? finalPath;
3480
5258
  const steps = [
@@ -3512,8 +5290,8 @@ function registerModelsCommands(parent) {
3512
5290
  }
3513
5291
  if (specPath) systemPrompt = loadSystemPromptFromSpec(specPath);
3514
5292
  }
3515
- modelfilePath = join2(outputDir, "Modelfile");
3516
- modelfileContent = buildModelfile(basename2(finalPath), systemPrompt);
5293
+ modelfilePath = join4(outputDir, "Modelfile");
5294
+ modelfileContent = buildModelfile(basename4(finalPath), systemPrompt);
3517
5295
  const createModel = cmdOpts.ollamaCreate !== false;
3518
5296
  if (createModel) {
3519
5297
  steps.push(
@@ -3599,7 +5377,7 @@ function registerModelsCommands(parent) {
3599
5377
  }
3600
5378
  });
3601
5379
  models.command("setup-runtime").description("Install an isolated Python runtime for local model serving").option("--python <path>", "Python 3.10-3.12 executable to create the runtime").option("--cache-dir <path>", "Cache directory for the managed runtime").option("-f, --force", "Recreate the runtime if it already exists").option("--print-command", "Print the setup commands without running them").action(async (cmdOpts) => {
3602
- const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
5380
+ const cacheDir = resolve5(cmdOpts.cacheDir || defaultCacheDir());
3603
5381
  const venvDir = runtimeDir(cacheDir);
3604
5382
  const venvPython = runtimePythonPath(cacheDir);
3605
5383
  const python = pickRuntimePython(cmdOpts.python);
@@ -3645,7 +5423,7 @@ function registerModelsCommands(parent) {
3645
5423
  });
3646
5424
  models.command("serve").description("Serve a downloaded model with an OpenAI-compatible local API").argument("<target>", "Model ID/prefix, model directory, or .tar.gz artifact").option("--spec <path>", "Behavior spec JSON to apply as the default system prompt").option("--no-spec-prompt", "Do not auto-apply a behavior spec system prompt").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "8000").option("--python <path>", "Python executable").option("--cache-dir <path>", "Cache directory for downloaded/extracted models").option("--device <device>", "Inference device: auto, cpu, cuda, or mps", "auto").option("--force-download", "Re-download and re-extract model artifacts").option("--max-tokens <n>", "Default max completion tokens", "512").option("--temperature <n>", "Default sampling temperature", "0.7").option("--json-schema <path>", "JSON Schema file to enforce by default for chat completions").option("--json-repair-attempts <n>", "Number of JSON/schema repair retries", "1").option("--trust-remote-code", "Pass trust_remote_code=True to transformers").option("--managed", "Run a local lifecycle manager in front of the model server").option("--idle-timeout <seconds>", "Managed mode idle timeout before stopping the model", "300").option("--restart-after-requests <n>", "Managed mode restart threshold; 0 disables request-count restarts", "100").option("--gate-field <field>", "Response JSON field to log as the managed serving gate result", "should_process").option("--log-file <path>", "Write managed serving JSONL request logs to a file").option("--print-command", "Print the underlying Python command without starting it").action(async (target, cmdOpts) => {
3647
5425
  const opts = parent.opts();
3648
- const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
5426
+ const cacheDir = resolve5(cmdOpts.cacheDir || defaultCacheDir());
3649
5427
  mkdirSync3(cacheDir, { recursive: true });
3650
5428
  const python = resolveServePython(cacheDir, cmdOpts.python);
3651
5429
  if (!cmdOpts.printCommand) ensureServingRuntime(python, cacheDir);
@@ -3694,7 +5472,7 @@ function registerModelsCommands(parent) {
3694
5472
  idle_timeout_seconds: idleTimeoutSeconds,
3695
5473
  restart_after_requests: restartAfterRequests,
3696
5474
  gate_field: String(cmdOpts.gateField),
3697
- log_file: cmdOpts.logFile ? resolve2(cmdOpts.logFile) : void 0
5475
+ log_file: cmdOpts.logFile ? resolve5(cmdOpts.logFile) : void 0
3698
5476
  } : void 0;
3699
5477
  if (cmdOpts.printCommand) return printServeCommand(python, args, env, managedConfig);
3700
5478
  if (!isJsonMode()) {
@@ -3731,12 +5509,12 @@ function registerModelsCommands(parent) {
3731
5509
  idleTimeoutSeconds,
3732
5510
  restartAfterRequests,
3733
5511
  gateField: String(cmdOpts.gateField),
3734
- logFile: cmdOpts.logFile ? resolve2(cmdOpts.logFile) : void 0
5512
+ logFile: cmdOpts.logFile ? resolve5(cmdOpts.logFile) : void 0
3735
5513
  });
3736
5514
  return;
3737
5515
  }
3738
5516
  await new Promise((resolvePromise, reject) => {
3739
- const child = spawn2(python, args, {
5517
+ const child = spawn3(python, args, {
3740
5518
  stdio: "inherit",
3741
5519
  env: { ...process.env, ...env }
3742
5520
  });
@@ -3757,7 +5535,7 @@ function registerModelsCommands(parent) {
3757
5535
  }
3758
5536
 
3759
5537
  // src/commands/balance.ts
3760
- import chalk3 from "chalk";
5538
+ import chalk4 from "chalk";
3761
5539
  var KIND_LABELS = {
3762
5540
  signup_bonus: "Signup bonus",
3763
5541
  topup: "Top-up",
@@ -3772,8 +5550,8 @@ function formatCents3(cents) {
3772
5550
  return `${sign}$${(abs / 100).toFixed(2)}`;
3773
5551
  }
3774
5552
  function formatSigned(cents) {
3775
- if (cents > 0) return chalk3.green(`+${formatCents3(cents)}`);
3776
- if (cents < 0) return chalk3.red(formatCents3(cents));
5553
+ if (cents > 0) return chalk4.green(`+${formatCents3(cents)}`);
5554
+ if (cents < 0) return chalk4.red(formatCents3(cents));
3777
5555
  return formatCents3(cents);
3778
5556
  }
3779
5557
  function registerBalanceCommands(parent) {
@@ -3788,7 +5566,7 @@ function registerBalanceCommands(parent) {
3788
5566
  return printJson({ balance, transactions });
3789
5567
  }
3790
5568
  const lowBalance = balance.balance_cents < 100;
3791
- const balanceLine = lowBalance ? chalk3.red(formatCents3(balance.balance_cents)) + chalk3.dim(" (low)") : chalk3.bold.green(formatCents3(balance.balance_cents));
5569
+ const balanceLine = lowBalance ? chalk4.red(formatCents3(balance.balance_cents)) + chalk4.dim(" (low)") : chalk4.bold.green(formatCents3(balance.balance_cents));
3792
5570
  printDetail([
3793
5571
  ["Credits", balanceLine],
3794
5572
  [
@@ -3803,7 +5581,7 @@ function registerBalanceCommands(parent) {
3803
5581
  ]);
3804
5582
  if (transactions && transactions.length > 0) {
3805
5583
  console.log(`
3806
- ${chalk3.bold("Recent transactions")}`);
5584
+ ${chalk4.bold("Recent transactions")}`);
3807
5585
  printTable(
3808
5586
  ["Date", "Type", "Amount", "Balance", "Reference"],
3809
5587
  transactions.map((row) => [
@@ -3811,19 +5589,19 @@ ${chalk3.bold("Recent transactions")}`);
3811
5589
  KIND_LABELS[row.kind] ?? row.kind,
3812
5590
  formatSigned(row.amount_cents),
3813
5591
  formatCents3(row.balance_after_cents),
3814
- row.reference_id ? shortId(row.reference_id) : chalk3.dim("\u2014")
5592
+ row.reference_id ? shortId(row.reference_id) : chalk4.dim("\u2014")
3815
5593
  ])
3816
5594
  );
3817
5595
  } else {
3818
5596
  console.log(
3819
5597
  `
3820
- ${chalk3.dim("No transactions yet \u2014 run `tt topup` to add credits.")}`
5598
+ ${chalk4.dim("No transactions yet \u2014 run `tt topup` to add credits.")}`
3821
5599
  );
3822
5600
  }
3823
5601
  if (lowBalance) {
3824
5602
  console.log(
3825
5603
  `
3826
- ${formatStatus("preparing")} ${chalk3.yellow(
5604
+ ${formatStatus("preparing")} ${chalk4.yellow(
3827
5605
  "Run `tt topup` to add more credits before starting a run."
3828
5606
  )}`
3829
5607
  );
@@ -3832,7 +5610,7 @@ ${formatStatus("preparing")} ${chalk3.yellow(
3832
5610
  }
3833
5611
 
3834
5612
  // src/commands/topup.ts
3835
- import chalk4 from "chalk";
5613
+ import chalk5 from "chalk";
3836
5614
  import open from "open";
3837
5615
  var PRESETS_USD = [10, 25, 50, 100];
3838
5616
  var MIN_USD = 5;
@@ -3871,9 +5649,9 @@ function registerTopupCommands(parent) {
3871
5649
  "amount is required in --json mode (use --amount <usd>)"
3872
5650
  );
3873
5651
  }
3874
- console.log(chalk4.bold("Add credits via Stripe Checkout"));
5652
+ console.log(chalk5.bold("Add credits via Stripe Checkout"));
3875
5653
  console.log(
3876
- chalk4.dim(
5654
+ chalk5.dim(
3877
5655
  `Quick picks: ${PRESETS_USD.map((n) => `$${n}`).join(", ")}`
3878
5656
  )
3879
5657
  );
@@ -3900,8 +5678,8 @@ function registerTopupCommands(parent) {
3900
5678
  }
3901
5679
  const url = data.checkout_url;
3902
5680
  console.log(
3903
- `${chalk4.bold("Checkout URL:")} ${chalk4.cyan(url)}
3904
- ` + chalk4.dim("Complete the payment to credit your account.")
5681
+ `${chalk5.bold("Checkout URL:")} ${chalk5.cyan(url)}
5682
+ ` + chalk5.dim("Complete the payment to credit your account.")
3905
5683
  );
3906
5684
  if (options.open !== false) {
3907
5685
  try {
@@ -3909,7 +5687,7 @@ function registerTopupCommands(parent) {
3909
5687
  printSuccess("Opened Stripe Checkout in your browser.");
3910
5688
  } catch {
3911
5689
  console.log(
3912
- chalk4.yellow(
5690
+ chalk5.yellow(
3913
5691
  "Could not open browser automatically \u2014 copy the URL above."
3914
5692
  )
3915
5693
  );
@@ -3919,7 +5697,7 @@ function registerTopupCommands(parent) {
3919
5697
  }
3920
5698
 
3921
5699
  // src/commands/eval.ts
3922
- import chalk5 from "chalk";
5700
+ import chalk6 from "chalk";
3923
5701
 
3924
5702
  // src/eval/rules.ts
3925
5703
  function validateSpec(spec) {
@@ -4054,12 +5832,12 @@ function checkExamplesAgainstConstraints(spec) {
4054
5832
  }
4055
5833
  return violations;
4056
5834
  }
4057
- function evaluateConstraint(text, constraint) {
5835
+ function evaluateConstraint(text2, constraint) {
4058
5836
  const lower = constraint.toLowerCase();
4059
5837
  const neverMatch = lower.match(/^never (?:share|mention|include|reveal|use|say|output|return|give|provide|show|disclose|expose)\s+(.+)/);
4060
5838
  if (neverMatch) {
4061
5839
  const forbidden = neverMatch[1].replace(/['"]/g, "").trim();
4062
- if (text.toLowerCase().includes(forbidden.toLowerCase())) {
5840
+ if (text2.toLowerCase().includes(forbidden.toLowerCase())) {
4063
5841
  return { passed: false, message: `Output contains "${forbidden}"` };
4064
5842
  }
4065
5843
  return { passed: true };
@@ -4067,7 +5845,7 @@ function evaluateConstraint(text, constraint) {
4067
5845
  const alwaysMatch = lower.match(/^always (?:include|use|start with|end with|contain|respond in|reply in)\s+(.+)/);
4068
5846
  if (alwaysMatch) {
4069
5847
  const required = alwaysMatch[1].replace(/['"]/g, "").trim();
4070
- if (!text.toLowerCase().includes(required.toLowerCase())) {
5848
+ if (!text2.toLowerCase().includes(required.toLowerCase())) {
4071
5849
  return { passed: false, message: `Output missing "${required}"` };
4072
5850
  }
4073
5851
  return { passed: true };
@@ -4080,11 +5858,11 @@ function evaluateConstraint(text, constraint) {
4080
5858
 
4081
5859
  // src/commands/eval.ts
4082
5860
  function registerEvalCommand(parent) {
4083
- parent.command("eval").description("Validate a local behaviour spec").option("-m, --model <model>", "Deprecated; ignored because tt eval only validates the local spec").option("-f, --file <path>", "Spec file path", DEFAULT_SPEC_FILE).action(async (cmdOpts) => {
5861
+ parent.command("eval").description("Validate a behaviour spec project file").option("-m, --model <model>", "Deprecated; ignored because tt eval only validates the local spec").option("-f, --file <path>", "Spec file path", DEFAULT_SPEC_FILE).action(async (cmdOpts) => {
4084
5862
  const spec = loadSpec(cmdOpts.file);
4085
5863
  const validation = validateSpec(spec);
4086
5864
  if (!isJsonMode()) {
4087
- console.log(chalk5.dim(`
5865
+ console.log(chalk6.dim(`
4088
5866
  Spec: ${cmdOpts.file}
4089
5867
  `));
4090
5868
  }
@@ -4099,71 +5877,321 @@ Spec: ${cmdOpts.file}
4099
5877
  });
4100
5878
  }
4101
5879
  function printValidation(checks) {
4102
- console.log(chalk5.bold("Spec Validation"));
5880
+ console.log(chalk6.bold("Spec Validation"));
4103
5881
  for (const check of checks) {
4104
- const icon = check.passed ? chalk5.green("\u2713") : chalk5.red("\u2717");
4105
- const msg = check.message ? chalk5.dim(` \u2014 ${check.message}`) : "";
5882
+ const icon = check.passed ? chalk6.green("\u2713") : chalk6.red("\u2717");
5883
+ const msg = check.message ? chalk6.dim(` \u2014 ${check.message}`) : "";
4106
5884
  console.log(` ${icon} ${check.name}${msg}`);
4107
5885
  }
4108
5886
  }
4109
5887
 
4110
5888
  // src/commands/push.ts
4111
5889
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
4112
- import { resolve as resolve3 } from "path";
5890
+ import { resolve as resolve6 } from "path";
5891
+ function persistRemoteId(filePath, id) {
5892
+ const raw = JSON.parse(readFileSync8(filePath, "utf-8"));
5893
+ raw.id = id;
5894
+ writeFileSync4(filePath, JSON.stringify(raw, null, 2) + "\n");
5895
+ }
4113
5896
  function registerPushCommand(parent) {
4114
5897
  parent.command("push").description("Push local spec to the Tuned Tensor API").option("-f, --file <path>", "Spec file path", DEFAULT_SPEC_FILE).action(async (cmdOpts) => {
4115
5898
  const opts = parent.opts();
4116
- const filePath = resolve3(cmdOpts.file);
5899
+ const filePath = resolve6(cmdOpts.file);
4117
5900
  const spec = loadSpec(cmdOpts.file);
4118
5901
  const evalCaseErrors = validateEvalCases(spec);
4119
5902
  if (evalCaseErrors.length > 0) {
4120
5903
  throw new Error(`Invalid eval_cases: ${evalCaseErrors.join("; ")}`);
4121
5904
  }
4122
- const { id, ...rawBody } = spec;
4123
- const body = canonicalizeSpecBaseModel(rawBody);
5905
+ const rawSpec = spec;
5906
+ const { body } = projectCloudSpec(rawSpec);
5907
+ const id = spec.id;
5908
+ const canRecoverLocalId = Boolean(
5909
+ id && hasLocalOnlySpecFields(rawSpec)
5910
+ );
4124
5911
  let data;
5912
+ let created = false;
4125
5913
  if (id) {
4126
- const res = await put(`/behavior-specs/${id}`, body, opts);
4127
- data = res.data;
5914
+ try {
5915
+ const res = await put(
5916
+ `/behavior-specs/${id}`,
5917
+ body,
5918
+ opts
5919
+ );
5920
+ data = res.data;
5921
+ } catch (error) {
5922
+ if (!canRecoverLocalId || !(error instanceof ApiError) || error.status !== 404) {
5923
+ throw error;
5924
+ }
5925
+ const res = await post("/behavior-specs", body, opts);
5926
+ data = res.data;
5927
+ created = true;
5928
+ persistRemoteId(filePath, data.id);
5929
+ }
4128
5930
  } else {
4129
5931
  const res = await post("/behavior-specs", body, opts);
4130
5932
  data = res.data;
4131
- const raw = JSON.parse(readFileSync8(filePath, "utf-8"));
4132
- raw.id = data.id;
4133
- writeFileSync4(filePath, JSON.stringify(raw, null, 2) + "\n");
5933
+ created = true;
5934
+ persistRemoteId(filePath, data.id);
4134
5935
  }
4135
5936
  if (isJsonMode()) return printJson(data);
4136
5937
  printSuccess(
4137
- `Spec ${id ? "updated" : "created"}: ${data.name} (${shortId(data.id)})`
5938
+ `Spec ${created ? "created" : "updated"}: ${data.name} (${shortId(data.id)})`
4138
5939
  );
4139
5940
  });
4140
5941
  }
4141
5942
 
4142
- // src/index.ts
4143
- var program = new Command();
4144
- program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.24").option("-k, --api-key <key>", "API key (overrides stored key)").option(
4145
- "-u, --base-url <url>",
4146
- "API base URL (default: https://tunedtensor.com)"
4147
- ).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
4148
- const rootOpts = program.opts();
4149
- if (rootOpts.json) setJsonMode(true);
4150
- if (rootOpts.color === false) {
4151
- process.env.FORCE_COLOR = "0";
5943
+ // src/cli.ts
5944
+ async function runSelfCommand(args, options = {}) {
5945
+ const entrypoint = options.entrypoint ?? process.argv[1];
5946
+ if (!entrypoint) {
5947
+ throw new Error("Cannot locate the tt CLI entrypoint.");
5948
+ }
5949
+ return await new Promise((resolve7, reject) => {
5950
+ const child = spawn4(process.execPath, [entrypoint, ...args], {
5951
+ cwd: options.cwd,
5952
+ env: options.env ?? process.env,
5953
+ stdio: "inherit",
5954
+ detached: process.platform !== "win32"
5955
+ });
5956
+ const forwardSignal = (signal) => {
5957
+ const childSignal = signal === "SIGHUP" ? "SIGTERM" : signal;
5958
+ if (child.pid && process.platform !== "win32") {
5959
+ try {
5960
+ process.kill(-child.pid, childSignal);
5961
+ return;
5962
+ } catch {
5963
+ }
5964
+ }
5965
+ child.kill(childSignal);
5966
+ };
5967
+ const onSigint = () => forwardSignal("SIGINT");
5968
+ const onSigterm = () => forwardSignal("SIGTERM");
5969
+ const onSighup = () => forwardSignal("SIGHUP");
5970
+ const cleanup = () => {
5971
+ process.off("SIGINT", onSigint);
5972
+ process.off("SIGTERM", onSigterm);
5973
+ if (process.platform !== "win32") process.off("SIGHUP", onSighup);
5974
+ };
5975
+ process.on("SIGINT", onSigint);
5976
+ process.on("SIGTERM", onSigterm);
5977
+ if (process.platform !== "win32") process.on("SIGHUP", onSighup);
5978
+ child.once("error", (error) => {
5979
+ cleanup();
5980
+ reject(error);
5981
+ });
5982
+ child.once("close", (exitCode, signal) => {
5983
+ cleanup();
5984
+ const signalNumber = signal ? osConstants2.signals[signal] : void 0;
5985
+ resolve7({
5986
+ exitCode: exitCode ?? (typeof signalNumber === "number" ? 128 + signalNumber : 1),
5987
+ signal
5988
+ });
5989
+ });
5990
+ });
5991
+ }
5992
+ function extractPassthroughOptions(args, root) {
5993
+ let json = Boolean(root.json);
5994
+ let color = root.color !== false;
5995
+ const forwarded = [];
5996
+ for (const arg of args) {
5997
+ if (arg === "--json") {
5998
+ json = true;
5999
+ } else if (arg === "--no-color") {
6000
+ color = false;
6001
+ } else {
6002
+ forwarded.push(arg);
6003
+ }
4152
6004
  }
4153
- });
4154
- registerAuthCommands(program);
4155
- registerSpecsCommands(program);
4156
- registerRunsCommands(program);
4157
- registerDatasetsCommands(program);
4158
- registerLabelCommands(program);
4159
- registerModelsCommands(program);
4160
- registerBalanceCommands(program);
4161
- registerTopupCommands(program);
4162
- registerInitCommand(program);
4163
- registerEvalCommand(program);
4164
- registerPushCommand(program);
4165
- if (process.argv.includes("--json")) setJsonMode(true);
4166
- program.parseAsync().catch((err) => {
6005
+ return { args: forwarded, json, color };
6006
+ }
6007
+ function childEnvironment(root, base) {
6008
+ return {
6009
+ ...base,
6010
+ ...root.apiKey ? { TUNED_TENSOR_API_KEY: root.apiKey } : {},
6011
+ ...root.baseUrl ? { TUNED_TENSOR_URL: root.baseUrl } : {},
6012
+ ...root.color === false ? { FORCE_COLOR: "0" } : {}
6013
+ };
6014
+ }
6015
+ function applyExitCode(result) {
6016
+ if (result?.exitCode !== null && result?.exitCode !== void 0) {
6017
+ process.exitCode = result.exitCode;
6018
+ }
6019
+ }
6020
+ function createProgram(version, runtime = {}) {
6021
+ const program = new Command();
6022
+ const env = runtime.env ?? process.env;
6023
+ const invokeSelf = runtime.runSelfCommand ?? runSelfCommand;
6024
+ const invokeLocal = runtime.runLocalCommand ?? executeLocalCommand;
6025
+ const launchShell = runtime.startShell ?? startInteractiveShell;
6026
+ const cwd = runtime.cwd ?? process.cwd();
6027
+ const shellRunner = async (request2) => {
6028
+ const root = program.opts();
6029
+ const args = request2.target === "local" ? ["local", ...request2.args] : request2.args;
6030
+ return await invokeSelf(args, {
6031
+ cwd: request2.cwd,
6032
+ env: childEnvironment(root, env),
6033
+ entrypoint: runtime.argv?.[1]
6034
+ });
6035
+ };
6036
+ const openShell = async () => {
6037
+ const root = program.opts();
6038
+ const shellEnvironment = childEnvironment(root, env);
6039
+ await launchShell({
6040
+ runner: shellRunner,
6041
+ input: runtime.stdin ?? process.stdin,
6042
+ output: runtime.stdout ?? process.stdout,
6043
+ error: runtime.stderr ?? process.stderr,
6044
+ cwd,
6045
+ env: shellEnvironment
6046
+ });
6047
+ };
6048
+ program.name("tt").description("Tuned Tensor \u2014 one terminal for cloud and local fine-tuning").version(version).option("-k, --api-key <key>", "API key (overrides stored key)").option(
6049
+ "-u, --base-url <url>",
6050
+ "API base URL (default: https://tunedtensor.com)"
6051
+ ).option("--json", "Output raw JSON").option("--no-color", "Disable colors").showSuggestionAfterError().hook("preAction", () => {
6052
+ const root = program.opts();
6053
+ if (root.json) setJsonMode(true);
6054
+ if (root.color === false) {
6055
+ process.env.FORCE_COLOR = "0";
6056
+ chalk7.level = 0;
6057
+ }
6058
+ });
6059
+ registerAuthCommands(program);
6060
+ registerSpecsCommands(program);
6061
+ registerRunsCommands(program);
6062
+ registerDatasetsCommands(program);
6063
+ registerLabelCommands(program);
6064
+ registerModelsCommands(program);
6065
+ registerBalanceCommands(program);
6066
+ registerTopupCommands(program);
6067
+ registerInitCommand(program);
6068
+ registerEvalCommand(program);
6069
+ registerPushCommand(program);
6070
+ program.command("status").description("Show read-only cloud, local, and project context").option("--target <target>", "Show cloud or local workflow status").action(async (commandOptions) => {
6071
+ const root = program.opts();
6072
+ const context = await discoverShellContext({
6073
+ cwd,
6074
+ env: childEnvironment(root, env)
6075
+ });
6076
+ const target = commandOptions.target ?? context.inferredTarget;
6077
+ if (target !== "cloud" && target !== "local") {
6078
+ throw new Error("--target must be cloud or local.");
6079
+ }
6080
+ const output = runtime.stdout ?? process.stdout;
6081
+ if (isJsonMode()) {
6082
+ output.write(`${JSON.stringify({
6083
+ target,
6084
+ context
6085
+ }, null, 2)}
6086
+ `);
6087
+ return;
6088
+ }
6089
+ output.write(`${chalk7.bold.cyan("Tuned Tensor status")}
6090
+ `);
6091
+ output.write(`${formatShellStatus(context, target).join("\n")}
6092
+
6093
+ `);
6094
+ output.write(`${chalk7.bold("Context")}
6095
+ `);
6096
+ output.write(
6097
+ `${formatShellContext(
6098
+ context,
6099
+ target,
6100
+ commandOptions.target ? "command-option" : context.targetSource
6101
+ ).join("\n")}
6102
+ `
6103
+ );
6104
+ });
6105
+ program.command("local").description("Run the local CUDA training and model workflow").helpOption(false).allowUnknownOption().argument("[args...]", "TT Local command and arguments").action(async (args) => {
6106
+ const root = program.opts();
6107
+ const passthrough = extractPassthroughOptions(args, root);
6108
+ if (!passthrough.color) chalk7.level = 0;
6109
+ const result = await invokeLocal(
6110
+ passthrough.args.length > 0 ? passthrough.args : ["--help"],
6111
+ {
6112
+ jsonMode: passthrough.json,
6113
+ cwd,
6114
+ env: childEnvironment(
6115
+ { ...root, color: passthrough.color },
6116
+ env
6117
+ ),
6118
+ stdin: runtime.stdin ?? process.stdin,
6119
+ stdout: runtime.stdout ?? process.stdout,
6120
+ stderr: runtime.stderr ?? process.stderr
6121
+ }
6122
+ );
6123
+ applyExitCode(result);
6124
+ });
6125
+ program.command("cloud").description("Run a hosted command explicitly").helpOption(false).allowUnknownOption().argument("[args...]", "Hosted tt command and arguments").action(async (args) => {
6126
+ const root = program.opts();
6127
+ const passthrough = extractPassthroughOptions(args, root);
6128
+ const forwarded = [
6129
+ ...passthrough.json ? ["--json"] : [],
6130
+ ...!passthrough.color ? ["--no-color"] : [],
6131
+ ...passthrough.args.length > 0 ? passthrough.args : ["--help"]
6132
+ ];
6133
+ const result = await invokeSelf(forwarded, {
6134
+ cwd,
6135
+ env: childEnvironment(
6136
+ { ...root, color: passthrough.color },
6137
+ env
6138
+ ),
6139
+ entrypoint: runtime.argv?.[1]
6140
+ });
6141
+ applyExitCode(result);
6142
+ });
6143
+ program.command("shell").description("Open the interactive cloud/local terminal").action(openShell);
6144
+ program.addHelpText(
6145
+ "after",
6146
+ `
6147
+ Workflows:
6148
+ tt Open the interactive terminal (TTY only)
6149
+ tt status Inspect cloud/local project context
6150
+ tt cloud <command> Run a hosted command explicitly
6151
+ tt local <command> Run a local GPU command
6152
+
6153
+ Examples:
6154
+ tt runs list
6155
+ tt local doctor tunedtensor.json
6156
+ tt local run tunedtensor.json --dry-run
6157
+ `
6158
+ );
6159
+ return program;
6160
+ }
6161
+ async function runCli(version, runtime = {}) {
6162
+ const argv = runtime.argv ?? process.argv;
6163
+ const env = runtime.env ?? process.env;
6164
+ const args = argv.slice(2);
6165
+ if (shouldStartInteractiveShell({
6166
+ args,
6167
+ stdinIsTTY: runtime.stdinIsTTY ?? process.stdin.isTTY,
6168
+ stdoutIsTTY: runtime.stdoutIsTTY ?? process.stdout.isTTY,
6169
+ env
6170
+ })) {
6171
+ const invokeSelf = runtime.runSelfCommand ?? runSelfCommand;
6172
+ await (runtime.startShell ?? startInteractiveShell)({
6173
+ runner: async (request2) => await invokeSelf(
6174
+ request2.target === "local" ? ["local", ...request2.args] : request2.args,
6175
+ {
6176
+ cwd: request2.cwd,
6177
+ env,
6178
+ entrypoint: argv[1]
6179
+ }
6180
+ ),
6181
+ input: runtime.stdin ?? process.stdin,
6182
+ output: runtime.stdout ?? process.stdout,
6183
+ error: runtime.stderr ?? process.stderr,
6184
+ cwd: runtime.cwd ?? process.cwd(),
6185
+ env
6186
+ });
6187
+ return;
6188
+ }
6189
+ if (argv.includes("--json")) setJsonMode(true);
6190
+ await createProgram(version, { ...runtime, argv }).parseAsync(argv);
6191
+ }
6192
+
6193
+ // src/index.ts
6194
+ runCli("0.5.0").catch((err) => {
4167
6195
  reportError(err);
4168
6196
  process.exit(1);
4169
6197
  });