@tuned-tensor/cli 0.5.0 → 0.7.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
@@ -4,163 +4,7 @@
4
4
  import { spawn as spawn4 } from "child_process";
5
5
  import { constants as osConstants2 } from "os";
6
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
- }
160
-
161
- // src/output.ts
162
- import chalk from "chalk";
163
- import Table from "cli-table3";
7
+ import chalk8 from "chalk";
164
8
 
165
9
  // src/client.ts
166
10
  import { readFileSync as readFileSync2, statSync } from "fs";
@@ -279,6 +123,37 @@ async function request(method, path, options = {}) {
279
123
  if (res.status === 204) return { data: null };
280
124
  return await res.json();
281
125
  }
126
+ async function postStream(path, body, opts) {
127
+ const key = resolveKey(opts);
128
+ const url = buildUrl(path, void 0, opts);
129
+ const res = await fetch(url, {
130
+ method: "POST",
131
+ headers: {
132
+ Authorization: `Bearer ${key}`,
133
+ Accept: "text/event-stream",
134
+ "Content-Type": "application/json"
135
+ },
136
+ body: JSON.stringify(body),
137
+ signal: opts?.signal
138
+ });
139
+ if (!res.ok) {
140
+ let errBody;
141
+ try {
142
+ errBody = await res.json();
143
+ } catch {
144
+ throw new ApiError(res.status, "UNKNOWN", res.statusText);
145
+ }
146
+ throw new ApiError(
147
+ res.status,
148
+ errBody.error?.code || "UNKNOWN",
149
+ errBody.error?.message || res.statusText
150
+ );
151
+ }
152
+ if (!res.body) {
153
+ throw new ApiError(502, "EMPTY_STREAM", "The agent returned an empty response.");
154
+ }
155
+ return res;
156
+ }
282
157
  function get(path, query, opts) {
283
158
  return request("GET", path, { query, opts });
284
159
  }
@@ -295,1739 +170,2622 @@ function del(path, opts) {
295
170
  return request("DELETE", path, { opts });
296
171
  }
297
172
 
298
- // src/output.ts
299
- var jsonMode = false;
300
- function setJsonMode(enabled) {
301
- jsonMode = enabled;
302
- }
303
- function isJsonMode() {
304
- return jsonMode;
305
- }
306
- function printJson(data) {
307
- console.log(JSON.stringify(data, null, 2));
308
- }
309
- function printTable(headers, rows2, meta) {
310
- const table = new Table({
311
- head: headers.map((h) => chalk.bold.cyan(h)),
312
- style: { head: [], border: [] }
313
- });
314
- for (const row of rows2) {
315
- table.push(row);
316
- }
317
- console.log(table.toString());
318
- if (meta) {
319
- const totalPages = Math.ceil(meta.total / meta.per_page);
320
- console.log(
321
- chalk.dim(
322
- `
323
- Page ${meta.page}/${totalPages} (${meta.total} total)`
324
- )
325
- );
326
- }
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")}`);
173
+ // src/agent-client.ts
174
+ function record(value) {
175
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
176
+ }
177
+ function stringValue(value) {
178
+ return typeof value === "string" && value.length > 0 ? value : null;
179
+ }
180
+ function dataEnvelope(value) {
181
+ const outer = record(value);
182
+ return outer && "data" in outer ? outer.data : value;
183
+ }
184
+ function normalizeEvent(block) {
185
+ let eventName = null;
186
+ const dataLines = [];
187
+ for (const rawLine of block.split(/\r?\n/)) {
188
+ if (!rawLine || rawLine.startsWith(":")) continue;
189
+ const separator = rawLine.indexOf(":");
190
+ const field = separator === -1 ? rawLine : rawLine.slice(0, separator);
191
+ const rawValue = separator === -1 ? "" : rawLine.slice(separator + 1);
192
+ const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
193
+ if (field === "event") eventName = value;
194
+ if (field === "data") dataLines.push(value);
195
+ }
196
+ const rawData = dataLines.join("\n");
197
+ if (!eventName && !rawData) return null;
198
+ if (rawData === "[DONE]") return { type: "final", payload: {} };
199
+ let decoded = rawData;
200
+ if (rawData) {
201
+ try {
202
+ decoded = JSON.parse(rawData);
203
+ } catch {
204
+ decoded = rawData;
205
+ }
333
206
  }
207
+ const decodedRecord = record(decoded);
208
+ const inferredType = stringValue(decodedRecord?.type) ?? stringValue(decodedRecord?.event);
209
+ const nestedData = record(decodedRecord?.data);
210
+ const payload = decodedRecord && nestedData && inferredType ? nestedData : decodedRecord ?? (rawData ? { text: rawData } : {});
211
+ const type = (eventName ?? inferredType ?? "message").toLowerCase().replaceAll("-", "_");
212
+ return { type, payload };
334
213
  }
335
- function printSuccess(message) {
336
- console.log(chalk.green("\u2713") + " " + message);
337
- }
338
- function printWarning(message) {
339
- console.log(chalk.yellow("!") + " " + message);
214
+ function createAgentEventDecoder(onEvent) {
215
+ let buffer = "";
216
+ const drain = (flush) => {
217
+ while (true) {
218
+ const boundary = buffer.match(/\r?\n\r?\n/);
219
+ if (!boundary || boundary.index === void 0) break;
220
+ const block = buffer.slice(0, boundary.index);
221
+ buffer = buffer.slice(boundary.index + boundary[0].length);
222
+ const event = normalizeEvent(block);
223
+ if (event) onEvent(event);
224
+ }
225
+ if (flush && buffer) {
226
+ const event = normalizeEvent(buffer);
227
+ buffer = "";
228
+ if (event) onEvent(event);
229
+ }
230
+ };
231
+ return {
232
+ push(chunk) {
233
+ buffer += chunk;
234
+ drain(false);
235
+ },
236
+ finish() {
237
+ drain(true);
238
+ }
239
+ };
340
240
  }
341
- function printError(message) {
342
- console.error(chalk.red("\u2717") + " " + message);
241
+ async function consumeAgentEventStream(response, onEvent) {
242
+ if (!response.body) throw new Error("The agent returned an empty response.");
243
+ const reader = response.body.getReader();
244
+ const decoder = new TextDecoder();
245
+ const events = createAgentEventDecoder(onEvent);
246
+ while (true) {
247
+ const { value, done } = await reader.read();
248
+ events.push(decoder.decode(value, { stream: !done }));
249
+ if (done) break;
250
+ }
251
+ events.finish();
252
+ }
253
+ function normalizeAction(value) {
254
+ const input = record(value);
255
+ const id = stringValue(input?.id);
256
+ const title = stringValue(input?.title);
257
+ if (!input || !id || !title) return null;
258
+ return {
259
+ id,
260
+ title,
261
+ summary: stringValue(input.summary) ?? "",
262
+ risk: stringValue(input.risk) ?? "unknown",
263
+ status: stringValue(input.status) ?? void 0,
264
+ operation: stringValue(input.operation) ?? void 0,
265
+ thread_id: stringValue(input.thread_id) ?? void 0,
266
+ turn_id: stringValue(input.turn_id) ?? void 0,
267
+ arguments: input.arguments,
268
+ preview: input.preview,
269
+ method: stringValue(input.method) ?? void 0,
270
+ path: stringValue(input.path) ?? void 0
271
+ };
343
272
  }
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
273
+ async function streamResult(response, fallbackThreadId, onEvent) {
274
+ let threadId = fallbackThreadId;
275
+ let turnId = null;
276
+ let status2 = null;
277
+ let assistantText = "";
278
+ let sawFinal = false;
279
+ const actions = [];
280
+ await consumeAgentEventStream(response, (event) => {
281
+ const payload = event.payload;
282
+ if (event.type === "turn_started") {
283
+ threadId = stringValue(payload.thread_id) ?? stringValue(payload.threadId) ?? threadId;
284
+ turnId = stringValue(payload.turn_id) ?? stringValue(payload.turnId) ?? turnId;
285
+ } else if (event.type === "text_delta") {
286
+ assistantText += stringValue(payload.delta) ?? stringValue(payload.text) ?? stringValue(payload.content) ?? "";
287
+ } else if (event.type === "approval_required") {
288
+ const action = normalizeAction(payload.action ?? payload.approval ?? payload);
289
+ if (action && !actions.some((candidate) => candidate.id === action.id)) {
290
+ actions.push(action);
353
291
  }
354
- });
355
- } else if (isApi) {
356
- printError(`[${err.status}] ${message}`);
357
- } else {
358
- printError(message);
292
+ } else if (event.type === "final") {
293
+ sawFinal = true;
294
+ status2 = stringValue(payload.status) ?? status2;
295
+ threadId = stringValue(payload.thread_id) ?? stringValue(payload.threadId) ?? threadId;
296
+ turnId = stringValue(payload.turn_id) ?? stringValue(payload.turnId) ?? turnId;
297
+ }
298
+ onEvent(event);
299
+ });
300
+ if (!sawFinal) {
301
+ throw new Error("The agent response ended before its final status was confirmed.");
359
302
  }
303
+ return { threadId, turnId, status: status2, response: assistantText, actions };
360
304
  }
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
305
+ function createAgentClient(opts) {
306
+ return {
307
+ async createThread() {
308
+ const response = await post("/agent/threads", {}, opts);
309
+ return dataEnvelope(response);
310
+ },
311
+ async listThreads() {
312
+ const response = await get("/agent/threads", void 0, opts);
313
+ return dataEnvelope(response);
314
+ },
315
+ async getThread(id) {
316
+ const response = await get(`/agent/threads/${encodeURIComponent(id)}`, void 0, opts);
317
+ const detail = dataEnvelope(response);
318
+ return {
319
+ thread: detail.thread,
320
+ actions: (detail.actions ?? []).map((action) => normalizeAction(action)).filter((action) => action !== null)
321
+ };
322
+ },
323
+ async runTurn(threadId, prompt, onEvent, signal) {
324
+ const streamOpts = { ...opts, signal };
325
+ const response = await postStream(
326
+ `/agent/threads/${encodeURIComponent(threadId)}/turns`,
327
+ { prompt },
328
+ streamOpts
329
+ );
330
+ return await streamResult(response, threadId, onEvent);
331
+ },
332
+ async approveAction(actionId, onEvent, signal) {
333
+ const streamOpts = { ...opts, signal };
334
+ const response = await postStream(
335
+ `/agent/actions/${encodeURIComponent(actionId)}/approve`,
336
+ {},
337
+ streamOpts
338
+ );
339
+ return await streamResult(response, "", onEvent);
340
+ },
341
+ async rejectAction(actionId) {
342
+ await post(`/agent/actions/${encodeURIComponent(actionId)}/reject`, {}, opts);
343
+ }
378
344
  };
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
345
  }
389
346
 
390
- // src/local-output.ts
391
- function isRecord(value) {
392
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
347
+ // src/agent.ts
348
+ import chalk2 from "chalk";
349
+
350
+ // src/shell.ts
351
+ import chalk from "chalk";
352
+ import { stat as stat2 } from "fs/promises";
353
+ import { homedir as homedir3 } from "os";
354
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
355
+ import { createInterface } from "readline";
356
+
357
+ // src/command-catalog.ts
358
+ var CLOUD = ["cloud"];
359
+ var LOCAL = ["local"];
360
+ var BOTH = ["cloud", "local"];
361
+ var COMMAND_CATALOG = [
362
+ { path: "init", description: "Create a behaviour-spec project.", group: "Workflow", modes: BOTH },
363
+ { path: "eval", description: "Validate a managed-service behaviour spec.", group: "Workflow", modes: CLOUD },
364
+ { path: "push", description: "Create or update the cloud behaviour spec.", group: "Workflow", modes: CLOUD },
365
+ { path: "validate", description: "Validate a local fine-tuning project.", group: "Workflow", modes: LOCAL },
366
+ { path: "doctor", description: "Check the local host and run prerequisites.", group: "Workflow", modes: LOCAL },
367
+ { path: "run", description: "Run the local fine-tuning and evaluation workflow.", group: "Workflow", modes: LOCAL },
368
+ { path: "runs list", description: "List runs.", group: "Inspect", modes: BOTH },
369
+ { path: "runs get", description: "Show a run.", group: "Inspect", modes: BOTH },
370
+ { path: "runs report", description: "Show a run report.", group: "Inspect", modes: BOTH },
371
+ { path: "runs compare", description: "Compare two local run reports.", group: "Inspect", modes: LOCAL },
372
+ { path: "runs events", description: "Show local run progress events.", group: "Inspect", modes: LOCAL },
373
+ { path: "runs estimate", description: "Estimate managed run cost and duration.", group: "Workflow", modes: CLOUD },
374
+ { path: "runs start", description: "Start a managed fine-tuning run.", group: "Workflow", modes: CLOUD },
375
+ { path: "runs watch", description: "Watch a managed run.", group: "Inspect", modes: CLOUD },
376
+ { path: "runs diagnose", description: "Show managed run diagnostics.", group: "Inspect", modes: CLOUD },
377
+ { path: "runs cancel", description: "Cancel a managed run.", group: "Workflow", modes: CLOUD },
378
+ { path: "models list", description: "List models.", group: "Inspect", modes: BOTH },
379
+ { path: "models get", description: "Show a model.", group: "Inspect", modes: BOTH },
380
+ { path: "models base", description: "List managed-service base models.", group: "Inspect", modes: CLOUD },
381
+ { path: "models download", description: "Download a managed model artifact.", group: "Serving", modes: CLOUD },
382
+ { path: "models export", description: "Export a managed model to GGUF or Ollama.", group: "Serving", modes: CLOUD },
383
+ { path: "models setup-runtime", description: "Install the managed model-serving runtime.", group: "Serving", modes: CLOUD },
384
+ { path: "models serve", description: "Serve a model through an OpenAI-compatible API.", group: "Serving", modes: BOTH },
385
+ { path: "models delete", description: "Delete a managed model.", group: "Workflow", modes: CLOUD },
386
+ { path: "models verify", description: "Verify a local model artifact.", group: "Serving", modes: LOCAL },
387
+ { path: "models prefetch", description: "Download the local base-model snapshot.", group: "Serving", modes: LOCAL },
388
+ { path: "models verify-base", description: "Verify the local base-model snapshot.", group: "Serving", modes: LOCAL },
389
+ { path: "models active", description: "Show the active local model.", group: "Serving", modes: LOCAL },
390
+ { path: "models activate", description: "Activate a verified local model.", group: "Serving", modes: LOCAL },
391
+ { path: "models rollback", description: "Roll back the active local model.", group: "Serving", modes: LOCAL },
392
+ { path: "serve", description: "Serve a local adapter, active model, or base model.", group: "Serving", modes: LOCAL },
393
+ { path: "specs list", description: "List cloud behaviour specs.", group: "Inspect", modes: CLOUD },
394
+ { path: "specs get", description: "Show a cloud behaviour spec.", group: "Inspect", modes: CLOUD },
395
+ { path: "specs create", description: "Create a cloud behaviour spec.", group: "Workflow", modes: CLOUD },
396
+ { path: "specs update", description: "Update a cloud behaviour spec.", group: "Workflow", modes: CLOUD },
397
+ { path: "specs delete", description: "Delete a cloud behaviour spec.", group: "Workflow", modes: CLOUD },
398
+ { path: "datasets list", description: "List cloud datasets.", group: "Data", modes: CLOUD },
399
+ { path: "datasets get", description: "Show a cloud dataset.", group: "Data", modes: CLOUD },
400
+ { path: "datasets upload", description: "Upload a cloud dataset.", group: "Data", modes: CLOUD },
401
+ { path: "datasets delete", description: "Delete a cloud dataset.", group: "Data", modes: CLOUD },
402
+ { path: "label upload", description: "Start a cloud teacher-labeling job.", group: "Data", modes: CLOUD },
403
+ { path: "label watch", description: "Watch a cloud labeling job.", group: "Data", modes: CLOUD },
404
+ { path: "label list", description: "List cloud labeling jobs.", group: "Data", modes: CLOUD },
405
+ { path: "label status", description: "Show cloud labeling-job status.", group: "Data", modes: CLOUD },
406
+ { path: "label rows", description: "Review labeled rows.", group: "Data", modes: CLOUD },
407
+ { path: "label accept", description: "Accept labeled rows.", group: "Data", modes: CLOUD },
408
+ { path: "label reject", description: "Reject labeled rows.", group: "Data", modes: CLOUD },
409
+ { path: "label edit", description: "Edit a labeled row.", group: "Data", modes: CLOUD },
410
+ { path: "label promote", description: "Promote reviewed rows to a dataset.", group: "Data", modes: CLOUD },
411
+ { path: "label cancel", description: "Cancel a labeling job.", group: "Data", modes: CLOUD },
412
+ { path: "auth login", description: "Store a cloud API key.", group: "Account", modes: CLOUD },
413
+ { path: "auth logout", description: "Remove stored cloud credentials.", group: "Account", modes: CLOUD },
414
+ { path: "auth status", description: "Show cloud authentication status.", group: "Account", modes: CLOUD },
415
+ { path: "balance", description: "Show cloud credit balance.", group: "Account", modes: CLOUD },
416
+ { path: "topup", description: "Open a cloud credit checkout.", group: "Account", modes: CLOUD },
417
+ { path: "info", description: "Show TT Local package information.", group: "Inspect", modes: LOCAL }
418
+ ];
419
+ var SLASH_COMMANDS = [
420
+ { path: "/help", description: "Show commands; add a word to filter." },
421
+ { path: "/status", description: "Show lightweight workflow status." },
422
+ { path: "/context", description: "Show the current project and backend context." },
423
+ { path: "/mode cloud", description: "Use the managed cloud workflow." },
424
+ { path: "/mode local", description: "Use the local GPU workflow." },
425
+ { path: "/model", description: "Show the active model; /model <id> activates a local model." },
426
+ { path: "/cd", description: "Change the shell's working directory." },
427
+ { path: "/clear", description: "Clear the terminal." },
428
+ { path: "/exit", description: "Exit the TT shell." }
429
+ ];
430
+ function catalogForMode(mode) {
431
+ return COMMAND_CATALOG.filter((command) => command.modes.includes(mode));
393
432
  }
394
- function text(value) {
395
- if (typeof value === "string") return value;
396
- if (typeof value === "number" || typeof value === "boolean") {
397
- return String(value);
433
+ function commandPathsForMode(mode) {
434
+ return [...new Set(catalogForMode(mode).map((command) => command.path))].sort((left, right) => left.localeCompare(right));
435
+ }
436
+ function groupedCatalog(mode, query) {
437
+ const normalizedQuery = query?.trim().toLowerCase();
438
+ const matches = catalogForMode(mode).filter((command) => {
439
+ if (!normalizedQuery) return true;
440
+ return command.path.toLowerCase().includes(normalizedQuery) || command.description.toLowerCase().includes(normalizedQuery) || command.group.toLowerCase().includes(normalizedQuery);
441
+ });
442
+ const groups = /* @__PURE__ */ new Map();
443
+ for (const command of matches) {
444
+ const group = groups.get(command.group) ?? [];
445
+ group.push(command);
446
+ groups.set(command.group, group);
398
447
  }
399
- return void 0;
448
+ return groups;
400
449
  }
401
- function number(value) {
402
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
450
+ function completionTarget(line, activeMode) {
451
+ const leadingWhitespace = line.match(/^\s*/)?.[0] ?? "";
452
+ const content = line.slice(leadingWhitespace.length);
453
+ const override = content.match(/^(cloud|local)(?:\s+|$)/);
454
+ if (!override) {
455
+ return { prefix: leadingWhitespace, mode: activeMode, fragment: content };
456
+ }
457
+ const mode = override[1];
458
+ const prefix = `${leadingWhitespace}${mode} `;
459
+ return {
460
+ prefix,
461
+ mode,
462
+ fragment: content.slice(override[0].length)
463
+ };
403
464
  }
404
- function record(value) {
405
- return isRecord(value) ? value : void 0;
465
+ function createCommandCompleter(getMode) {
466
+ return (line) => {
467
+ const trimmed = line.trimStart();
468
+ if (trimmed.startsWith("/")) {
469
+ const candidates = [
470
+ ...SLASH_COMMANDS.map((command) => command.path),
471
+ "/new",
472
+ "/threads",
473
+ "/resume ",
474
+ "/approve ",
475
+ "/reject "
476
+ ];
477
+ const matches2 = candidates.filter((candidate) => candidate.startsWith(trimmed));
478
+ return [matches2.length > 0 ? matches2 : candidates, line];
479
+ }
480
+ if (trimmed.startsWith("?")) {
481
+ return [["?"], line];
482
+ }
483
+ const { prefix, mode, fragment } = completionTarget(line, getMode());
484
+ const paths = commandPathsForMode(mode);
485
+ const targetCandidates = paths.map((path) => `${prefix}${path}`);
486
+ if (!prefix.trim()) targetCandidates.unshift("cloud ", "local ");
487
+ const normalizedLine = line.trimStart().toLowerCase();
488
+ const normalizedFragment = fragment.toLowerCase();
489
+ const matches = targetCandidates.filter((candidate) => {
490
+ if (prefix.trim()) return candidate.toLowerCase().startsWith(`${prefix.toLowerCase()}${normalizedFragment}`);
491
+ return candidate.toLowerCase().startsWith(normalizedLine);
492
+ });
493
+ return [matches.length > 0 ? matches : targetCandidates, line];
494
+ };
406
495
  }
407
- function rows(value) {
408
- return Array.isArray(value) ? value.filter(isRecord) : [];
496
+
497
+ // src/shell-context.ts
498
+ import { readFile, readdir, stat } from "fs/promises";
499
+ import { homedir as homedir2 } from "os";
500
+ import { basename as basename2, dirname, isAbsolute, join as join2, resolve } from "path";
501
+ async function readJsonObject(path) {
502
+ try {
503
+ const parsed = JSON.parse(await readFile(path, "utf8"));
504
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
505
+ return { found: true, invalid: true };
506
+ }
507
+ return {
508
+ found: true,
509
+ value: parsed,
510
+ invalid: false
511
+ };
512
+ } catch (error) {
513
+ if (error.code === "ENOENT") {
514
+ return { found: false, invalid: false };
515
+ }
516
+ return { found: true, invalid: true };
517
+ }
409
518
  }
410
- function displayId(value) {
411
- const id = text(value);
412
- return id ? shortId(id) : "\u2014";
519
+ function stringField(value, key) {
520
+ const field = value?.[key];
521
+ return typeof field === "string" && field.trim() ? field : void 0;
413
522
  }
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+$/, "");
523
+ function expandPath(value, baseDirectory, homeDirectory) {
524
+ if (value === "~") return homeDirectory;
525
+ if (value.startsWith("~/")) return resolve(homeDirectory, value.slice(2));
526
+ return isAbsolute(value) ? resolve(value) : resolve(baseDirectory, value);
418
527
  }
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]}`;
528
+ function environmentHome(env) {
529
+ return env.HOME ? resolve(env.HOME) : homedir2();
431
530
  }
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(" ");
531
+ function cloudConfigPath(env, homeDirectory) {
532
+ const configHome = env.XDG_CONFIG_HOME ? resolve(env.XDG_CONFIG_HOME) : join2(homeDirectory, ".config");
533
+ return join2(configHome, "tuned-tensor", "config.json");
439
534
  }
440
- function status(value) {
441
- const parsed = text(value) ?? "unknown";
442
- return formatStatus(parsed);
535
+ function redactApiKey(key) {
536
+ if (!key) return void 0;
537
+ if (key.length <= 4) return "\u2026";
538
+ const prefixLength = key.length > 8 ? 8 : Math.max(1, Math.floor(key.length / 2));
539
+ return `${key.slice(0, prefixLength)}\u2026`;
443
540
  }
444
- function gateFrom(value) {
445
- return record(record(value)?.general_regression);
541
+ function targetFromEnvironment(env) {
542
+ const value = env.TT_TARGET?.trim().toLowerCase();
543
+ return value === "cloud" || value === "local" ? value : void 0;
446
544
  }
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;
545
+ async function adjacentFile(path) {
546
+ try {
547
+ return (await stat(path)).isFile();
548
+ } catch {
549
+ return false;
458
550
  }
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
551
  }
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
- }
552
+ async function readActiveModelId(storeRoot) {
553
+ const result = await readJsonObject(join2(storeRoot, "active-model.json"));
554
+ if (!result.found || result.invalid) return void 0;
555
+ const modelId = result.value?.model_id;
556
+ if (modelId === null) return "base";
557
+ return typeof modelId === "string" && modelId ? modelId : void 0;
485
558
  }
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
- ]);
559
+ async function readLatestRun(storeRoot) {
560
+ const runsRoot = join2(storeRoot, "runs");
561
+ let names;
562
+ try {
563
+ names = (await readdir(runsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
564
+ } catch {
565
+ return void 0;
566
+ }
567
+ const states = await Promise.all(names.map(async (name) => {
568
+ const result = await readJsonObject(join2(runsRoot, name, "state.json"));
569
+ if (!result.found || result.invalid || !result.value) return void 0;
570
+ const id = stringField(result.value, "id") ?? name;
571
+ const state = {
572
+ id,
573
+ status: stringField(result.value, "status"),
574
+ specName: stringField(result.value, "spec_name"),
575
+ updatedAt: stringField(result.value, "updated_at")
576
+ };
577
+ return state;
578
+ }));
579
+ return states.filter((state) => Boolean(state)).sort((left, right) => {
580
+ const leftUpdated = left.updatedAt ?? "";
581
+ const rightUpdated = right.updatedAt ?? "";
582
+ if (leftUpdated !== rightUpdated) {
583
+ return rightUpdated.localeCompare(leftUpdated);
584
+ }
585
+ return right.id.localeCompare(left.id);
586
+ })[0];
495
587
  }
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)]
588
+ async function discoverShellContext(options = {}) {
589
+ const env = options.env ?? process.env;
590
+ const cwd = resolve(options.cwd ?? process.cwd());
591
+ const homeDirectory = environmentHome(env);
592
+ const warnings = [];
593
+ const specPath = join2(cwd, "tunedtensor.json");
594
+ const specJson = await readJsonObject(specPath);
595
+ let spec;
596
+ if (specJson.found) {
597
+ if (specJson.invalid || !specJson.value) {
598
+ spec = { path: specPath, parseError: true };
599
+ warnings.push("tunedtensor.json could not be parsed.");
600
+ } else {
601
+ const examples = specJson.value.examples;
602
+ spec = {
603
+ path: specPath,
604
+ id: stringField(specJson.value, "id"),
605
+ name: stringField(specJson.value, "name"),
606
+ baseModel: stringField(specJson.value, "base_model"),
607
+ exampleCount: Array.isArray(examples) ? examples.length : void 0,
608
+ parseError: false
609
+ };
610
+ }
611
+ }
612
+ const localConfigPath = join2(cwd, "local-runner.json");
613
+ const hasAdjacentLocalConfig = await adjacentFile(localConfigPath);
614
+ const localConfigJson = hasAdjacentLocalConfig ? await readJsonObject(localConfigPath) : { found: false, invalid: false };
615
+ if (localConfigJson.invalid) {
616
+ warnings.push("local-runner.json could not be parsed.");
617
+ }
618
+ const localConfigDirectory = hasAdjacentLocalConfig ? dirname(localConfigPath) : cwd;
619
+ const configuredArtifactRoot = stringField(localConfigJson.value, "artifactRoot");
620
+ const configuredStoreRoot = stringField(localConfigJson.value, "storeRoot");
621
+ const artifactRoot = configuredArtifactRoot ? expandPath(configuredArtifactRoot, localConfigDirectory, homeDirectory) : resolve(cwd, ".tt-local", "artifacts");
622
+ const storeRoot = configuredStoreRoot ? expandPath(configuredStoreRoot, localConfigDirectory, homeDirectory) : env.TT_LOCAL_HOME ? expandPath(env.TT_LOCAL_HOME, cwd, homeDirectory) : join2(homeDirectory, ".tuned-tensor-local");
623
+ const cloudPath = cloudConfigPath(env, homeDirectory);
624
+ const cloudConfigJson = await readJsonObject(cloudPath);
625
+ if (cloudConfigJson.invalid) {
626
+ warnings.push("The Tuned Tensor cloud config could not be parsed.");
627
+ }
628
+ const environmentApiKey = env.TUNED_TENSOR_API_KEY?.trim();
629
+ const storedApiKey = stringField(cloudConfigJson.value, "api_key");
630
+ const apiKey = environmentApiKey || storedApiKey;
631
+ const baseUrl = env.TUNED_TENSOR_URL?.trim() || stringField(cloudConfigJson.value, "base_url") || "https://tunedtensor.com";
632
+ const environmentTarget = targetFromEnvironment(env);
633
+ if (env.TT_TARGET && !environmentTarget) {
634
+ warnings.push(`Ignoring invalid TT_TARGET=${JSON.stringify(env.TT_TARGET)}; use cloud or local.`);
635
+ }
636
+ const inferredTarget = environmentTarget ?? (hasAdjacentLocalConfig ? "local" : "cloud");
637
+ const targetSource = environmentTarget ? "environment" : hasAdjacentLocalConfig ? "adjacent-config" : "default-cloud";
638
+ const [activeModelId, latestRun] = await Promise.all([
639
+ readActiveModelId(storeRoot),
640
+ readLatestRun(storeRoot)
507
641
  ]);
642
+ return {
643
+ cwd,
644
+ projectName: basename2(cwd) || cwd,
645
+ inferredTarget,
646
+ targetSource,
647
+ spec,
648
+ cloud: {
649
+ authenticated: Boolean(apiKey),
650
+ keyPrefix: redactApiKey(apiKey),
651
+ baseUrl,
652
+ configPath: cloudPath,
653
+ configFound: cloudConfigJson.found
654
+ },
655
+ local: {
656
+ configPath: hasAdjacentLocalConfig ? localConfigPath : void 0,
657
+ artifactRoot,
658
+ storeRoot,
659
+ activeModelId,
660
+ latestRun
661
+ },
662
+ warnings
663
+ };
508
664
  }
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}`);
665
+ function valueOrDash(value) {
666
+ return value === void 0 || value === "" ? "\u2014" : String(value);
667
+ }
668
+ function targetSourceLabel(source) {
669
+ switch (source) {
670
+ case "environment":
671
+ return "TT_TARGET";
672
+ case "adjacent-config":
673
+ return "adjacent local-runner.json";
674
+ default:
675
+ return "default";
515
676
  }
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
677
  }
528
- function renderRunsList(value) {
529
- const runRows = rows(value);
530
- if (runRows.length === 0) {
531
- printWarning("No local runs found.");
532
- return;
678
+ function formatShellContext(context, selectedTarget, targetSource) {
679
+ const specLabel = context.spec ? context.spec.parseError ? `${context.spec.path} (invalid JSON)` : context.spec.path : "not found";
680
+ const sourceLabel = targetSource === "session" ? "session" : targetSource === "command-option" ? "--target" : targetSourceLabel(targetSource);
681
+ const lines = [
682
+ `Target ${selectedTarget} (${sourceLabel})`,
683
+ `Project ${context.projectName}`,
684
+ `Directory ${context.cwd}`,
685
+ `Spec ${specLabel}`,
686
+ `Spec ID ${valueOrDash(context.spec?.id)}`,
687
+ `Base model ${valueOrDash(context.spec?.baseModel)}`,
688
+ `Examples ${valueOrDash(context.spec?.exampleCount)}`,
689
+ `Cloud endpoint ${context.cloud.baseUrl}`,
690
+ `Cloud auth ${context.cloud.authenticated ? `yes (${context.cloud.keyPrefix})` : "no"}`,
691
+ `Local config ${context.local.configPath ?? "not found"}`,
692
+ `Artifact root ${context.local.artifactRoot}`,
693
+ `Store root ${context.local.storeRoot}`
694
+ ];
695
+ for (const warning of context.warnings) lines.push(`Warning ${warning}`);
696
+ return lines;
697
+ }
698
+ function formatShellStatus(context, selectedTarget) {
699
+ const lines = [
700
+ `Workflow ${selectedTarget}`,
701
+ `Spec ${context.spec?.name ?? (context.spec ? "unnamed" : "not found")}`
702
+ ];
703
+ if (selectedTarget === "cloud") {
704
+ lines.push(
705
+ `Authentication ${context.cloud.authenticated ? `ready (${context.cloud.keyPrefix})` : "not configured"}`,
706
+ `Endpoint ${context.cloud.baseUrl}`,
707
+ "Remote status not queried"
708
+ );
709
+ } else {
710
+ lines.push(
711
+ `Local config ${context.local.configPath ?? "not found"}`,
712
+ `Active model ${context.local.activeModelId ?? "base"}`,
713
+ `Latest run ${context.local.latestRun ? `${context.local.latestRun.id} (${context.local.latestRun.status ?? "unknown"})` : "none"}`,
714
+ "Host checks not run (use doctor)"
715
+ );
533
716
  }
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
- );
717
+ return lines;
545
718
  }
546
- function renderRunEvents(value) {
547
- const eventRows = rows(value);
548
- if (eventRows.length === 0) {
549
- printWarning("No local run events found.");
550
- return;
719
+
720
+ // src/shell.ts
721
+ var AGENT_SLASH_COMMANDS = /* @__PURE__ */ new Set([
722
+ "new",
723
+ "threads",
724
+ "resume",
725
+ "approve",
726
+ "reject"
727
+ ]);
728
+ var ShellParseError = class extends Error {
729
+ constructor(message) {
730
+ super(message);
731
+ this.name = "ShellParseError";
551
732
  }
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
- ])
733
+ };
734
+ function operatorError(operator) {
735
+ return new ShellParseError(
736
+ `Shell operator ${JSON.stringify(operator)} is not supported. Run TT commands directly; pipes, redirects, and command substitution are disabled.`
560
737
  );
561
738
  }
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;
739
+ function tokenizeShellInput(input) {
740
+ if (input.includes("\0")) {
741
+ throw new ShellParseError("Command input must not contain NUL bytes.");
703
742
  }
704
- if (command === "validate" && isRecord(value)) {
705
- renderValidate(value);
706
- return true;
743
+ if (/[\r\n]/.test(input)) {
744
+ throw new ShellParseError("Enter one command at a time.");
707
745
  }
708
- if (command === "run" && isRecord(value)) {
709
- renderRun(value);
710
- return true;
746
+ if (input.trimStart().startsWith("!")) {
747
+ throw new ShellParseError("Shell escapes are disabled; enter a TT command instead.");
711
748
  }
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;
749
+ const tokens = [];
750
+ let token = "";
751
+ let tokenStarted = false;
752
+ let quote = "none";
753
+ const finishToken = () => {
754
+ if (!tokenStarted) return;
755
+ tokens.push(token);
756
+ token = "";
757
+ tokenStarted = false;
758
+ };
759
+ for (let index = 0; index < input.length; index += 1) {
760
+ const character = input[index];
761
+ if (quote === "single") {
762
+ if (character === "'") {
763
+ quote = "none";
764
+ } else {
765
+ token += character;
766
+ }
767
+ continue;
720
768
  }
721
- if (subcommand === "get" && isRecord(value)) {
722
- renderRunRecord(value);
723
- return true;
769
+ if (quote === "double") {
770
+ if (character === '"') {
771
+ quote = "none";
772
+ continue;
773
+ }
774
+ if (character === "\\") {
775
+ const escaped = input[index + 1];
776
+ if (escaped === void 0) {
777
+ throw new ShellParseError("Command ends with an unfinished escape.");
778
+ }
779
+ token += escaped;
780
+ tokenStarted = true;
781
+ index += 1;
782
+ continue;
783
+ }
784
+ if (character === "`") throw operatorError("`\u2026`");
785
+ if (character === "$" && input[index + 1] === "(") {
786
+ throw operatorError("$(\u2026)");
787
+ }
788
+ token += character;
789
+ tokenStarted = true;
790
+ continue;
724
791
  }
725
- if (subcommand === "report" && isRecord(value)) {
726
- renderRunReport(value);
727
- return true;
792
+ if (/\s/.test(character)) {
793
+ finishToken();
794
+ continue;
728
795
  }
729
- if (subcommand === "compare" && isRecord(value)) {
730
- renderRunComparison(value);
731
- return true;
796
+ if (character === "'") {
797
+ quote = "single";
798
+ tokenStarted = true;
799
+ continue;
732
800
  }
733
- }
734
- if (command === "models") {
735
- if ((subcommand === void 0 || subcommand === "list") && Array.isArray(value)) {
736
- renderModelsList(value);
737
- return true;
801
+ if (character === '"') {
802
+ quote = "double";
803
+ tokenStarted = true;
804
+ continue;
738
805
  }
739
- if ((subcommand === "get" || subcommand === "verify") && isRecord(value)) {
740
- if (subcommand === "verify") printSuccess("Local model artifact is valid");
741
- renderModelRecord(value);
742
- return true;
806
+ if (character === "\\") {
807
+ const escaped = input[index + 1];
808
+ if (escaped === void 0) {
809
+ throw new ShellParseError("Command ends with an unfinished escape.");
810
+ }
811
+ token += escaped;
812
+ tokenStarted = true;
813
+ index += 1;
814
+ continue;
743
815
  }
744
- if ((subcommand === "prefetch" || subcommand === "verify-base") && isRecord(value)) {
745
- renderModelPrefetch(value);
746
- return true;
816
+ if (character === "`") throw operatorError("`\u2026`");
817
+ if (character === "$" && input[index + 1] === "(") {
818
+ throw operatorError("$(\u2026)");
747
819
  }
748
- if (["active", "activate", "rollback"].includes(subcommand ?? "") && isRecord(value)) {
749
- renderActiveModel(value);
750
- return true;
820
+ if (character === "|" || character === ">" || character === "<" || character === ";" || character === "&") {
821
+ const doubled = input[index + 1] === character && (character === "|" || character === "&");
822
+ throw operatorError(doubled ? character.repeat(2) : character);
751
823
  }
752
- if (subcommand === "serve" && isRecord(value)) {
753
- renderServePlan(value);
754
- return true;
824
+ token += character;
825
+ tokenStarted = true;
826
+ }
827
+ if (quote !== "none") {
828
+ throw new ShellParseError(
829
+ `Command has an unterminated ${quote === "single" ? "single" : "double"} quote.`
830
+ );
831
+ }
832
+ finishToken();
833
+ return tokens;
834
+ }
835
+ function routeShellCommand(input, activeMode) {
836
+ let args = tokenizeShellInput(input);
837
+ if (args.length === 0) return null;
838
+ if (args[0] === "tt") {
839
+ args = args.slice(1);
840
+ if (args.length === 0) {
841
+ throw new ShellParseError("The TT shell is already open; enter a command such as `runs list`.");
755
842
  }
756
843
  }
757
- if (command === "serve" && isRecord(value)) {
758
- renderServePlan(value);
759
- return true;
844
+ if (args[0]?.startsWith("/")) {
845
+ throw new ShellParseError("Slash commands are session commands and cannot be routed to a workflow.");
760
846
  }
761
- return false;
847
+ const override = args[0];
848
+ if (override === "cloud" || override === "local") {
849
+ if (args.length === 1) {
850
+ throw new ShellParseError(`Add a ${override} command, or use \`/mode ${override}\` to switch workflows.`);
851
+ }
852
+ return { target: override, args: args.slice(1) };
853
+ }
854
+ return { target: activeMode, args };
762
855
  }
763
- function lastNonEmptyLine(value) {
764
- return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1);
856
+ function isCatalogCommand(mode, args) {
857
+ if (args.length === 0) return false;
858
+ if (args[0] === "status") {
859
+ return args.length === 1 || args[1]?.startsWith("-") === true;
860
+ }
861
+ return COMMAND_CATALOG.some((command) => {
862
+ if (!command.modes.includes(mode)) return false;
863
+ const path = command.path.split(" ");
864
+ return path.every((token, index) => args[index] === token);
865
+ });
765
866
  }
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");
867
+ function hasCatalogIntent(input, activeMode) {
868
+ let args = input.trim().split(/\s+/);
869
+ if (args[0] === "tt") args = args.slice(1);
870
+ const override = args[0];
871
+ const mode = override === "cloud" || override === "local" ? override : activeMode;
872
+ if (override === "cloud" || override === "local") args = args.slice(1);
873
+ return isCatalogCommand(mode, args);
771
874
  }
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
- };
875
+ var SLASH_NAMES = /* @__PURE__ */ new Set([
876
+ "help",
877
+ "status",
878
+ "context",
879
+ "mode",
880
+ "model",
881
+ "clear",
882
+ "cd",
883
+ "exit"
884
+ ]);
885
+ var WORKFLOW_ROOT_COMMANDS = new Set(
886
+ COMMAND_CATALOG.map((command) => command.path.split(" ", 1)[0])
887
+ );
888
+ function editDistance(left, right) {
889
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
890
+ for (let i = 1; i <= left.length; i += 1) {
891
+ let diagonal = previous[0];
892
+ previous[0] = i;
893
+ for (let j = 1; j <= right.length; j += 1) {
894
+ const above = previous[j];
895
+ previous[j] = Math.min(
896
+ previous[j] + 1,
897
+ previous[j - 1] + 1,
898
+ diagonal + (left[i - 1] === right[j - 1] ? 0 : 1)
899
+ );
900
+ diagonal = above;
803
901
  }
804
902
  }
805
- return { data: { output } };
903
+ return previous[right.length];
806
904
  }
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(", ")}.`
905
+ function suggestSlashCommands(rawName) {
906
+ const names = [...SLASH_NAMES].sort();
907
+ const prefixMatches = names.filter((name) => name.startsWith(rawName));
908
+ if (prefixMatches.length > 0) return prefixMatches;
909
+ return names.filter((name) => editDistance(name, rawName) <= 2);
910
+ }
911
+ function unknownSlashCommandError(rawName) {
912
+ if (WORKFLOW_ROOT_COMMANDS.has(rawName)) {
913
+ return new ShellParseError(
914
+ `Unknown session command: /${rawName}. Workflow commands need no slash \u2014 try "${rawName} --help".`
829
915
  );
830
916
  }
831
- if (payload.hasJson && renderKnownJson(payload.args, payload.json)) return;
832
- if (payload.hasJson) {
833
- printJson(payload.json);
834
- return;
917
+ const suggestions = suggestSlashCommands(rawName);
918
+ const hint = suggestions.length > 0 ? ` Did you mean ${suggestions.map((name) => `/${name}`).join(" or ")}?` : "";
919
+ return new ShellParseError(
920
+ `Unknown session command: /${rawName}.${hint} Use /help to see available commands.`
921
+ );
922
+ }
923
+ function parseSlashCommand(input) {
924
+ const trimmed = input.trim();
925
+ if (!trimmed) return null;
926
+ if (trimmed === "?") return { name: "help", args: [] };
927
+ if (trimmed.startsWith("? ")) {
928
+ return {
929
+ name: "help",
930
+ args: tokenizeShellInput(trimmed.slice(1))
931
+ };
835
932
  }
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
- );
933
+ if (!trimmed.startsWith("/")) return null;
934
+ if (trimmed === "/") return { name: "palette", args: [] };
935
+ const tokens = tokenizeShellInput(trimmed);
936
+ const rawName = tokens[0].slice(1).toLowerCase();
937
+ if (!SLASH_NAMES.has(rawName)) {
938
+ throw unknownSlashCommandError(rawName);
841
939
  }
940
+ return {
941
+ name: rawName,
942
+ args: tokens.slice(1)
943
+ };
842
944
  }
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));
945
+ function errorMessage(error) {
946
+ return error instanceof Error ? error.message : String(error);
866
947
  }
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;
948
+ var successMark = () => chalk.green("\u2713");
949
+ var errorMark = () => chalk.red("\u2717");
950
+ var accent = chalk.hex("#8B5CF6");
951
+ var DETAIL_LABEL_WIDTH = 15;
952
+ function styleDetailLines(lines) {
953
+ return lines.map((line) => {
954
+ if (line.length <= DETAIL_LABEL_WIDTH) return line;
955
+ const label = line.slice(0, DETAIL_LABEL_WIDTH);
956
+ if (!label.trim()) return line;
957
+ const color = label.startsWith("Warning") ? chalk.yellow : accent.bold;
958
+ return `${color(label)}${line.slice(DETAIL_LABEL_WIDTH)}`;
959
+ });
883
960
  }
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
- }
961
+ function detailLine(label, value) {
962
+ return `${label.padEnd(DETAIL_LABEL_WIDTH)}${value}`;
963
+ }
964
+ function assertNoArgs(command, args) {
965
+ if (args.length > 0) {
966
+ throw new ShellParseError(`/${command} does not accept arguments.`);
907
967
  }
908
- return null;
909
968
  }
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;
969
+ function expandDirectory(value, cwd, env) {
970
+ const home = env.HOME ? resolve2(env.HOME) : homedir3();
971
+ if (value === "~") return home;
972
+ if (value.startsWith("~/")) return resolve2(home, value.slice(2));
973
+ return isAbsolute2(value) ? resolve2(value) : resolve2(cwd, value);
974
+ }
975
+ function helpText(mode, query, palette = false) {
976
+ const groups = groupedCatalog(mode, query);
977
+ const lines = [];
978
+ lines.push(accent.bold(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)}` : ""}`));
979
+ if (groups.size === 0) {
980
+ lines.push(chalk.dim(" No matching commands."));
981
+ } else {
982
+ for (const [group, commands] of groups) {
983
+ lines.push("");
984
+ lines.push(chalk.bold(group));
985
+ for (const command of commands) {
986
+ lines.push(` ${accent(command.path.padEnd(22))} ${command.description}`);
917
987
  }
918
- };
919
- }
920
- return {
921
- path: defaultPath,
922
- replace(projectedPath) {
923
- args.splice(start, 0, projectedPath);
924
988
  }
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
989
  }
932
- if (command === "models" && ["prefetch", "verify-base"].includes(args[1] ?? "")) {
933
- return positionalSpecArgument(args, 2, join2(cwd, DEFAULT_SPEC_NAME));
990
+ if (!query) {
991
+ lines.push("");
992
+ lines.push(chalk.bold("Conversation"));
993
+ lines.push(` ${accent("plain text".padEnd(22))} Ask the Tuned Tensor agent anything.`);
994
+ lines.push(` ${accent("/new".padEnd(22))} Start a new agent conversation.`);
995
+ lines.push(` ${accent("/threads".padEnd(22))} List recent conversations.`);
996
+ lines.push(` ${accent("/resume <id>".padEnd(22))} Resume a conversation.`);
997
+ lines.push(` ${accent("/approve [id]".padEnd(22))} Approve a proposed action.`);
998
+ lines.push(` ${accent("/reject [id]".padEnd(22))} Reject a proposed action.`);
999
+ lines.push("");
1000
+ lines.push(chalk.bold("Session"));
1001
+ for (const command of SLASH_COMMANDS) {
1002
+ lines.push(` ${accent(command.path.padEnd(22))} ${command.description}`);
1003
+ }
1004
+ lines.push(` ${accent("?".padEnd(22))} Alias for /help.`);
1005
+ lines.push(chalk.dim(
1006
+ "\nKnown TT commands execute directly; other text goes to the agent. Prefix a command with : to make the intent explicit."
1007
+ ));
934
1008
  }
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;
1009
+ return `${lines.join("\n")}
1010
+ `;
940
1011
  }
941
- async function pathIsFile(path) {
942
- return (await stat(path).catch(() => null))?.isFile() ?? false;
1012
+ function activeModelLabel(snapshot) {
1013
+ return snapshot.mode === "local" ? snapshot.context.local.activeModelId ?? "base" : snapshot.context.spec?.baseModel ?? "\u2014";
943
1014
  }
944
- function resolveFromCwd(path, cwd) {
945
- if (isAbsolute(path)) return path;
946
- return resolve(cwd, path);
1015
+ function logoRows() {
1016
+ const muted = chalk.dim("\u2588\u2588");
1017
+ return [
1018
+ `${chalk.hex("#A78BFA")("\u2588\u2588")} ${muted} ${muted}`,
1019
+ `${muted} ${accent("\u2588\u2588")} ${muted}`,
1020
+ `${muted} ${muted} ${chalk.hex("#7C3AED")("\u2588\u2588")}`
1021
+ ];
947
1022
  }
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
- };
1023
+ function renderShellBanner(snapshot) {
1024
+ const spec = snapshot.context.spec?.name ?? snapshot.context.spec?.path ?? "no spec";
1025
+ const heading = snapshot.version ? `${accent.bold("Tuned Tensor")} ${chalk.dim(`v${snapshot.version}`)}` : accent.bold("Tuned Tensor");
1026
+ const textRows = [
1027
+ heading,
1028
+ `${chalk.bold(snapshot.mode)} \xB7 ${snapshot.context.projectName} \xB7 ${spec} \xB7 model ${activeModelLabel(snapshot)}`,
1029
+ chalk.dim("Ask TT anything \xB7 known commands run directly \xB7 /help for more")
1030
+ ];
1031
+ const logo = logoRows();
1032
+ const lines = textRows.map((row, index) => `${logo[index]} ${row}`);
1033
+ return `${lines.join("\n")}
1034
+
1035
+ `;
1036
+ }
1037
+ function renderShellPrompt(snapshot) {
1038
+ return `${accent("tt")} ${chalk.bold(snapshot.mode)} ${chalk.dim(snapshot.context.projectName)} \u203A `;
1039
+ }
1040
+ var TunedTensorShellSession = class _TunedTensorShellSession {
1041
+ constructor(runner, io, agent, env, contextProvider, version, initialContext) {
1042
+ this.runner = runner;
1043
+ this.io = io;
1044
+ this.agent = agent;
1045
+ this.env = env;
1046
+ this.contextProvider = contextProvider;
1047
+ this.version = version;
1048
+ this.mode = initialContext.inferredTarget;
1049
+ this.modeSource = initialContext.targetSource;
1050
+ this.cwd = initialContext.cwd;
1051
+ this.context = initialContext;
958
1052
  }
959
- const selected = selectedSpecArgument(args, cwd);
960
- if (!selected) {
961
- return {
962
- args,
963
- droppedSpecKeys: [],
964
- pathReplacements: [],
965
- cleanup: async () => {
966
- }
967
- };
1053
+ mode;
1054
+ modeSource;
1055
+ cwd;
1056
+ context;
1057
+ static async create(options) {
1058
+ const env = options.env ?? process.env;
1059
+ const cwd = resolve2(options.cwd ?? process.cwd());
1060
+ const contextProvider = options.contextProvider ?? ((input) => discoverShellContext(input));
1061
+ const initialContext = await contextProvider({ cwd, env });
1062
+ return new _TunedTensorShellSession(
1063
+ options.runner,
1064
+ options.io,
1065
+ options.agent,
1066
+ env,
1067
+ contextProvider,
1068
+ options.version,
1069
+ initialContext
1070
+ );
968
1071
  }
969
- const sourcePath = resolveFromCwd(selected.path, cwd);
970
- if (!await pathIsFile(sourcePath)) {
1072
+ interruptAgent() {
1073
+ return this.agent?.interrupt() ?? false;
1074
+ }
1075
+ snapshot() {
971
1076
  return {
972
- args,
973
- droppedSpecKeys: [],
974
- pathReplacements: [],
975
- cleanup: async () => {
976
- }
1077
+ mode: this.mode,
1078
+ modeSource: this.modeSource,
1079
+ cwd: this.cwd,
1080
+ context: this.context,
1081
+ version: this.version
977
1082
  };
978
1083
  }
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
- };
1084
+ prompt() {
1085
+ return renderShellPrompt(this.snapshot());
998
1086
  }
999
- const projected = projectLocalSpec(body);
1000
- if (projected.droppedKeys.length === 0) {
1001
- return {
1002
- args,
1003
- droppedSpecKeys: [],
1004
- pathReplacements: [],
1005
- cleanup: async () => {
1087
+ banner() {
1088
+ return renderShellBanner(this.snapshot());
1089
+ }
1090
+ async refreshContext() {
1091
+ this.context = await this.contextProvider({ cwd: this.cwd, env: this.env });
1092
+ this.cwd = this.context.cwd;
1093
+ if (this.modeSource !== "session" && this.modeSource !== "environment") {
1094
+ this.mode = this.context.inferredTarget;
1095
+ this.modeSource = this.context.targetSource;
1096
+ }
1097
+ }
1098
+ writeLines(lines) {
1099
+ this.io.write(`${lines.join("\n")}
1100
+ `);
1101
+ }
1102
+ modelLines() {
1103
+ if (this.mode === "local") {
1104
+ return styleDetailLines([
1105
+ detailLine("Active model", this.context.local.activeModelId ?? "base"),
1106
+ detailLine("Change", "/model <id> to activate a verified local model")
1107
+ ]);
1108
+ }
1109
+ return styleDetailLines([
1110
+ detailLine("Base model", this.context.spec?.baseModel ?? "\u2014"),
1111
+ detailLine("Change", "edit base_model in tunedtensor.json, then run push")
1112
+ ]);
1113
+ }
1114
+ async handleSlash(command) {
1115
+ switch (command.name) {
1116
+ case "palette":
1117
+ this.io.write(helpText(this.mode, void 0, true));
1118
+ return "continue";
1119
+ case "help":
1120
+ this.io.write(helpText(this.mode, command.args.join(" ") || void 0));
1121
+ return "continue";
1122
+ case "status":
1123
+ assertNoArgs("status", command.args);
1124
+ await this.refreshContext();
1125
+ this.writeLines(styleDetailLines(formatShellStatus(this.context, this.mode)));
1126
+ return "continue";
1127
+ case "context":
1128
+ assertNoArgs("context", command.args);
1129
+ await this.refreshContext();
1130
+ this.writeLines(styleDetailLines(
1131
+ formatShellContext(this.context, this.mode, this.modeSource)
1132
+ ));
1133
+ return "continue";
1134
+ case "mode": {
1135
+ if (command.args.length === 0) {
1136
+ this.io.write(`Current workflow: ${this.mode}
1137
+ `);
1138
+ return "continue";
1139
+ }
1140
+ if (command.args.length !== 1 || !["cloud", "local"].includes(command.args[0])) {
1141
+ throw new ShellParseError("Usage: /mode cloud|local");
1142
+ }
1143
+ this.mode = command.args[0];
1144
+ this.modeSource = "session";
1145
+ this.io.write(`${successMark()} Workflow switched to ${this.mode}.
1146
+ `);
1147
+ return "continue";
1006
1148
  }
1007
- };
1149
+ case "model": {
1150
+ if (command.args.length === 0) {
1151
+ await this.refreshContext();
1152
+ this.writeLines(this.modelLines());
1153
+ return "continue";
1154
+ }
1155
+ if (command.args.length !== 1) {
1156
+ throw new ShellParseError("Usage: /model [model-id]");
1157
+ }
1158
+ if (this.mode !== "local") {
1159
+ throw new ShellParseError(
1160
+ "Activating models is a local workflow action. Use /mode local first; cloud specs change base_model in tunedtensor.json."
1161
+ );
1162
+ }
1163
+ await this.runner({
1164
+ target: "local",
1165
+ args: ["models", "activate", command.args[0]],
1166
+ cwd: this.cwd
1167
+ });
1168
+ await this.refreshContext();
1169
+ return "continue";
1170
+ }
1171
+ case "clear":
1172
+ assertNoArgs("clear", command.args);
1173
+ this.io.clear();
1174
+ return "continue";
1175
+ case "cd": {
1176
+ if (command.args.length === 0) {
1177
+ this.io.write(`${this.cwd}
1178
+ `);
1179
+ return "continue";
1180
+ }
1181
+ if (command.args.length !== 1) {
1182
+ throw new ShellParseError("Usage: /cd <directory>; quote paths containing spaces.");
1183
+ }
1184
+ const nextDirectory = expandDirectory(command.args[0], this.cwd, this.env);
1185
+ const metadata = await stat2(nextDirectory).catch(() => null);
1186
+ if (!metadata?.isDirectory()) {
1187
+ throw new ShellParseError(`Not a directory: ${nextDirectory}`);
1188
+ }
1189
+ this.cwd = nextDirectory;
1190
+ await this.refreshContext();
1191
+ this.io.write(`${successMark()} Directory: ${this.cwd}
1192
+ `);
1193
+ return "continue";
1194
+ }
1195
+ case "exit":
1196
+ assertNoArgs("exit", command.args);
1197
+ return "exit";
1198
+ }
1008
1199
  }
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
- );
1200
+ async handleLine(input) {
1201
+ try {
1202
+ if (/^(exit|quit)$/i.test(input.trim())) return "exit";
1203
+ const trimmed = input.trim();
1204
+ if (trimmed.startsWith("/")) {
1205
+ const rawName = trimmed.slice(1).split(/\s+/, 1)[0].toLowerCase();
1206
+ if (AGENT_SLASH_COMMANDS.has(rawName)) {
1207
+ if (!this.agent) {
1208
+ throw new ShellParseError("The conversational agent is unavailable.");
1209
+ }
1210
+ await this.agent.handleLine(trimmed);
1211
+ await this.refreshContext();
1212
+ return "continue";
1213
+ }
1214
+ }
1215
+ const slash = parseSlashCommand(input);
1216
+ if (slash) return await this.handleSlash(slash);
1217
+ const explicitCommand = trimmed.startsWith(":") ? trimmed.slice(1).trimStart() : null;
1218
+ if (explicitCommand !== null && !explicitCommand) {
1219
+ throw new ShellParseError("Add a TT command after :.");
1220
+ }
1221
+ const explicitWorkflowPrefix = /^(?:tt\s+)?(?:cloud|local)(?:\s|$)/.test(trimmed) || /^tt(?:\s|$)/.test(trimmed);
1222
+ if (explicitCommand === null && !explicitWorkflowPrefix && this.agent && !hasCatalogIntent(input, this.mode)) {
1223
+ await this.agent.handleLine(input);
1224
+ await this.refreshContext();
1225
+ return "continue";
1226
+ }
1227
+ const commandInput = explicitCommand ?? input;
1228
+ const routed = routeShellCommand(commandInput, this.mode);
1229
+ if (!routed) return "continue";
1230
+ await this.runner({
1231
+ target: routed.target,
1232
+ args: [...routed.args],
1233
+ cwd: this.cwd
1234
+ });
1235
+ await this.refreshContext();
1236
+ return "continue";
1237
+ } catch (error) {
1238
+ this.io.writeError(`${errorMark()} ${errorMessage(error)}
1239
+ `);
1240
+ return "continue";
1241
+ }
1038
1242
  }
1243
+ };
1244
+ async function createShellSession(options) {
1245
+ return TunedTensorShellSession.create(options);
1039
1246
  }
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");
1247
+ function streamIsTTY(stream) {
1248
+ return Boolean(stream.isTTY);
1043
1249
  }
1044
- function parseJsonOutput(stdout2) {
1045
- if (!stdout2.trim()) return { hasJson: false, json: void 0 };
1250
+ async function withForegroundSignalHandoff(control, run) {
1251
+ const signals = control.signals ?? process;
1252
+ const restoreRawMode = control.input.isRaw === true && typeof control.input.setRawMode === "function";
1253
+ const keepParentAlive = () => {
1254
+ };
1255
+ control.pauseReadline();
1256
+ if (restoreRawMode) control.input.setRawMode(false);
1257
+ signals.on("SIGINT", keepParentAlive);
1046
1258
  try {
1047
- return { hasJson: true, json: JSON.parse(stdout2) };
1048
- } catch {
1049
- return { hasJson: false, json: void 0 };
1259
+ return await run();
1260
+ } finally {
1261
+ signals.off("SIGINT", keepParentAlive);
1262
+ if (restoreRawMode) control.input.setRawMode(true);
1263
+ control.resumeReadline();
1050
1264
  }
1051
1265
  }
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);
1266
+ function streamIO(output, error) {
1267
+ return {
1268
+ write(text2) {
1269
+ output.write(text2);
1270
+ },
1271
+ writeError(text2) {
1272
+ error.write(text2);
1273
+ },
1274
+ clear() {
1275
+ output.write("\x1B[2J\x1B[H");
1276
+ }
1277
+ };
1059
1278
  }
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);
1279
+ async function startInteractiveShell(options) {
1280
+ const input = options.input ?? process.stdin;
1281
+ const output = options.output ?? process.stdout;
1282
+ const error = options.error ?? process.stderr;
1283
+ if (options.requireTTY !== false && (!streamIsTTY(input) || !streamIsTTY(output))) {
1284
+ throw new Error("The interactive TT shell requires a TTY.");
1070
1285
  }
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],
1286
+ let readline;
1287
+ const terminalInput = input;
1288
+ const foregroundRunner = (request2) => withForegroundSignalHandoff(
1081
1289
  {
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
- }
1290
+ input: terminalInput,
1291
+ pauseReadline: () => readline?.pause(),
1292
+ resumeReadline: () => readline?.resume()
1293
+ },
1294
+ () => options.runner(request2)
1091
1295
  );
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;
1296
+ const session = await createShellSession({
1297
+ runner: foregroundRunner,
1298
+ io: streamIO(output, error),
1299
+ agent: options.agent,
1300
+ cwd: options.cwd,
1301
+ env: options.env,
1302
+ version: options.version
1104
1303
  });
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
- );
1304
+ const completer = createCommandCompleter(() => session.snapshot().mode);
1305
+ readline = createInterface({
1306
+ input,
1307
+ output,
1308
+ terminal: true,
1309
+ historySize: 100,
1310
+ removeHistoryDuplicates: true,
1311
+ completer
1115
1312
  });
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
- }
1313
+ readline.on("SIGINT", () => {
1314
+ if (session.interruptAgent()) {
1315
+ output.write("\n");
1316
+ return;
1317
+ }
1318
+ output.write("\n");
1319
+ readline?.setPrompt(session.prompt());
1320
+ readline?.write(null, { ctrl: true, name: "u" });
1321
+ });
1322
+ output.write(session.banner());
1323
+ readline.setPrompt(session.prompt());
1324
+ readline.prompt();
1325
+ try {
1326
+ for await (const line of readline) {
1327
+ const action = await session.handleLine(line);
1328
+ if (action === "exit") {
1329
+ readline.close();
1330
+ break;
1126
1331
  }
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
- });
1332
+ readline.setPrompt(session.prompt());
1333
+ readline.prompt();
1334
+ }
1335
+ } finally {
1336
+ readline.close();
1337
+ }
1338
+ }
1339
+ function ciEnabled(value) {
1340
+ if (!value) return false;
1341
+ return !["0", "false", "no", "off"].includes(value.trim().toLowerCase());
1342
+ }
1343
+ function shouldStartInteractiveShell(options) {
1344
+ const env = options.env ?? process.env;
1345
+ return options.args.length === 0 && options.stdinIsTTY === true && options.stdoutIsTTY === true && !ciEnabled(env.CI) && env.TERM !== "dumb";
1346
+ }
1347
+
1348
+ // src/agent.ts
1349
+ var accent2 = chalk2.hex("#8B5CF6");
1350
+ var successMark2 = () => chalk2.green("\u2713");
1351
+ var errorMark2 = () => chalk2.red("\u2717");
1352
+ function isRecord(value) {
1353
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1354
+ }
1355
+ function stringValue2(value) {
1356
+ return typeof value === "string" && value.length > 0 ? value : null;
1357
+ }
1358
+ function helpText2() {
1359
+ return [
1360
+ accent2.bold("Agent commands"),
1361
+ "",
1362
+ ` ${accent2("/new".padEnd(22))} Start a new conversation.`,
1363
+ ` ${accent2("/threads".padEnd(22))} List recent conversations.`,
1364
+ ` ${accent2("/resume <id>".padEnd(22))} Resume a conversation by ID or prefix.`,
1365
+ ` ${accent2("/approve [id]".padEnd(22))} Approve a proposed action.`,
1366
+ ` ${accent2("/reject [id]".padEnd(22))} Reject a proposed action.`,
1367
+ ` ${accent2("/status".padEnd(22))} Show the active conversation.`,
1368
+ ` ${accent2("/clear".padEnd(22))} Clear the terminal.`,
1369
+ ` ${accent2("/exit".padEnd(22))} Exit the agent.`,
1370
+ "",
1371
+ chalk2.dim(
1372
+ "Everything else is sent to the Tuned Tensor agent; known TT commands still run directly in the shell."
1373
+ ),
1374
+ ""
1375
+ ].join("\n");
1376
+ }
1377
+ function formatThread(thread, activeId) {
1378
+ const active = thread.id === activeId ? accent2("\u25CF") : chalk2.dim("\u25CB");
1379
+ const time = thread.last_message_at ?? thread.updated_at ?? thread.created_at;
1380
+ const date = new Date(time);
1381
+ const dateLabel = Number.isNaN(date.getTime()) ? "" : date.toLocaleString([], {
1382
+ month: "short",
1383
+ day: "numeric",
1384
+ hour: "2-digit",
1385
+ minute: "2-digit"
1153
1386
  });
1387
+ return `${active} ${chalk2.dim(thread.id.slice(0, 8))} ${thread.title} ${chalk2.dim(dateLabel)}`;
1154
1388
  }
1155
- function setupFailure(originalArgs, error) {
1156
- const message = error instanceof Error ? error.message : String(error);
1389
+ function actionFromPayload(payload) {
1390
+ const value = isRecord(payload.action) ? payload.action : isRecord(payload.approval) ? payload.approval : payload;
1391
+ const id = stringValue2(value.id);
1392
+ const title = stringValue2(value.title);
1393
+ if (!id || !title) return null;
1157
1394
  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
1395
+ id,
1396
+ title,
1397
+ summary: stringValue2(value.summary) ?? "",
1398
+ risk: stringValue2(value.risk) ?? "unknown",
1399
+ status: stringValue2(value.status) ?? "proposed",
1400
+ operation: stringValue2(value.operation) ?? void 0,
1401
+ thread_id: stringValue2(value.thread_id) ?? void 0,
1402
+ turn_id: stringValue2(value.turn_id) ?? void 0,
1403
+ arguments: value.arguments,
1404
+ preview: value.preview,
1405
+ method: stringValue2(value.method) ?? void 0,
1406
+ path: stringValue2(value.path) ?? void 0
1170
1407
  };
1171
1408
  }
1172
- async function runLocalCommand(args, options = {}) {
1173
- const cwd = resolve(options.cwd ?? process.cwd());
1174
- let projected;
1409
+ function formatJson(value) {
1175
1410
  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);
1411
+ return JSON.stringify(value, null, 2);
1412
+ } catch {
1413
+ return String(value);
1414
+ }
1415
+ }
1416
+ var TunedTensorAgentSession = class {
1417
+ constructor(options) {
1418
+ this.options = options;
1419
+ this.thread = options.thread ?? null;
1420
+ }
1421
+ thread;
1422
+ pendingActions = /* @__PURE__ */ new Map();
1423
+ activeRequest = null;
1424
+ responseStarted = false;
1425
+ lineOpen = false;
1426
+ get busy() {
1427
+ return this.activeRequest !== null;
1428
+ }
1429
+ snapshot() {
1193
1430
  return {
1194
- args: [...args],
1195
- projectedArgs: projected.args,
1196
- entrypoint,
1197
- ...result,
1198
- ...parsed,
1199
- streamingStdout,
1200
- droppedSpecKeys: projected.droppedSpecKeys
1431
+ thread: this.thread,
1432
+ pendingActions: [...this.pendingActions.values()]
1201
1433
  };
1202
- } catch (error) {
1203
- return setupFailure(args, error);
1204
- } finally {
1205
- await projected?.cleanup().catch(() => void 0);
1206
1434
  }
1435
+ interrupt() {
1436
+ if (!this.activeRequest) return false;
1437
+ this.activeRequest.abort("user-stop");
1438
+ return true;
1439
+ }
1440
+ endOpenLine() {
1441
+ if (this.lineOpen) this.options.io.write("\n");
1442
+ this.lineOpen = false;
1443
+ }
1444
+ renderAction(action) {
1445
+ this.endOpenLine();
1446
+ this.options.io.write(
1447
+ `
1448
+ ${chalk2.yellow.bold("Approval required")} ${chalk2.dim(action.id.slice(0, 8))}
1449
+ `
1450
+ );
1451
+ this.options.io.write(`${chalk2.bold(action.title)} ${chalk2.yellow(`[${action.risk} risk]`)}
1452
+ `);
1453
+ if (action.summary) this.options.io.write(`${action.summary}
1454
+ `);
1455
+ const request2 = [action.method, action.path].filter(Boolean).join(" ");
1456
+ const technical = {
1457
+ operation: action.operation,
1458
+ ...request2 ? { request: request2 } : {},
1459
+ arguments: action.arguments ?? null,
1460
+ preview: action.preview ?? null
1461
+ };
1462
+ this.options.io.write(`${chalk2.bold("Proposed action")}
1463
+ `);
1464
+ this.options.io.write(`${formatJson(technical)}
1465
+ `);
1466
+ this.options.io.write(
1467
+ chalk2.dim("Use /approve or /reject. The action will not run without approval.") + "\n"
1468
+ );
1469
+ }
1470
+ renderEvent(event) {
1471
+ const payload = event.payload;
1472
+ if (event.type === "text_delta") {
1473
+ const delta = stringValue2(payload.delta) ?? stringValue2(payload.text) ?? stringValue2(payload.content);
1474
+ if (!delta) return;
1475
+ if (!this.responseStarted) {
1476
+ this.options.io.write(`${accent2.bold("TT")} `);
1477
+ this.responseStarted = true;
1478
+ this.lineOpen = true;
1479
+ }
1480
+ this.options.io.write(delta);
1481
+ this.lineOpen = !delta.endsWith("\n");
1482
+ return;
1483
+ }
1484
+ if (event.type === "tool_call") {
1485
+ this.endOpenLine();
1486
+ const name = stringValue2(payload.name) ?? stringValue2(isRecord(payload.tool_call) ? payload.tool_call.name : null) ?? "tool";
1487
+ this.options.io.write(chalk2.dim(` \u25CB ${name}
1488
+ `));
1489
+ return;
1490
+ }
1491
+ if (event.type === "tool_result") {
1492
+ this.endOpenLine();
1493
+ const failed = payload.status === "error" || Boolean(payload.error);
1494
+ this.options.io.write(
1495
+ chalk2.dim(` ${failed ? chalk2.red("\u2717") : chalk2.green("\u2713")} Tool ${failed ? "failed" : "complete"}
1496
+ `)
1497
+ );
1498
+ return;
1499
+ }
1500
+ if (event.type === "approval_required") {
1501
+ const action = actionFromPayload(payload);
1502
+ if (!action) return;
1503
+ this.pendingActions.set(action.id, action);
1504
+ this.renderAction(action);
1505
+ return;
1506
+ }
1507
+ if (event.type === "action_started") {
1508
+ this.endOpenLine();
1509
+ this.options.io.write(chalk2.dim(" \u25CB Running approved action\u2026\n"));
1510
+ return;
1511
+ }
1512
+ if (event.type === "action_result") {
1513
+ this.endOpenLine();
1514
+ const failed = payload.status === "failed" || Boolean(payload.error);
1515
+ this.options.io.write(
1516
+ `${failed ? errorMark2() : successMark2()} Approved action ${failed ? "failed" : "completed"}.
1517
+ `
1518
+ );
1519
+ return;
1520
+ }
1521
+ if (event.type === "error") {
1522
+ this.endOpenLine();
1523
+ const message = stringValue2(payload.message) ?? stringValue2(payload.error) ?? "The agent could not complete this request.";
1524
+ this.options.io.writeError(`${errorMark2()} ${message}
1525
+ `);
1526
+ }
1527
+ }
1528
+ async ensureThread() {
1529
+ if (!this.thread) this.thread = await this.options.client.createThread();
1530
+ return this.thread;
1531
+ }
1532
+ async runStream(operation) {
1533
+ if (this.activeRequest) {
1534
+ this.options.io.writeError(`${errorMark2()} A response is already running.
1535
+ `);
1536
+ return null;
1537
+ }
1538
+ const controller = new AbortController();
1539
+ this.activeRequest = controller;
1540
+ this.responseStarted = false;
1541
+ this.lineOpen = false;
1542
+ try {
1543
+ const result = await operation(controller.signal);
1544
+ this.endOpenLine();
1545
+ for (const action of result.actions) {
1546
+ this.pendingActions.set(action.id, action);
1547
+ }
1548
+ return result;
1549
+ } catch (error) {
1550
+ this.endOpenLine();
1551
+ if (controller.signal.aborted) {
1552
+ this.options.io.write(chalk2.dim("Response stopped.\n"));
1553
+ return null;
1554
+ }
1555
+ throw error;
1556
+ } finally {
1557
+ this.activeRequest = null;
1558
+ this.responseStarted = false;
1559
+ this.lineOpen = false;
1560
+ }
1561
+ }
1562
+ async send(prompt) {
1563
+ const normalized = prompt.trim();
1564
+ if (!normalized) return null;
1565
+ const thread = await this.ensureThread();
1566
+ return await this.runStream(
1567
+ async (signal) => await this.options.client.runTurn(
1568
+ thread.id,
1569
+ normalized,
1570
+ (event) => this.renderEvent(event),
1571
+ signal
1572
+ )
1573
+ );
1574
+ }
1575
+ resolvePendingAction(idOrPrefix) {
1576
+ const candidates = [...this.pendingActions.values()].filter(
1577
+ (action) => idOrPrefix ? action.id.startsWith(idOrPrefix) : true
1578
+ );
1579
+ if (candidates.length === 0) {
1580
+ throw new Error(
1581
+ idOrPrefix ? `No pending action matches ${idOrPrefix}.` : "There is no pending action."
1582
+ );
1583
+ }
1584
+ if (candidates.length > 1) {
1585
+ throw new Error("More than one action is pending; include its ID or prefix.");
1586
+ }
1587
+ return candidates[0];
1588
+ }
1589
+ async approve(idOrPrefix) {
1590
+ const action = this.resolvePendingAction(idOrPrefix);
1591
+ const result = await this.runStream(
1592
+ async (signal) => await this.options.client.approveAction(
1593
+ action.id,
1594
+ (event) => this.renderEvent(event),
1595
+ signal
1596
+ )
1597
+ );
1598
+ if (result) this.pendingActions.delete(action.id);
1599
+ }
1600
+ async reject(idOrPrefix) {
1601
+ const action = this.resolvePendingAction(idOrPrefix);
1602
+ await this.options.client.rejectAction(action.id);
1603
+ this.pendingActions.delete(action.id);
1604
+ this.options.io.write(`${successMark2()} Action rejected.
1605
+ `);
1606
+ }
1607
+ async resolveThread(idOrPrefix) {
1608
+ const id = idOrPrefix.trim();
1609
+ if (!id) throw new Error("Usage: /resume <conversation-id>");
1610
+ if (id.length >= 32) return await this.options.client.getThread(id);
1611
+ const matches = (await this.options.client.listThreads()).filter((thread) => thread.id.startsWith(id));
1612
+ if (matches.length === 0) throw new Error(`No conversation matches ${id}.`);
1613
+ if (matches.length > 1) {
1614
+ throw new Error(`Conversation prefix ${id} is ambiguous.`);
1615
+ }
1616
+ return await this.options.client.getThread(matches[0].id);
1617
+ }
1618
+ async handleCommand(input) {
1619
+ const tokens = tokenizeShellInput(input);
1620
+ const command = tokens[0].slice(1).toLowerCase();
1621
+ const args = tokens.slice(1);
1622
+ switch (command) {
1623
+ case "help":
1624
+ this.options.io.write(helpText2());
1625
+ return "continue";
1626
+ case "new":
1627
+ if (args.length > 0) throw new Error("/new does not accept arguments.");
1628
+ this.thread = null;
1629
+ this.pendingActions.clear();
1630
+ this.options.io.write(`${successMark2()} New conversation ready.
1631
+ `);
1632
+ return "continue";
1633
+ case "threads": {
1634
+ if (args.length > 0) throw new Error("/threads does not accept arguments.");
1635
+ const threads = await this.options.client.listThreads();
1636
+ if (threads.length === 0) {
1637
+ this.options.io.write(chalk2.dim("No conversations yet.\n"));
1638
+ return "continue";
1639
+ }
1640
+ this.options.io.write(
1641
+ `${threads.slice(0, 20).map((thread) => formatThread(thread, this.thread?.id)).join("\n")}
1642
+ `
1643
+ );
1644
+ return "continue";
1645
+ }
1646
+ case "resume":
1647
+ if (args.length !== 1) throw new Error("Usage: /resume <conversation-id>");
1648
+ {
1649
+ const detail = await this.resolveThread(args[0]);
1650
+ this.thread = detail.thread;
1651
+ this.pendingActions.clear();
1652
+ for (const action of detail.actions) {
1653
+ if (action.status === "proposed") {
1654
+ this.pendingActions.set(action.id, action);
1655
+ }
1656
+ }
1657
+ }
1658
+ this.options.io.write(
1659
+ `${successMark2()} Resumed ${this.thread.title} (${this.thread.id.slice(0, 8)}).
1660
+ `
1661
+ );
1662
+ return "continue";
1663
+ case "approve":
1664
+ if (args.length > 1) throw new Error("Usage: /approve [action-id]");
1665
+ await this.approve(args[0]);
1666
+ return "continue";
1667
+ case "reject":
1668
+ if (args.length > 1) throw new Error("Usage: /reject [action-id]");
1669
+ await this.reject(args[0]);
1670
+ return "continue";
1671
+ case "status":
1672
+ if (args.length > 0) throw new Error("/status does not accept arguments.");
1673
+ this.options.io.write(
1674
+ this.thread ? `${formatThread(this.thread, this.thread.id)}
1675
+ ` : chalk2.dim("New conversation; send a message to create it.\n")
1676
+ );
1677
+ this.options.io.write(
1678
+ chalk2.dim(`${this.pendingActions.size} pending approval(s).
1679
+ `)
1680
+ );
1681
+ return "continue";
1682
+ case "clear":
1683
+ if (args.length > 0) throw new Error("/clear does not accept arguments.");
1684
+ this.options.io.clear();
1685
+ return "continue";
1686
+ case "exit":
1687
+ if (args.length > 0) throw new Error("/exit does not accept arguments.");
1688
+ return "exit";
1689
+ default:
1690
+ throw new Error(`Unknown agent command: /${command}. Use /help.`);
1691
+ }
1692
+ }
1693
+ async handleLine(input) {
1694
+ try {
1695
+ const normalized = input.trim();
1696
+ if (!normalized) return "continue";
1697
+ if (/^(exit|quit)$/i.test(normalized)) return "exit";
1698
+ if (normalized.startsWith("/")) return await this.handleCommand(normalized);
1699
+ await this.send(normalized);
1700
+ return "continue";
1701
+ } catch (error) {
1702
+ const message = error instanceof Error ? error.message : String(error);
1703
+ this.options.io.writeError(`${errorMark2()} ${message}
1704
+ `);
1705
+ return "continue";
1706
+ }
1707
+ }
1708
+ };
1709
+
1710
+ // src/local-runner.ts
1711
+ import { constants as osConstants } from "os";
1712
+ import { randomUUID } from "crypto";
1713
+ import { createRequire } from "module";
1714
+ import { spawn } from "child_process";
1715
+ import {
1716
+ readFile as readFile2,
1717
+ rm,
1718
+ stat as stat3,
1719
+ writeFile
1720
+ } from "fs/promises";
1721
+ import {
1722
+ basename as basename3,
1723
+ dirname as dirname2,
1724
+ isAbsolute as isAbsolute3,
1725
+ join as join3,
1726
+ resolve as resolve3
1727
+ } from "path";
1728
+
1729
+ // src/base-models.ts
1730
+ var SUPPORTED_BASE_MODELS = [
1731
+ "google/gemma-4-E2B-it",
1732
+ "google/gemma-4-E4B-it",
1733
+ "Qwen/Qwen3.5-2B",
1734
+ "Qwen/Qwen3-VL-2B-Instruct",
1735
+ "Qwen/Qwen3.5-4B",
1736
+ "meta-llama/Llama-3.2-3B-Instruct",
1737
+ "microsoft/Phi-4-mini-instruct",
1738
+ "ibm-granite/granite-3.3-2b-instruct",
1739
+ "bigcode/starcoder2-3b"
1740
+ ];
1741
+ var BASE_MODEL_ALIASES = /* @__PURE__ */ new Map();
1742
+ for (const model of SUPPORTED_BASE_MODELS) {
1743
+ BASE_MODEL_ALIASES.set(model.toLowerCase(), model);
1744
+ }
1745
+ for (const [alias, model] of [
1746
+ ["google/gemma-4-E2B", "google/gemma-4-E2B-it"],
1747
+ ["google/gemma-4-e2b", "google/gemma-4-E2B-it"],
1748
+ ["google/gemma-4-e2b-it", "google/gemma-4-E2B-it"],
1749
+ ["google/gemma-4-E4B", "google/gemma-4-E4B-it"],
1750
+ ["google/gemma-4-e4b", "google/gemma-4-E4B-it"],
1751
+ ["google/gemma-4-e4b-it", "google/gemma-4-E4B-it"],
1752
+ ["qwen/qwen3.5-2b", "Qwen/Qwen3.5-2B"],
1753
+ ["Qwen/Qwen3.5-2B-Base", "Qwen/Qwen3.5-2B"],
1754
+ ["qwen/qwen3.5-2b-base", "Qwen/Qwen3.5-2B"],
1755
+ ["qwen/qwen3-vl-2b-instruct", "Qwen/Qwen3-VL-2B-Instruct"],
1756
+ ["Qwen/Qwen3-VL-2B", "Qwen/Qwen3-VL-2B-Instruct"],
1757
+ ["qwen/qwen3-vl-2b", "Qwen/Qwen3-VL-2B-Instruct"],
1758
+ ["qwen/qwen3.5-4b", "Qwen/Qwen3.5-4B"],
1759
+ ["Qwen/Qwen3.5-4B-Base", "Qwen/Qwen3.5-4B"],
1760
+ ["qwen/qwen3.5-4b-base", "Qwen/Qwen3.5-4B"],
1761
+ ["meta-llama/llama-3.2-3b-instruct", "meta-llama/Llama-3.2-3B-Instruct"],
1762
+ ["meta-llama/Llama-3.2-3B", "meta-llama/Llama-3.2-3B-Instruct"],
1763
+ ["meta-llama/llama-3.2-3b", "meta-llama/Llama-3.2-3B-Instruct"],
1764
+ ["microsoft/phi-4-mini-instruct", "microsoft/Phi-4-mini-instruct"],
1765
+ ["phi-4-mini-instruct", "microsoft/Phi-4-mini-instruct"],
1766
+ ["ibm-granite/granite-3.3-2b-instruct", "ibm-granite/granite-3.3-2b-instruct"],
1767
+ ["granite-3.3-2b-instruct", "ibm-granite/granite-3.3-2b-instruct"],
1768
+ ["bigcode/starcoder2-3b", "bigcode/starcoder2-3b"],
1769
+ ["starcoder2-3b", "bigcode/starcoder2-3b"]
1770
+ ]) {
1771
+ BASE_MODEL_ALIASES.set(alias.toLowerCase(), model);
1772
+ }
1773
+ var UnsupportedBaseModelError = class extends Error {
1774
+ constructor(model) {
1775
+ const supported = SUPPORTED_BASE_MODELS.join(", ");
1776
+ super(
1777
+ `Unsupported base_model "${String(model)}". Supported base models: ${supported}`
1778
+ );
1779
+ this.name = "UnsupportedBaseModelError";
1780
+ }
1781
+ };
1782
+ function canonicalizeBaseModel(model) {
1783
+ if (typeof model !== "string" || !model.trim()) {
1784
+ throw new UnsupportedBaseModelError(model);
1785
+ }
1786
+ const canonical = BASE_MODEL_ALIASES.get(model.trim().toLowerCase());
1787
+ if (!canonical) {
1788
+ throw new UnsupportedBaseModelError(model);
1789
+ }
1790
+ return canonical;
1207
1791
  }
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;
1792
+ function canonicalizeSpecBaseModel(body) {
1793
+ if ("base_model" in body && body.base_model !== void 0) {
1794
+ return { ...body, base_model: canonicalizeBaseModel(body.base_model) };
1795
+ }
1796
+ return body;
1216
1797
  }
1217
1798
 
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 }
1799
+ // src/project-spec.ts
1800
+ var CLOUD_SPEC_KEYS = [
1801
+ "name",
1802
+ "description",
1803
+ "base_model",
1804
+ "system_prompt",
1805
+ "guidelines",
1806
+ "constraints",
1807
+ "examples",
1808
+ "eval_cases"
1286
1809
  ];
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." }
1810
+ var LOCAL_ONLY_SPEC_KEYS = [
1811
+ "hyperparameters",
1812
+ "dataset_prebuilt"
1296
1813
  ];
1297
- function catalogForMode(mode) {
1298
- return COMMAND_CATALOG.filter((command) => command.modes.includes(mode));
1814
+ var LOCAL_SPEC_KEYS = [
1815
+ "id",
1816
+ "name",
1817
+ "description",
1818
+ "system_prompt",
1819
+ "guidelines",
1820
+ "constraints",
1821
+ "base_model",
1822
+ "examples",
1823
+ ...LOCAL_ONLY_SPEC_KEYS
1824
+ ];
1825
+ var CLOUD_SPEC_KEY_SET = new Set(CLOUD_SPEC_KEYS);
1826
+ var LOCAL_ONLY_SPEC_KEY_SET = new Set(LOCAL_ONLY_SPEC_KEYS);
1827
+ var LOCAL_SPEC_KEY_SET = new Set(LOCAL_SPEC_KEYS);
1828
+ var KNOWN_PROJECT_SPEC_KEY_SET = /* @__PURE__ */ new Set([
1829
+ ...CLOUD_SPEC_KEYS,
1830
+ ...LOCAL_SPEC_KEYS
1831
+ ]);
1832
+ function projectSpec(raw, supportedKeys) {
1833
+ const body = {};
1834
+ const droppedKeys = [];
1835
+ for (const [key, value] of Object.entries(raw)) {
1836
+ if (supportedKeys.has(key)) {
1837
+ body[key] = value;
1838
+ } else {
1839
+ droppedKeys.push(key);
1840
+ }
1841
+ }
1842
+ return {
1843
+ body: canonicalizeSpecBaseModel(body),
1844
+ droppedKeys
1845
+ };
1299
1846
  }
1300
- function commandPathsForMode(mode) {
1301
- return [...new Set(catalogForMode(mode).map((command) => command.path))].sort((left, right) => left.localeCompare(right));
1847
+ function projectCloudSpec(raw) {
1848
+ return projectSpec(raw, CLOUD_SPEC_KEY_SET);
1302
1849
  }
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);
1850
+ function projectLocalSpec(raw) {
1851
+ return projectSpec(raw, LOCAL_SPEC_KEY_SET);
1852
+ }
1853
+ function hasLocalOnlySpecFields(raw) {
1854
+ return Object.keys(raw).some((key) => LOCAL_ONLY_SPEC_KEY_SET.has(key));
1855
+ }
1856
+ function unknownProjectSpecKeys(raw) {
1857
+ return Object.keys(raw).filter(
1858
+ (key) => !KNOWN_PROJECT_SPEC_KEY_SET.has(key)
1859
+ );
1860
+ }
1861
+
1862
+ // src/output.ts
1863
+ import chalk3 from "chalk";
1864
+ import Table from "cli-table3";
1865
+ var jsonMode = false;
1866
+ function setJsonMode(enabled) {
1867
+ jsonMode = enabled;
1868
+ }
1869
+ function isJsonMode() {
1870
+ return jsonMode;
1871
+ }
1872
+ function printJson(data) {
1873
+ console.log(JSON.stringify(data, null, 2));
1874
+ }
1875
+ function printTable(headers, rows2, meta) {
1876
+ const table = new Table({
1877
+ head: headers.map((h) => chalk3.bold.cyan(h)),
1878
+ style: { head: [], border: [] }
1308
1879
  });
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);
1880
+ for (const row of rows2) {
1881
+ table.push(row);
1882
+ }
1883
+ console.log(table.toString());
1884
+ if (meta) {
1885
+ const totalPages = Math.ceil(meta.total / meta.per_page);
1886
+ console.log(
1887
+ chalk3.dim(
1888
+ `
1889
+ Page ${meta.page}/${totalPages} (${meta.total} total)`
1890
+ )
1891
+ );
1314
1892
  }
1315
- return groups;
1316
1893
  }
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 };
1894
+ function printDetail(fields) {
1895
+ const maxLabel = Math.max(...fields.map(([l]) => l.length));
1896
+ for (const [label, value] of fields) {
1897
+ const paddedLabel = label.padEnd(maxLabel);
1898
+ console.log(`${chalk3.bold.cyan(paddedLabel)} ${value ?? chalk3.dim("\u2014")}`);
1323
1899
  }
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
1900
  }
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);
1901
+ function printSuccess(message) {
1902
+ console.log(chalk3.green("\u2713") + " " + message);
1903
+ }
1904
+ function printWarning(message) {
1905
+ console.log(chalk3.yellow("!") + " " + message);
1906
+ }
1907
+ function printError(message) {
1908
+ console.error(chalk3.red("\u2717") + " " + message);
1909
+ }
1910
+ function reportError(err) {
1911
+ const isApi = err instanceof ApiError;
1912
+ const message = err?.message || String(err);
1913
+ if (jsonMode) {
1914
+ printJson({
1915
+ error: {
1916
+ status: isApi ? err.status : null,
1917
+ code: isApi ? err.code : "CLI_ERROR",
1918
+ message
1919
+ }
1352
1920
  });
1353
- return [matches.length > 0 ? matches : targetCandidates, line];
1921
+ } else if (isApi) {
1922
+ printError(`[${err.status}] ${message}`);
1923
+ } else {
1924
+ printError(message);
1925
+ }
1926
+ }
1927
+ function formatDate(iso) {
1928
+ if (!iso) return "\u2014";
1929
+ return new Date(iso).toLocaleString();
1930
+ }
1931
+ function formatStatus(status2) {
1932
+ const map = {
1933
+ completed: chalk3.green,
1934
+ running: chalk3.blue,
1935
+ training: chalk3.blue,
1936
+ evaluating: chalk3.blue,
1937
+ preparing: chalk3.yellow,
1938
+ pending: chalk3.yellow,
1939
+ uploading: chalk3.yellow,
1940
+ failed: chalk3.red,
1941
+ cancelled: chalk3.dim,
1942
+ validated: chalk3.green,
1943
+ invalid: chalk3.red
1354
1944
  };
1945
+ const colorFn = map[status2] || chalk3.white;
1946
+ return colorFn(status2);
1947
+ }
1948
+ function truncate(str, max) {
1949
+ if (str.length <= max) return str;
1950
+ return str.slice(0, max - 1) + "\u2026";
1951
+ }
1952
+ function shortId(id) {
1953
+ return id.slice(0, 8);
1355
1954
  }
1356
1955
 
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 };
1956
+ // src/local-output.ts
1957
+ function isRecord2(value) {
1958
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1959
+ }
1960
+ function text(value) {
1961
+ if (typeof value === "string") return value;
1962
+ if (typeof value === "number" || typeof value === "boolean") {
1963
+ return String(value);
1377
1964
  }
1965
+ return void 0;
1378
1966
  }
1379
- function stringField(value, key) {
1380
- const field = value?.[key];
1381
- return typeof field === "string" && field.trim() ? field : void 0;
1967
+ function number(value) {
1968
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1382
1969
  }
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);
1970
+ function record2(value) {
1971
+ return isRecord2(value) ? value : void 0;
1387
1972
  }
1388
- function environmentHome(env) {
1389
- return env.HOME ? resolve2(env.HOME) : homedir2();
1973
+ function rows(value) {
1974
+ return Array.isArray(value) ? value.filter(isRecord2) : [];
1390
1975
  }
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");
1976
+ function displayId(value) {
1977
+ const id = text(value);
1978
+ return id ? shortId(id) : "\u2014";
1394
1979
  }
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`;
1980
+ function displayNumber(value, digits = 4) {
1981
+ const parsed = number(value);
1982
+ if (parsed === void 0) return void 0;
1983
+ return parsed.toFixed(digits).replace(/\.?0+$/, "");
1400
1984
  }
1401
- function targetFromEnvironment(env) {
1402
- const value = env.TT_TARGET?.trim().toLowerCase();
1403
- return value === "cloud" || value === "local" ? value : void 0;
1985
+ function displayBytes(value) {
1986
+ const bytes = number(value);
1987
+ if (bytes === void 0 || bytes < 0) return void 0;
1988
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
1989
+ let amount = bytes;
1990
+ let unit = 0;
1991
+ while (amount >= 1024 && unit < units.length - 1) {
1992
+ amount /= 1024;
1993
+ unit += 1;
1994
+ }
1995
+ const precision = amount >= 10 || unit === 0 ? 0 : 1;
1996
+ return `${amount.toFixed(precision)} ${units[unit]}`;
1404
1997
  }
1405
- async function adjacentFile(path) {
1406
- try {
1407
- return (await stat2(path)).isFile();
1408
- } catch {
1409
- return false;
1998
+ function displayCommand(value) {
1999
+ if (!Array.isArray(value) || !value.every((part) => typeof part === "string")) {
2000
+ return void 0;
1410
2001
  }
2002
+ return value.map(
2003
+ (part) => /[\s"'\\]/.test(part) ? JSON.stringify(part) : part
2004
+ ).join(" ");
1411
2005
  }
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;
2006
+ function status(value) {
2007
+ const parsed = text(value) ?? "unknown";
2008
+ return formatStatus(parsed);
1418
2009
  }
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;
2010
+ function gateFrom(value) {
2011
+ return record2(record2(value)?.general_regression);
2012
+ }
2013
+ function renderGate(value) {
2014
+ const gate = gateFrom(value);
2015
+ if (!gate) {
2016
+ printWarning(
2017
+ "General regression was not run; this model cannot be activated yet."
2018
+ );
2019
+ return;
1426
2020
  }
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];
2021
+ if (gate.passed === true) {
2022
+ printSuccess("General regression gate passed");
2023
+ return;
2024
+ }
2025
+ printWarning(
2026
+ "The local run completed, but the general regression gate failed."
2027
+ );
2028
+ const failures = Array.isArray(gate.failures) ? gate.failures.filter(
2029
+ (failure) => typeof failure === "string"
2030
+ ) : [];
2031
+ for (const failure of failures) printWarning(failure);
1447
2032
  }
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
- }
2033
+ function renderDoctor(value) {
2034
+ const checks = rows(value.checks);
2035
+ printTable(
2036
+ ["Check", "Status", "Details"],
2037
+ checks.map((check) => [
2038
+ text(check.name) ?? "unknown",
2039
+ check.ok === true ? formatStatus("completed") : formatStatus("failed"),
2040
+ truncate(text(check.message) ?? text(check.detail) ?? "\u2014", 100)
2041
+ ])
2042
+ );
2043
+ if (value.ok === true) {
2044
+ printSuccess("Local environment is ready");
2045
+ } else {
2046
+ printError("Local environment needs attention");
1471
2047
  }
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.");
2048
+ if (value.config_path) {
2049
+ printDetail([["Config", text(value.config_path)]]);
1477
2050
  }
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.");
2051
+ }
2052
+ function renderInit(value) {
2053
+ printSuccess("Created local Tuned Tensor project");
2054
+ printDetail([
2055
+ ["Path", text(value.path)],
2056
+ ["Name", text(value.name)],
2057
+ ["ID", text(value.id)],
2058
+ ["Base model", text(value.base_model)],
2059
+ ["Config", text(value.config_path)]
2060
+ ]);
2061
+ }
2062
+ function renderValidate(value) {
2063
+ printSuccess("Local behavior spec is valid");
2064
+ printDetail([
2065
+ ["Input", text(value.input_path)],
2066
+ ["Config", text(value.config_path)],
2067
+ ["Spec", text(value.behavior_spec_id)],
2068
+ ["Base model", text(value.base_model)],
2069
+ ["Dataset", text(value.dataset_format)],
2070
+ ["Artifacts", text(value.artifact_root)],
2071
+ ["Store", text(value.store_root)],
2072
+ ["Dry run", text(value.dry_run)]
2073
+ ]);
2074
+ }
2075
+ function renderRun(value) {
2076
+ const runStatus = text(value.status) ?? "unknown";
2077
+ if (runStatus === "completed") {
2078
+ printSuccess("Local run completed");
2079
+ } else {
2080
+ printWarning(`Local run finished with status ${runStatus}`);
1487
2081
  }
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.`);
2082
+ const comparison = record2(value.comparison);
2083
+ printDetail([
2084
+ ["Run", text(value.run_id)],
2085
+ ["Status", status(runStatus)],
2086
+ ["Model", text(value.model_id) ?? text(value.fine_tuned_model_id)],
2087
+ ["Report", text(value.report_path)],
2088
+ ["Artifacts", text(value.artifact_dir)],
2089
+ ["Score delta", displayNumber(comparison?.avg_score_delta)],
2090
+ ["Pass-rate delta", displayNumber(comparison?.pass_rate_delta)]
2091
+ ]);
2092
+ renderGate(value);
2093
+ }
2094
+ function renderRunsList(value) {
2095
+ const runRows = rows(value);
2096
+ if (runRows.length === 0) {
2097
+ printWarning("No local runs found.");
2098
+ return;
2099
+ }
2100
+ printTable(
2101
+ ["Run", "Spec", "Status", "Stage", "Model", "Updated"],
2102
+ runRows.map((run) => [
2103
+ displayId(run.id),
2104
+ truncate(text(run.spec_name) ?? displayId(run.behavior_spec_id), 28),
2105
+ status(run.status),
2106
+ text(run.current_stage) ?? "\u2014",
2107
+ displayId(run.model_id),
2108
+ formatDate(text(run.updated_at))
2109
+ ])
2110
+ );
2111
+ }
2112
+ function renderRunEvents(value) {
2113
+ const eventRows = rows(value);
2114
+ if (eventRows.length === 0) {
2115
+ printWarning("No local run events found.");
2116
+ return;
1495
2117
  }
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)
2118
+ printTable(
2119
+ ["Time", "Stage", "Status", "Message"],
2120
+ eventRows.map((event) => [
2121
+ formatDate(text(event.occurred_at)),
2122
+ text(event.stage) ?? "\u2014",
2123
+ status(event.status),
2124
+ truncate(text(event.message) ?? "\u2014", 80)
2125
+ ])
2126
+ );
2127
+ }
2128
+ function renderRunRecord(value) {
2129
+ printDetail([
2130
+ ["Run", text(value.id) ?? text(value.run_id)],
2131
+ ["Spec", text(value.spec_name) ?? text(value.behavior_spec_id)],
2132
+ ["Status", status(value.status)],
2133
+ ["Stage", text(value.current_stage)],
2134
+ ["Model", text(value.model_id) ?? text(value.fine_tuned_model_id)],
2135
+ ["Base model", text(value.base_model)],
2136
+ ["Report", text(value.report_path)],
2137
+ ["Artifacts", text(value.artifact_dir)],
2138
+ ["Updated", formatDate(text(value.updated_at) ?? text(value.created_at))]
1501
2139
  ]);
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
2140
  }
1525
- function valueOrDash(value) {
1526
- return value === void 0 || value === "" ? "\u2014" : String(value);
2141
+ function renderRunReport(value) {
2142
+ const baseline = record2(value.baseline);
2143
+ const candidate = record2(value.candidate);
2144
+ const comparison = record2(value.comparison);
2145
+ const metadata = record2(value.run_metadata);
2146
+ printDetail([
2147
+ ["Run", text(value.run_id)],
2148
+ ["Status", status(value.status)],
2149
+ ["Base model", text(value.base_model)],
2150
+ ["Tuned model", text(value.fine_tuned_model_id)],
2151
+ ["Training examples", text(metadata?.training_example_count)],
2152
+ ["Evaluation examples", text(metadata?.eval_examples_used)],
2153
+ ["Baseline score", displayNumber(baseline?.avg_score)],
2154
+ ["Candidate score", displayNumber(candidate?.avg_score)],
2155
+ ["Score delta", displayNumber(comparison?.avg_score_delta)],
2156
+ ["Pass-rate delta", displayNumber(comparison?.pass_rate_delta)],
2157
+ ["Regressions", text(comparison?.regressions)],
2158
+ ["Improvements", text(comparison?.improvements)]
2159
+ ]);
2160
+ renderGate(value);
1527
2161
  }
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";
2162
+ function renderRunComparison(value) {
2163
+ const runA = record2(value.run_a);
2164
+ const runB = record2(value.run_b);
2165
+ const shared = record2(value.shared);
2166
+ const sharedA = record2(shared?.run_a);
2167
+ const sharedB = record2(shared?.run_b);
2168
+ printTable(
2169
+ ["Run", "Candidate score", "Token F1", "Examples"],
2170
+ [
2171
+ [
2172
+ displayId(runA?.run_id),
2173
+ displayNumber(sharedA?.candidate_avg_score) ?? "\u2014",
2174
+ displayNumber(sharedA?.candidate_avg_token_f1) ?? "\u2014",
2175
+ text(shared?.examples) ?? "0"
2176
+ ],
2177
+ [
2178
+ displayId(runB?.run_id),
2179
+ displayNumber(sharedB?.candidate_avg_score) ?? "\u2014",
2180
+ displayNumber(sharedB?.candidate_avg_token_f1) ?? "\u2014",
2181
+ text(shared?.examples) ?? "0"
2182
+ ]
2183
+ ]
2184
+ );
2185
+ const notes = Array.isArray(value.notes) ? value.notes.filter((note) => typeof note === "string") : [];
2186
+ for (const note of notes) printWarning(note);
2187
+ }
2188
+ function renderModelsList(value) {
2189
+ const modelRows = rows(value);
2190
+ if (modelRows.length === 0) {
2191
+ printWarning("No local models found.");
2192
+ return;
1536
2193
  }
2194
+ printTable(
2195
+ ["Model", "Base model", "Run", "Provider", "Created"],
2196
+ modelRows.map((model) => [
2197
+ displayId(model.id),
2198
+ truncate(text(model.base_model) ?? "\u2014", 32),
2199
+ displayId(model.run_id),
2200
+ text(model.provider) ?? "\u2014",
2201
+ formatDate(text(model.created_at))
2202
+ ])
2203
+ );
1537
2204
  }
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;
2205
+ function renderModelRecord(value) {
2206
+ const model = record2(value.model) ?? value;
2207
+ const artifact = record2(value.artifact);
2208
+ const integrity = record2(value.integrity);
2209
+ printDetail([
2210
+ ["Model", text(model.id) ?? text(value.active)],
2211
+ ["Base model", text(model.base_model)],
2212
+ ["Run", text(model.run_id)],
2213
+ ["Provider", text(model.provider)],
2214
+ ["Artifact", text(model.artifact_uri) ?? text(artifact?.path)],
2215
+ ["Manifest", text(value.manifest_path)],
2216
+ ["Verified files", text(integrity?.checked)],
2217
+ ["Created", formatDate(text(model.created_at))]
2218
+ ]);
1557
2219
  }
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;
2220
+ function renderModelPrefetch(value) {
2221
+ printSuccess(
2222
+ value.local_files_only === true ? "Verified the local base-model snapshot" : "Base model is ready locally"
2223
+ );
2224
+ printDetail([
2225
+ ["Base model", text(value.base_model)],
2226
+ ["Revision", text(value.snapshot_revision)],
2227
+ ["Snapshot", text(value.snapshot_path)],
2228
+ ["Files", text(value.file_count)],
2229
+ ["Size", displayBytes(value.size_bytes)],
2230
+ ["Cache", text(value.model_cache) ?? text(value.hf_home)]
2231
+ ]);
1578
2232
  }
1579
-
1580
- // src/shell.ts
1581
- var ShellParseError = class extends Error {
1582
- constructor(message) {
1583
- super(message);
1584
- this.name = "ShellParseError";
2233
+ function renderActiveModel(value) {
2234
+ const active = text(value.active) ?? "base";
2235
+ const pointer = record2(value.pointer);
2236
+ if (pointer?.action === "activate") {
2237
+ printSuccess(`Activated local model ${active}`);
2238
+ } else if (pointer?.action === "rollback") {
2239
+ printSuccess(`Rolled back to ${active}`);
1585
2240
  }
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
- );
2241
+ printDetail([
2242
+ ["Active", active],
2243
+ ["Run", text(pointer?.run_id)],
2244
+ ["Previous", text(pointer?.previous_model_id) ?? "base"],
2245
+ ["Changed", formatDate(text(pointer?.activated_at))],
2246
+ ["Manifest", text(value.manifest_path)]
2247
+ ]);
1591
2248
  }
1592
- function tokenizeShellInput(input) {
1593
- if (input.includes("\0")) {
1594
- throw new ShellParseError("Command input must not contain NUL bytes.");
2249
+ function renderServePlan(value) {
2250
+ printSuccess("Local serving plan is valid");
2251
+ printDetail([
2252
+ ["Model", text(value.model_id) ?? text(value.base_model)],
2253
+ ["URL", text(value.url)],
2254
+ ["Artifact", text(value.artifact_path)],
2255
+ ["Manifest", text(value.manifest_path)],
2256
+ ["Command", displayCommand(value.command)]
2257
+ ]);
2258
+ }
2259
+ function renderKnownJson(args, value) {
2260
+ const command = args[0];
2261
+ const subcommand = args[1];
2262
+ if (command === "doctor" && isRecord2(value)) {
2263
+ renderDoctor(value);
2264
+ return true;
1595
2265
  }
1596
- if (/[\r\n]/.test(input)) {
1597
- throw new ShellParseError("Enter one command at a time.");
2266
+ if (command === "init" && isRecord2(value)) {
2267
+ renderInit(value);
2268
+ return true;
1598
2269
  }
1599
- if (input.trimStart().startsWith("!")) {
1600
- throw new ShellParseError("Shell escapes are disabled; enter a TT command instead.");
2270
+ if (command === "validate" && isRecord2(value)) {
2271
+ renderValidate(value);
2272
+ return true;
1601
2273
  }
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;
2274
+ if (command === "run" && isRecord2(value)) {
2275
+ renderRun(value);
2276
+ return true;
2277
+ }
2278
+ if (command === "runs") {
2279
+ if ((subcommand === void 0 || subcommand === "list") && Array.isArray(value)) {
2280
+ renderRunsList(value);
2281
+ return true;
2282
+ }
2283
+ if (subcommand === "events" && Array.isArray(value)) {
2284
+ renderRunEvents(value);
2285
+ return true;
1621
2286
  }
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;
2287
+ if (subcommand === "get" && isRecord2(value)) {
2288
+ renderRunRecord(value);
2289
+ return true;
1644
2290
  }
1645
- if (/\s/.test(character)) {
1646
- finishToken();
1647
- continue;
2291
+ if (subcommand === "report" && isRecord2(value)) {
2292
+ renderRunReport(value);
2293
+ return true;
1648
2294
  }
1649
- if (character === "'") {
1650
- quote = "single";
1651
- tokenStarted = true;
1652
- continue;
2295
+ if (subcommand === "compare" && isRecord2(value)) {
2296
+ renderRunComparison(value);
2297
+ return true;
1653
2298
  }
1654
- if (character === '"') {
1655
- quote = "double";
1656
- tokenStarted = true;
1657
- continue;
2299
+ }
2300
+ if (command === "models") {
2301
+ if ((subcommand === void 0 || subcommand === "list") && Array.isArray(value)) {
2302
+ renderModelsList(value);
2303
+ return true;
1658
2304
  }
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;
2305
+ if ((subcommand === "get" || subcommand === "verify") && isRecord2(value)) {
2306
+ if (subcommand === "verify") printSuccess("Local model artifact is valid");
2307
+ renderModelRecord(value);
2308
+ return true;
1668
2309
  }
1669
- if (character === "`") throw operatorError("`\u2026`");
1670
- if (character === "$" && input[index + 1] === "(") {
1671
- throw operatorError("$(\u2026)");
2310
+ if ((subcommand === "prefetch" || subcommand === "verify-base") && isRecord2(value)) {
2311
+ renderModelPrefetch(value);
2312
+ return true;
1672
2313
  }
1673
- if (character === "|" || character === ">" || character === "<" || character === ";" || character === "&") {
1674
- const doubled = input[index + 1] === character && (character === "|" || character === "&");
1675
- throw operatorError(doubled ? character.repeat(2) : character);
2314
+ if (["active", "activate", "rollback"].includes(subcommand ?? "") && isRecord2(value)) {
2315
+ renderActiveModel(value);
2316
+ return true;
2317
+ }
2318
+ if (subcommand === "serve" && isRecord2(value)) {
2319
+ renderServePlan(value);
2320
+ return true;
1676
2321
  }
1677
- token += character;
1678
- tokenStarted = true;
1679
2322
  }
1680
- if (quote !== "none") {
1681
- throw new ShellParseError(
1682
- `Command has an unterminated ${quote === "single" ? "single" : "double"} quote.`
1683
- );
2323
+ if (command === "serve" && isRecord2(value)) {
2324
+ renderServePlan(value);
2325
+ return true;
1684
2326
  }
1685
- finishToken();
1686
- return tokens;
2327
+ return false;
1687
2328
  }
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`.");
2329
+ function lastNonEmptyLine(value) {
2330
+ return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1);
2331
+ }
2332
+ function adaptLocalCliText(value) {
2333
+ return value.replace(
2334
+ /(?<![.@/\w])tuned-tensor-local(?=$|[\s:`\]])/g,
2335
+ "tt local"
2336
+ ).replace(/(?<![.@/\w])tt-local(?=$|[\s:`\]])/g, "tt local");
2337
+ }
2338
+ function localCliErrorEnvelope(payload) {
2339
+ const rawMessage = payload.errorMessage ?? (payload.stderr.trim() || void 0) ?? lastNonEmptyLine(payload.stdout) ?? `tt local exited with code ${payload.exitCode}.`;
2340
+ return {
2341
+ error: {
2342
+ status: null,
2343
+ code: "LOCAL_CLI_ERROR",
2344
+ message: adaptLocalCliText(rawMessage),
2345
+ exit_code: payload.exitCode,
2346
+ signal: payload.signal
2347
+ }
2348
+ };
2349
+ }
2350
+ function localCliTextEnvelope(payload) {
2351
+ const output = adaptLocalCliText(payload.stdout).trim();
2352
+ if (payload.args[0] === "info") {
2353
+ const lines = output.split(/\r?\n/);
2354
+ const heading = lines[0]?.match(/^([^:]+):\s*(.+)$/);
2355
+ const fields = Object.fromEntries(
2356
+ lines.slice(1).flatMap((line) => {
2357
+ const match = line.match(/^([^:]+):\s*(.*)$/);
2358
+ return match ? [[match[1].trim().toLowerCase().replaceAll(" ", "_"), match[2].trim()]] : [];
2359
+ })
2360
+ );
2361
+ if (heading) {
2362
+ return {
2363
+ data: {
2364
+ name: heading[1],
2365
+ description: heading[2],
2366
+ ...fields
2367
+ }
2368
+ };
1695
2369
  }
1696
2370
  }
1697
- if (args[0]?.startsWith("/")) {
1698
- throw new ShellParseError("Slash commands are session commands and cannot be routed to a workflow.");
2371
+ return { data: { output } };
2372
+ }
2373
+ function renderLocalOutput(payload, options = {}) {
2374
+ if (payload.streamingStdout && payload.exitCode === 0 && !payload.errorMessage) {
2375
+ return;
1699
2376
  }
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.`);
2377
+ const output = options.stdout ?? process.stdout;
2378
+ if (options.jsonMode) {
2379
+ if (payload.hasJson) {
2380
+ output.write(payload.stdout);
2381
+ return;
1704
2382
  }
1705
- return { target: override, args: args.slice(1) };
2383
+ if (payload.exitCode !== 0 || payload.errorMessage) {
2384
+ output.write(`${JSON.stringify(localCliErrorEnvelope(payload), null, 2)}
2385
+ `);
2386
+ return;
2387
+ }
2388
+ output.write(`${JSON.stringify(localCliTextEnvelope(payload), null, 2)}
2389
+ `);
2390
+ return;
2391
+ }
2392
+ if (payload.droppedSpecKeys.length > 0) {
2393
+ printWarning(
2394
+ `Ignored project field(s) unsupported by the local runtime: ${payload.droppedSpecKeys.join(", ")}.`
2395
+ );
2396
+ }
2397
+ if (payload.hasJson && renderKnownJson(payload.args, payload.json)) return;
2398
+ if (payload.hasJson) {
2399
+ printJson(payload.json);
2400
+ return;
2401
+ }
2402
+ if (payload.stdout) output.write(adaptLocalCliText(payload.stdout));
2403
+ if (payload.exitCode !== 0 && !payload.stderr && !payload.stdout) {
2404
+ printError(
2405
+ payload.errorMessage ? adaptLocalCliText(payload.errorMessage) : `tt local exited with code ${payload.exitCode}.`
2406
+ );
1706
2407
  }
1707
- return { target: activeMode, args };
1708
2408
  }
1709
- var SLASH_NAMES = /* @__PURE__ */ new Set([
1710
- "help",
1711
- "status",
1712
- "context",
1713
- "mode",
1714
- "clear",
1715
- "cd",
1716
- "exit"
2409
+
2410
+ // src/local-runner.ts
2411
+ var localRequire = createRequire(import.meta.url);
2412
+ var DEFAULT_SPEC_NAME = "tunedtensor.json";
2413
+ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
2414
+ "--config",
2415
+ "--name",
2416
+ "--model",
2417
+ "--output",
2418
+ "--profile",
2419
+ "--host",
2420
+ "--port",
2421
+ "--device",
2422
+ "--max-tokens",
2423
+ "--temperature",
2424
+ "--top-p",
2425
+ "--max-concurrent-requests",
2426
+ "--spec",
2427
+ "--api-key-env"
1717
2428
  ]);
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("? ")) {
2429
+ function isOptionWithInlineValue(value) {
2430
+ const equals = value.indexOf("=");
2431
+ return equals !== -1 && VALUE_OPTIONS.has(value.slice(0, equals));
2432
+ }
2433
+ function positionalIndices(args, start) {
2434
+ const indices = [];
2435
+ let optionsEnded = false;
2436
+ for (let index = start; index < args.length; index += 1) {
2437
+ const value = args[index];
2438
+ if (!optionsEnded && value === "--") {
2439
+ optionsEnded = true;
2440
+ continue;
2441
+ }
2442
+ if (!optionsEnded && value.startsWith("-")) {
2443
+ if (!isOptionWithInlineValue(value) && VALUE_OPTIONS.has(value)) index += 1;
2444
+ continue;
2445
+ }
2446
+ indices.push(index);
2447
+ }
2448
+ return indices;
2449
+ }
2450
+ function optionValueArgument(args, name) {
2451
+ for (let index = 0; index < args.length; index += 1) {
2452
+ const value = args[index];
2453
+ if (value === name) {
2454
+ const path = args[index + 1];
2455
+ if (!path) return null;
2456
+ return {
2457
+ path,
2458
+ replace(projectedPath) {
2459
+ args[index + 1] = projectedPath;
2460
+ }
2461
+ };
2462
+ }
2463
+ if (value.startsWith(`${name}=`)) {
2464
+ const path = value.slice(name.length + 1);
2465
+ if (!path) return null;
2466
+ return {
2467
+ path,
2468
+ replace(projectedPath) {
2469
+ args[index] = `${name}=${projectedPath}`;
2470
+ }
2471
+ };
2472
+ }
2473
+ }
2474
+ return null;
2475
+ }
2476
+ function positionalSpecArgument(args, start, defaultPath) {
2477
+ const positional = positionalIndices(args, start)[0];
2478
+ if (positional !== void 0) {
1723
2479
  return {
1724
- name: "help",
1725
- args: tokenizeShellInput(trimmed.slice(1))
2480
+ path: args[positional],
2481
+ replace(projectedPath) {
2482
+ args[positional] = projectedPath;
2483
+ }
1726
2484
  };
1727
2485
  }
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
2486
  return {
1736
- name: rawName,
1737
- args: tokens.slice(1)
2487
+ path: defaultPath,
2488
+ replace(projectedPath) {
2489
+ args.splice(start, 0, projectedPath);
2490
+ }
1738
2491
  };
1739
2492
  }
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
- }
2493
+ function selectedSpecArgument(args, cwd) {
2494
+ const command = args[0];
2495
+ if (["doctor", "validate", "run"].includes(command ?? "")) {
2496
+ return positionalSpecArgument(args, 1, join3(cwd, DEFAULT_SPEC_NAME));
1768
2497
  }
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.");
2498
+ if (command === "models" && ["prefetch", "verify-base"].includes(args[1] ?? "")) {
2499
+ return positionalSpecArgument(args, 2, join3(cwd, DEFAULT_SPEC_NAME));
1776
2500
  }
1777
- return `${lines.join("\n")}
1778
- `;
2501
+ const serves = command === "serve" || command === "models" && args[1] === "serve";
2502
+ if (!serves) return null;
2503
+ const explicit = optionValueArgument(args, "--spec");
2504
+ if (explicit) return explicit;
2505
+ return null;
1779
2506
  }
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");
2507
+ async function pathIsFile(path) {
2508
+ return (await stat3(path).catch(() => null))?.isFile() ?? false;
1788
2509
  }
1789
- function renderShellPrompt(snapshot) {
1790
- return `${chalk2.cyan("tt")} ${chalk2.bold(snapshot.mode)} ${chalk2.dim(snapshot.context.projectName)} \u203A `;
2510
+ function resolveFromCwd(path, cwd) {
2511
+ if (isAbsolute3(path)) return path;
2512
+ return resolve3(cwd, path);
1791
2513
  }
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() {
2514
+ async function projectSpecArguments(originalArgs, cwd) {
2515
+ const args = [...originalArgs];
2516
+ if (args.includes("--help") || args.includes("-h")) {
1821
2517
  return {
1822
- mode: this.mode,
1823
- modeSource: this.modeSource,
1824
- cwd: this.cwd,
1825
- context: this.context
2518
+ args,
2519
+ droppedSpecKeys: [],
2520
+ pathReplacements: [],
2521
+ cleanup: async () => {
2522
+ }
1826
2523
  };
1827
2524
  }
1828
- prompt() {
1829
- return renderShellPrompt(this.snapshot());
1830
- }
1831
- banner() {
1832
- return renderShellBanner(this.snapshot());
2525
+ const selected = selectedSpecArgument(args, cwd);
2526
+ if (!selected) {
2527
+ return {
2528
+ args,
2529
+ droppedSpecKeys: [],
2530
+ pathReplacements: [],
2531
+ cleanup: async () => {
2532
+ }
2533
+ };
1833
2534
  }
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
- }
2535
+ const sourcePath = resolveFromCwd(selected.path, cwd);
2536
+ if (!await pathIsFile(sourcePath)) {
2537
+ return {
2538
+ args,
2539
+ droppedSpecKeys: [],
2540
+ pathReplacements: [],
2541
+ cleanup: async () => {
2542
+ }
2543
+ };
1841
2544
  }
1842
- writeLines(lines) {
1843
- this.io.write(`${lines.join("\n")}
1844
- `);
2545
+ const raw = JSON.parse(await readFile2(sourcePath, "utf8"));
2546
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2547
+ throw new Error(`${sourcePath} must contain a JSON object.`);
1845
2548
  }
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";
2549
+ const body = raw;
2550
+ const runRequestKeys = [
2551
+ "run_id",
2552
+ "behavior_spec_id",
2553
+ "run_number",
2554
+ "spec_snapshot"
2555
+ ].filter((key) => key in body);
2556
+ if ("spec_snapshot" in body || runRequestKeys.length >= 2) {
2557
+ return {
2558
+ args,
2559
+ droppedSpecKeys: [],
2560
+ pathReplacements: [],
2561
+ cleanup: async () => {
1902
2562
  }
1903
- case "exit":
1904
- assertNoArgs("exit", command.args);
1905
- return "exit";
1906
- }
2563
+ };
1907
2564
  }
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
- }
2565
+ const projected = projectLocalSpec(body);
2566
+ if (projected.droppedKeys.length === 0) {
2567
+ return {
2568
+ args,
2569
+ droppedSpecKeys: [],
2570
+ pathReplacements: [],
2571
+ cleanup: async () => {
2572
+ }
2573
+ };
1926
2574
  }
1927
- };
1928
- async function createShellSession(options) {
1929
- return TunedTensorShellSession.create(options);
2575
+ const temporaryPath = join3(
2576
+ dirname2(sourcePath),
2577
+ `.${basename3(sourcePath)}.tt-local-${process.pid}-${randomUUID()}.json`
2578
+ );
2579
+ await writeFile(
2580
+ temporaryPath,
2581
+ `${JSON.stringify(projected.body, null, 2)}
2582
+ `,
2583
+ { encoding: "utf8", flag: "wx", mode: 384 }
2584
+ );
2585
+ selected.replace(temporaryPath);
2586
+ return {
2587
+ args,
2588
+ droppedSpecKeys: projected.droppedKeys,
2589
+ pathReplacements: [{
2590
+ sourcePath,
2591
+ projectedPath: temporaryPath
2592
+ }],
2593
+ cleanup: () => rm(temporaryPath, { force: true })
2594
+ };
1930
2595
  }
1931
- function streamIsTTY(stream) {
1932
- return Boolean(stream.isTTY);
2596
+ function resolveLocalCliEntrypoint() {
2597
+ try {
2598
+ return localRequire.resolve("@tuned-tensor/local");
2599
+ } catch (error) {
2600
+ throw new Error(
2601
+ "TT Local is not installed. Reinstall @tuned-tensor/cli with its local runtime dependency.",
2602
+ { cause: error }
2603
+ );
2604
+ }
1933
2605
  }
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);
2606
+ function localCommandStreamsStdout(args) {
2607
+ const serves = args[0] === "serve" || args[0] === "models" && args[1] === "serve";
2608
+ return serves && !args.includes("--print-command") && !args.includes("--help") && !args.includes("-h");
2609
+ }
2610
+ function parseJsonOutput(stdout2) {
2611
+ if (!stdout2.trim()) return { hasJson: false, json: void 0 };
1942
2612
  try {
1943
- return await run();
1944
- } finally {
1945
- signals.off("SIGINT", keepParentAlive);
1946
- if (restoreRawMode) control.input.setRawMode(true);
1947
- control.resumeReadline();
2613
+ return { hasJson: true, json: JSON.parse(stdout2) };
2614
+ } catch {
2615
+ return { hasJson: false, json: void 0 };
1948
2616
  }
1949
2617
  }
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
- };
2618
+ function signalExitCode(signal) {
2619
+ if (!signal) return 1;
2620
+ const signalNumber = osConstants.signals[signal];
2621
+ return typeof signalNumber === "number" ? 128 + signalNumber : 1;
1962
2622
  }
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.");
2623
+ function writeStream(stream, chunk) {
2624
+ stream.write(chunk);
2625
+ }
2626
+ function normalizeProjectedPaths(value, replacements) {
2627
+ let normalized = value;
2628
+ for (const replacement of replacements) {
2629
+ const encodedProjectedPath = JSON.stringify(
2630
+ replacement.projectedPath
2631
+ ).slice(1, -1);
2632
+ const encodedSourcePath = JSON.stringify(
2633
+ replacement.sourcePath
2634
+ ).slice(1, -1);
2635
+ normalized = normalized.replaceAll(encodedProjectedPath, encodedSourcePath).replaceAll(replacement.projectedPath, replacement.sourcePath);
1969
2636
  }
1970
- let readline;
1971
- const terminalInput = input;
1972
- const foregroundRunner = (request2) => withForegroundSignalHandoff(
2637
+ return normalized;
2638
+ }
2639
+ async function spawnLocalCli(args) {
2640
+ const output = args.stdout ?? process.stdout;
2641
+ const errors = args.stderr ?? process.stderr;
2642
+ const pipeInput = Boolean(args.stdin && args.stdin !== process.stdin);
2643
+ const pipeStreamingOutput = args.streamingStdout && Boolean(args.stdout && args.stdout !== process.stdout);
2644
+ const child = spawn(
2645
+ args.nodeExecutable,
2646
+ [args.entrypoint, ...args.commandArgs],
1973
2647
  {
1974
- input: terminalInput,
1975
- pauseReadline: () => readline?.pause(),
1976
- resumeReadline: () => readline?.resume()
1977
- },
1978
- () => options.runner(request2)
2648
+ cwd: args.cwd,
2649
+ env: args.env,
2650
+ stdio: [
2651
+ pipeInput ? "pipe" : "inherit",
2652
+ args.streamingStdout && !pipeStreamingOutput ? "inherit" : "pipe",
2653
+ "pipe"
2654
+ ],
2655
+ detached: process.platform !== "win32"
2656
+ }
1979
2657
  );
1980
- const session = await createShellSession({
1981
- runner: foregroundRunner,
1982
- io: streamIO(output, error),
1983
- cwd: options.cwd,
1984
- env: options.env
2658
+ if (pipeInput && args.stdin && child.stdin) args.stdin.pipe(child.stdin);
2659
+ let stdout2 = "";
2660
+ let stderr = "";
2661
+ child.stdout?.setEncoding("utf8");
2662
+ child.stderr?.setEncoding("utf8");
2663
+ child.stdout?.on("data", (chunk) => {
2664
+ const normalized = normalizeProjectedPaths(
2665
+ chunk,
2666
+ args.pathReplacements
2667
+ );
2668
+ if (args.streamingStdout) writeStream(output, normalized);
2669
+ else stdout2 += normalized;
1985
2670
  });
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
2671
+ child.stderr?.on("data", (chunk) => {
2672
+ const normalized = normalizeProjectedPaths(
2673
+ chunk,
2674
+ args.pathReplacements
2675
+ );
2676
+ stderr += normalized;
2677
+ writeStream(
2678
+ errors,
2679
+ args.jsonMode ? normalized : adaptLocalCliText(normalized)
2680
+ );
1994
2681
  });
1995
- readline.on("SIGINT", () => {
1996
- output.write("\n");
1997
- readline?.setPrompt(session.prompt());
1998
- readline?.write(null, { ctrl: true, name: "u" });
2682
+ return new Promise((resolveChild) => {
2683
+ let spawnError;
2684
+ const forwardSignal = (signal) => {
2685
+ const childSignal = signal === "SIGHUP" ? "SIGTERM" : signal;
2686
+ if (child.pid && process.platform !== "win32") {
2687
+ try {
2688
+ process.kill(-child.pid, childSignal);
2689
+ return;
2690
+ } catch {
2691
+ }
2692
+ }
2693
+ child.kill(childSignal);
2694
+ };
2695
+ const onSigint = () => forwardSignal("SIGINT");
2696
+ const onSigterm = () => forwardSignal("SIGTERM");
2697
+ const onSighup = () => forwardSignal("SIGHUP");
2698
+ const cleanup = () => {
2699
+ process.off("SIGINT", onSigint);
2700
+ process.off("SIGTERM", onSigterm);
2701
+ if (process.platform !== "win32") process.off("SIGHUP", onSighup);
2702
+ };
2703
+ process.on("SIGINT", onSigint);
2704
+ process.on("SIGTERM", onSigterm);
2705
+ if (process.platform !== "win32") process.on("SIGHUP", onSighup);
2706
+ child.once("error", (error) => {
2707
+ spawnError = error;
2708
+ });
2709
+ child.once("close", (code, signal) => {
2710
+ cleanup();
2711
+ resolveChild({
2712
+ exitCode: spawnError ? 1 : code ?? signalExitCode(signal),
2713
+ signal,
2714
+ stdout: stdout2,
2715
+ stderr,
2716
+ ...spawnError ? { errorMessage: spawnError.message } : {}
2717
+ });
2718
+ });
1999
2719
  });
2000
- output.write(session.banner());
2001
- readline.setPrompt(session.prompt());
2002
- readline.prompt();
2720
+ }
2721
+ function setupFailure(originalArgs, error) {
2722
+ const message = error instanceof Error ? error.message : String(error);
2723
+ return {
2724
+ args: [...originalArgs],
2725
+ projectedArgs: [...originalArgs],
2726
+ entrypoint: null,
2727
+ exitCode: 1,
2728
+ signal: null,
2729
+ stdout: "",
2730
+ stderr: "",
2731
+ hasJson: false,
2732
+ json: void 0,
2733
+ streamingStdout: false,
2734
+ droppedSpecKeys: [],
2735
+ errorMessage: message
2736
+ };
2737
+ }
2738
+ async function runLocalCommand(args, options = {}) {
2739
+ const cwd = resolve3(options.cwd ?? process.cwd());
2740
+ let projected;
2003
2741
  try {
2004
- for await (const line of readline) {
2005
- const action = await session.handleLine(line);
2006
- if (action === "exit") {
2007
- readline.close();
2008
- break;
2009
- }
2010
- readline.setPrompt(session.prompt());
2011
- readline.prompt();
2012
- }
2742
+ const entrypoint = options.entrypoint ?? resolveLocalCliEntrypoint();
2743
+ projected = await projectSpecArguments(args, cwd);
2744
+ const streamingStdout = localCommandStreamsStdout(projected.args);
2745
+ const result = await spawnLocalCli({
2746
+ entrypoint,
2747
+ commandArgs: projected.args,
2748
+ cwd,
2749
+ env: options.env ?? process.env,
2750
+ nodeExecutable: options.nodeExecutable ?? process.execPath,
2751
+ streamingStdout,
2752
+ jsonMode: options.jsonMode ?? false,
2753
+ stdin: options.stdin,
2754
+ stdout: options.stdout,
2755
+ stderr: options.stderr,
2756
+ pathReplacements: projected.pathReplacements
2757
+ });
2758
+ const parsed = parseJsonOutput(result.stdout);
2759
+ return {
2760
+ args: [...args],
2761
+ projectedArgs: projected.args,
2762
+ entrypoint,
2763
+ ...result,
2764
+ ...parsed,
2765
+ streamingStdout,
2766
+ droppedSpecKeys: projected.droppedSpecKeys
2767
+ };
2768
+ } catch (error) {
2769
+ return setupFailure(args, error);
2013
2770
  } finally {
2014
- readline.close();
2771
+ await projected?.cleanup().catch(() => void 0);
2015
2772
  }
2016
2773
  }
2017
- function ciEnabled(value) {
2018
- if (!value) return false;
2019
- return !["0", "false", "no", "off"].includes(value.trim().toLowerCase());
2020
- }
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";
2774
+ async function executeLocalCommand(args, options = {}) {
2775
+ const result = await runLocalCommand(args, options);
2776
+ renderLocalOutput(result, {
2777
+ jsonMode: options.jsonMode,
2778
+ stdout: options.stdout
2779
+ });
2780
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
2781
+ return result;
2024
2782
  }
2025
2783
 
2026
2784
  // src/commands/auth.ts
2027
2785
  import { createInterface as createInterface2 } from "readline/promises";
2028
2786
  import { stdin, stdout } from "process";
2029
2787
  import { Writable } from "stream";
2030
- import chalk3 from "chalk";
2788
+ import chalk4 from "chalk";
2031
2789
  async function promptForApiKey(input = stdin, output = stdout) {
2032
2790
  if (input.isTTY !== true || output.isTTY !== true) {
2033
2791
  throw new Error(
@@ -2069,9 +2827,9 @@ function registerAuthCommands(parent) {
2069
2827
  const settingsUrl = `${DEFAULT_BASE_URL}/dashboard/settings`;
2070
2828
  console.log();
2071
2829
  console.log(
2072
- ` To get your API key, go to ${chalk3.bold("Settings \u2192 API Keys")} in the Tuned Tensor dashboard:`
2830
+ ` To get your API key, go to ${chalk4.bold("Settings \u2192 API Keys")} in the Tuned Tensor dashboard:`
2073
2831
  );
2074
- console.log(` ${chalk3.cyan.underline(settingsUrl)}`);
2832
+ console.log(` ${chalk4.cyan.underline(settingsUrl)}`);
2075
2833
  console.log();
2076
2834
  apiKey = await promptForApiKey();
2077
2835
  }
@@ -2958,18 +3716,18 @@ function findControlChar(value) {
2958
3716
  function formatCodepoint(code) {
2959
3717
  return `U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
2960
3718
  }
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") {
3719
+ function detectRowFormat(record3) {
3720
+ const input = record3.input;
3721
+ if (input && typeof input === "object" && typeof input.prompt === "string" && Array.isArray(input.assets) && input.assets.length > 0 && typeof record3.output === "string") {
2964
3722
  return "document_ocr_jsonl";
2965
3723
  }
2966
- if (typeof record2.input === "string" || typeof record2.output === "string" || !("input" in record2) || !("output" in record2)) {
3724
+ if (typeof record3.input === "string" || typeof record3.output === "string" || !("input" in record3) || !("output" in record3)) {
2967
3725
  return "jsonl";
2968
3726
  }
2969
3727
  return null;
2970
3728
  }
2971
- function validateDocumentOcrRow(record2, rowNumber, errors) {
2972
- const input = record2.input;
3729
+ function validateDocumentOcrRow(record3, rowNumber, errors) {
3730
+ const input = record3.input;
2973
3731
  const assets = input.assets;
2974
3732
  if (typeof input.prompt !== "string" || input.prompt.trim().length === 0) {
2975
3733
  errors.push(`Row ${rowNumber}: OCR input.prompt must be a non-empty string`);
@@ -2981,10 +3739,10 @@ function validateDocumentOcrRow(record2, rowNumber, errors) {
2981
3739
  );
2982
3740
  }
2983
3741
  }
2984
- if (typeof record2.output !== "string" || record2.output.trim().length === 0) {
3742
+ if (typeof record3.output !== "string" || record3.output.trim().length === 0) {
2985
3743
  errors.push(`Row ${rowNumber}: OCR output must be a non-empty string`);
2986
3744
  } else {
2987
- const hit = findControlChar(record2.output);
3745
+ const hit = findControlChar(record3.output);
2988
3746
  if (hit) {
2989
3747
  errors.push(
2990
3748
  `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`
@@ -3053,14 +3811,14 @@ function validateDatasetFile(file, requestedFormat) {
3053
3811
  errors.push(`Row ${rowNumber}: expected an object with "input" and "output" fields`);
3054
3812
  continue;
3055
3813
  }
3056
- const record2 = row;
3057
- if ("messages" in record2 && !("input" in record2) && !("output" in record2)) {
3814
+ const record3 = row;
3815
+ if ("messages" in record3 && !("input" in record3) && !("output" in record3)) {
3058
3816
  errors.push(
3059
3817
  `Row ${rowNumber}: found OpenAI SFT-style "messages"; Tuned Tensor datasets require flat "input" and "output" strings`
3060
3818
  );
3061
3819
  continue;
3062
3820
  }
3063
- const rowFormat = detectRowFormat(record2);
3821
+ const rowFormat = detectRowFormat(record3);
3064
3822
  if (!rowFormat) {
3065
3823
  errors.push(
3066
3824
  `Row ${rowNumber}: expected text {"input": string, "output": string} or OCR {"input":{"prompt": string, "assets": [...]}, "output": string}`
@@ -3077,17 +3835,17 @@ function validateDatasetFile(file, requestedFormat) {
3077
3835
  continue;
3078
3836
  }
3079
3837
  if (rowFormat === "document_ocr_jsonl") {
3080
- validateDocumentOcrRow(record2, rowNumber, errors);
3838
+ validateDocumentOcrRow(record3, rowNumber, errors);
3081
3839
  continue;
3082
3840
  }
3083
- if (typeof record2.input !== "string") {
3841
+ if (typeof record3.input !== "string") {
3084
3842
  errors.push(`Row ${rowNumber}: missing string "input" field`);
3085
3843
  }
3086
- if (typeof record2.output !== "string") {
3844
+ if (typeof record3.output !== "string") {
3087
3845
  errors.push(`Row ${rowNumber}: missing string "output" field`);
3088
3846
  }
3089
3847
  for (const field of ["input", "output"]) {
3090
- const value = record2[field];
3848
+ const value = record3[field];
3091
3849
  if (typeof value !== "string") continue;
3092
3850
  const hit = findControlChar(value);
3093
3851
  if (hit) {
@@ -4090,12 +4848,12 @@ var ManagedModelServer = class {
4090
4848
  clearTimeout(this.idleTimer);
4091
4849
  this.idleTimer = null;
4092
4850
  }
4093
- writeLog(record2) {
4851
+ writeLog(record3) {
4094
4852
  if (this.logRecordOverride) {
4095
- this.logRecordOverride(record2);
4853
+ this.logRecordOverride(record3);
4096
4854
  return;
4097
4855
  }
4098
- const line = `${JSON.stringify(record2)}
4856
+ const line = `${JSON.stringify(record3)}
4099
4857
  `;
4100
4858
  if (this.options.logFile) {
4101
4859
  mkdirSync2(dirname3(this.options.logFile), { recursive: true });
@@ -5535,7 +6293,7 @@ function registerModelsCommands(parent) {
5535
6293
  }
5536
6294
 
5537
6295
  // src/commands/balance.ts
5538
- import chalk4 from "chalk";
6296
+ import chalk5 from "chalk";
5539
6297
  var KIND_LABELS = {
5540
6298
  signup_bonus: "Signup bonus",
5541
6299
  topup: "Top-up",
@@ -5550,8 +6308,8 @@ function formatCents3(cents) {
5550
6308
  return `${sign}$${(abs / 100).toFixed(2)}`;
5551
6309
  }
5552
6310
  function formatSigned(cents) {
5553
- if (cents > 0) return chalk4.green(`+${formatCents3(cents)}`);
5554
- if (cents < 0) return chalk4.red(formatCents3(cents));
6311
+ if (cents > 0) return chalk5.green(`+${formatCents3(cents)}`);
6312
+ if (cents < 0) return chalk5.red(formatCents3(cents));
5555
6313
  return formatCents3(cents);
5556
6314
  }
5557
6315
  function registerBalanceCommands(parent) {
@@ -5566,7 +6324,7 @@ function registerBalanceCommands(parent) {
5566
6324
  return printJson({ balance, transactions });
5567
6325
  }
5568
6326
  const lowBalance = balance.balance_cents < 100;
5569
- const balanceLine = lowBalance ? chalk4.red(formatCents3(balance.balance_cents)) + chalk4.dim(" (low)") : chalk4.bold.green(formatCents3(balance.balance_cents));
6327
+ const balanceLine = lowBalance ? chalk5.red(formatCents3(balance.balance_cents)) + chalk5.dim(" (low)") : chalk5.bold.green(formatCents3(balance.balance_cents));
5570
6328
  printDetail([
5571
6329
  ["Credits", balanceLine],
5572
6330
  [
@@ -5581,7 +6339,7 @@ function registerBalanceCommands(parent) {
5581
6339
  ]);
5582
6340
  if (transactions && transactions.length > 0) {
5583
6341
  console.log(`
5584
- ${chalk4.bold("Recent transactions")}`);
6342
+ ${chalk5.bold("Recent transactions")}`);
5585
6343
  printTable(
5586
6344
  ["Date", "Type", "Amount", "Balance", "Reference"],
5587
6345
  transactions.map((row) => [
@@ -5589,19 +6347,19 @@ ${chalk4.bold("Recent transactions")}`);
5589
6347
  KIND_LABELS[row.kind] ?? row.kind,
5590
6348
  formatSigned(row.amount_cents),
5591
6349
  formatCents3(row.balance_after_cents),
5592
- row.reference_id ? shortId(row.reference_id) : chalk4.dim("\u2014")
6350
+ row.reference_id ? shortId(row.reference_id) : chalk5.dim("\u2014")
5593
6351
  ])
5594
6352
  );
5595
6353
  } else {
5596
6354
  console.log(
5597
6355
  `
5598
- ${chalk4.dim("No transactions yet \u2014 run `tt topup` to add credits.")}`
6356
+ ${chalk5.dim("No transactions yet \u2014 run `tt topup` to add credits.")}`
5599
6357
  );
5600
6358
  }
5601
6359
  if (lowBalance) {
5602
6360
  console.log(
5603
6361
  `
5604
- ${formatStatus("preparing")} ${chalk4.yellow(
6362
+ ${formatStatus("preparing")} ${chalk5.yellow(
5605
6363
  "Run `tt topup` to add more credits before starting a run."
5606
6364
  )}`
5607
6365
  );
@@ -5610,7 +6368,7 @@ ${formatStatus("preparing")} ${chalk4.yellow(
5610
6368
  }
5611
6369
 
5612
6370
  // src/commands/topup.ts
5613
- import chalk5 from "chalk";
6371
+ import chalk6 from "chalk";
5614
6372
  import open from "open";
5615
6373
  var PRESETS_USD = [10, 25, 50, 100];
5616
6374
  var MIN_USD = 5;
@@ -5649,9 +6407,9 @@ function registerTopupCommands(parent) {
5649
6407
  "amount is required in --json mode (use --amount <usd>)"
5650
6408
  );
5651
6409
  }
5652
- console.log(chalk5.bold("Add credits via Stripe Checkout"));
6410
+ console.log(chalk6.bold("Add credits via Stripe Checkout"));
5653
6411
  console.log(
5654
- chalk5.dim(
6412
+ chalk6.dim(
5655
6413
  `Quick picks: ${PRESETS_USD.map((n) => `$${n}`).join(", ")}`
5656
6414
  )
5657
6415
  );
@@ -5678,8 +6436,8 @@ function registerTopupCommands(parent) {
5678
6436
  }
5679
6437
  const url = data.checkout_url;
5680
6438
  console.log(
5681
- `${chalk5.bold("Checkout URL:")} ${chalk5.cyan(url)}
5682
- ` + chalk5.dim("Complete the payment to credit your account.")
6439
+ `${chalk6.bold("Checkout URL:")} ${chalk6.cyan(url)}
6440
+ ` + chalk6.dim("Complete the payment to credit your account.")
5683
6441
  );
5684
6442
  if (options.open !== false) {
5685
6443
  try {
@@ -5687,7 +6445,7 @@ function registerTopupCommands(parent) {
5687
6445
  printSuccess("Opened Stripe Checkout in your browser.");
5688
6446
  } catch {
5689
6447
  console.log(
5690
- chalk5.yellow(
6448
+ chalk6.yellow(
5691
6449
  "Could not open browser automatically \u2014 copy the URL above."
5692
6450
  )
5693
6451
  );
@@ -5697,7 +6455,7 @@ function registerTopupCommands(parent) {
5697
6455
  }
5698
6456
 
5699
6457
  // src/commands/eval.ts
5700
- import chalk6 from "chalk";
6458
+ import chalk7 from "chalk";
5701
6459
 
5702
6460
  // src/eval/rules.ts
5703
6461
  function validateSpec(spec) {
@@ -5862,7 +6620,7 @@ function registerEvalCommand(parent) {
5862
6620
  const spec = loadSpec(cmdOpts.file);
5863
6621
  const validation = validateSpec(spec);
5864
6622
  if (!isJsonMode()) {
5865
- console.log(chalk6.dim(`
6623
+ console.log(chalk7.dim(`
5866
6624
  Spec: ${cmdOpts.file}
5867
6625
  `));
5868
6626
  }
@@ -5877,10 +6635,10 @@ Spec: ${cmdOpts.file}
5877
6635
  });
5878
6636
  }
5879
6637
  function printValidation(checks) {
5880
- console.log(chalk6.bold("Spec Validation"));
6638
+ console.log(chalk7.bold("Spec Validation"));
5881
6639
  for (const check of checks) {
5882
- const icon = check.passed ? chalk6.green("\u2713") : chalk6.red("\u2717");
5883
- const msg = check.message ? chalk6.dim(` \u2014 ${check.message}`) : "";
6640
+ const icon = check.passed ? chalk7.green("\u2713") : chalk7.red("\u2717");
6641
+ const msg = check.message ? chalk7.dim(` \u2014 ${check.message}`) : "";
5884
6642
  console.log(` ${icon} ${check.name}${msg}`);
5885
6643
  }
5886
6644
  }
@@ -6036,16 +6794,28 @@ function createProgram(version, runtime = {}) {
6036
6794
  const openShell = async () => {
6037
6795
  const root = program.opts();
6038
6796
  const shellEnvironment = childEnvironment(root, env);
6797
+ const output = runtime.stdout ?? process.stdout;
6798
+ const error = runtime.stderr ?? process.stderr;
6799
+ const agent = createShellAgent({
6800
+ client: runtime.agentClient ?? createAgentClient({
6801
+ apiKey: root.apiKey ?? shellEnvironment.TUNED_TENSOR_API_KEY,
6802
+ baseUrl: root.baseUrl ?? shellEnvironment.TUNED_TENSOR_URL
6803
+ }),
6804
+ output,
6805
+ error
6806
+ });
6039
6807
  await launchShell({
6040
6808
  runner: shellRunner,
6809
+ agent,
6041
6810
  input: runtime.stdin ?? process.stdin,
6042
- output: runtime.stdout ?? process.stdout,
6043
- error: runtime.stderr ?? process.stderr,
6811
+ output,
6812
+ error,
6044
6813
  cwd,
6045
- env: shellEnvironment
6814
+ env: shellEnvironment,
6815
+ version
6046
6816
  });
6047
6817
  };
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(
6818
+ program.name("tt").description("Tuned Tensor \u2014 converse, train, and inspect from one terminal").version(version).option("-k, --api-key <key>", "API key (overrides stored key)").option(
6049
6819
  "-u, --base-url <url>",
6050
6820
  "API base URL (default: https://tunedtensor.com)"
6051
6821
  ).option("--json", "Output raw JSON").option("--no-color", "Disable colors").showSuggestionAfterError().hook("preAction", () => {
@@ -6053,7 +6823,7 @@ function createProgram(version, runtime = {}) {
6053
6823
  if (root.json) setJsonMode(true);
6054
6824
  if (root.color === false) {
6055
6825
  process.env.FORCE_COLOR = "0";
6056
- chalk7.level = 0;
6826
+ chalk8.level = 0;
6057
6827
  }
6058
6828
  });
6059
6829
  registerAuthCommands(program);
@@ -6086,12 +6856,12 @@ function createProgram(version, runtime = {}) {
6086
6856
  `);
6087
6857
  return;
6088
6858
  }
6089
- output.write(`${chalk7.bold.cyan("Tuned Tensor status")}
6859
+ output.write(`${chalk8.bold.cyan("Tuned Tensor status")}
6090
6860
  `);
6091
6861
  output.write(`${formatShellStatus(context, target).join("\n")}
6092
6862
 
6093
6863
  `);
6094
- output.write(`${chalk7.bold("Context")}
6864
+ output.write(`${chalk8.bold("Context")}
6095
6865
  `);
6096
6866
  output.write(
6097
6867
  `${formatShellContext(
@@ -6105,7 +6875,7 @@ function createProgram(version, runtime = {}) {
6105
6875
  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
6876
  const root = program.opts();
6107
6877
  const passthrough = extractPassthroughOptions(args, root);
6108
- if (!passthrough.color) chalk7.level = 0;
6878
+ if (!passthrough.color) chalk8.level = 0;
6109
6879
  const result = await invokeLocal(
6110
6880
  passthrough.args.length > 0 ? passthrough.args : ["--help"],
6111
6881
  {
@@ -6145,7 +6915,7 @@ function createProgram(version, runtime = {}) {
6145
6915
  "after",
6146
6916
  `
6147
6917
  Workflows:
6148
- tt Open the interactive terminal (TTY only)
6918
+ tt Open the conversational terminal (TTY only)
6149
6919
  tt status Inspect cloud/local project context
6150
6920
  tt cloud <command> Run a hosted command explicitly
6151
6921
  tt local <command> Run a local GPU command
@@ -6158,6 +6928,22 @@ Examples:
6158
6928
  );
6159
6929
  return program;
6160
6930
  }
6931
+ function createShellAgent(options) {
6932
+ return new TunedTensorAgentSession({
6933
+ client: options.client,
6934
+ io: {
6935
+ write(text2) {
6936
+ options.output.write(text2);
6937
+ },
6938
+ writeError(text2) {
6939
+ options.error.write(text2);
6940
+ },
6941
+ clear() {
6942
+ options.output.write("\x1B[2J\x1B[H");
6943
+ }
6944
+ }
6945
+ });
6946
+ }
6161
6947
  async function runCli(version, runtime = {}) {
6162
6948
  const argv = runtime.argv ?? process.argv;
6163
6949
  const env = runtime.env ?? process.env;
@@ -6169,6 +6955,16 @@ async function runCli(version, runtime = {}) {
6169
6955
  env
6170
6956
  })) {
6171
6957
  const invokeSelf = runtime.runSelfCommand ?? runSelfCommand;
6958
+ const output = runtime.stdout ?? process.stdout;
6959
+ const error = runtime.stderr ?? process.stderr;
6960
+ const agent = createShellAgent({
6961
+ client: runtime.agentClient ?? createAgentClient({
6962
+ apiKey: env.TUNED_TENSOR_API_KEY,
6963
+ baseUrl: env.TUNED_TENSOR_URL
6964
+ }),
6965
+ output,
6966
+ error
6967
+ });
6172
6968
  await (runtime.startShell ?? startInteractiveShell)({
6173
6969
  runner: async (request2) => await invokeSelf(
6174
6970
  request2.target === "local" ? ["local", ...request2.args] : request2.args,
@@ -6178,11 +6974,13 @@ async function runCli(version, runtime = {}) {
6178
6974
  entrypoint: argv[1]
6179
6975
  }
6180
6976
  ),
6977
+ agent,
6181
6978
  input: runtime.stdin ?? process.stdin,
6182
- output: runtime.stdout ?? process.stdout,
6183
- error: runtime.stderr ?? process.stderr,
6979
+ output,
6980
+ error,
6184
6981
  cwd: runtime.cwd ?? process.cwd(),
6185
- env
6982
+ env,
6983
+ version
6186
6984
  });
6187
6985
  return;
6188
6986
  }
@@ -6191,7 +6989,7 @@ async function runCli(version, runtime = {}) {
6191
6989
  }
6192
6990
 
6193
6991
  // src/index.ts
6194
- runCli("0.5.0").catch((err) => {
6992
+ runCli("0.7.0").catch((err) => {
6195
6993
  reportError(err);
6196
6994
  process.exit(1);
6197
6995
  });