tines 0.0.1 → 0.0.76

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.
Files changed (2) hide show
  1. package/dist/index.js +641 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
4
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync3 } from "node:fs";
5
5
  import { hostname as hostname2 } from "node:os";
6
- import { dirname as dirname3, join as join3 } from "node:path";
6
+ import { basename, dirname as dirname3, join as join3 } from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
8
 
9
9
  // src/daemon/daemon.ts
@@ -40,12 +40,46 @@ function repoDirFromUrl(url) {
40
40
  if (!base || base === "." || base === ".." || base.includes("\\") || base.includes("=")) return "repo";
41
41
  return base;
42
42
  }
43
+ var ARTIFACT_FILE_MAX_BYTES = 25 * 1024 * 1024;
44
+ var ARTIFACT_TEXT_MAX_BYTES = 256 * 1024;
45
+ var ARTIFACT_FOLDER_MAX_BYTES = 50 * 1024 * 1024;
46
+ function canonicalGitHubRepoUrl(url) {
47
+ const match = url.trim().match(
48
+ /^(?:(?:https?|ssh):\/\/(?:[^@/]+@)?|git@)?(?:www\.)?github\.com[/:]([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/
49
+ );
50
+ if (!match) return null;
51
+ return `https://github.com/${match[1]}/${match[2]}`;
52
+ }
53
+ function parsePrSpec(spec) {
54
+ const short = spec.trim().match(/^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)#(\d+)$/);
55
+ if (short) {
56
+ const repoUrl2 = canonicalGitHubRepoUrl(`https://github.com/${short[1]}`);
57
+ return repoUrl2 ? { repo_url: repoUrl2, number: Number.parseInt(short[2], 10) } : null;
58
+ }
59
+ const url = spec.trim().match(/^(.*?)\/pull\/(\d+)(?:[/?#].*)?$/);
60
+ if (!url) return null;
61
+ const repoUrl = canonicalGitHubRepoUrl(url[1]);
62
+ return repoUrl ? { repo_url: repoUrl, number: Number.parseInt(url[2], 10) } : null;
63
+ }
43
64
  var MODEL_TIERS = ["smartest", "balanced", "cheapest"];
44
65
  var RUNNER_ONLINE_WINDOW_MS = 2 * 60 * 1e3;
45
66
  var RUNNER_OFFLINE_FAIL_MS = 5 * 60 * 1e3;
46
67
  var LAUNCH_STALL_MS = 5 * 60 * 1e3;
47
68
  var RUN_KEY_SLACK_MS = 10 * 60 * 1e3;
48
69
  var RUN_LOG_MAX_BYTES = 256 * 1024;
70
+ var MODEL_PREDECESSORS = {
71
+ "claude-fable-5": ["claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
72
+ "claude-opus-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5", "claude-opus-4-1"],
73
+ "claude-sonnet-5": ["claude-sonnet-4-6", "claude-sonnet-4-5", "claude-3-7-sonnet-latest"],
74
+ "claude-haiku-4-5": ["claude-3-5-haiku-latest"],
75
+ "gemini-2.5-pro": ["gemini-1.5-pro"],
76
+ "gemini-2.5-flash": ["gemini-2.0-flash", "gemini-1.5-flash"],
77
+ "gemini-2.5-flash-lite": ["gemini-2.0-flash-lite", "gemini-1.5-flash-8b"]
78
+ };
79
+ function isStaleTierOverride(builtinModel, overrideModel) {
80
+ if (!builtinModel || !overrideModel || builtinModel === overrideModel) return false;
81
+ return (MODEL_PREDECESSORS[builtinModel] ?? []).includes(overrideModel);
82
+ }
49
83
  function runDurationLabel(run, now = Date.now()) {
50
84
  if (!run.started_at) return "\u2014";
51
85
  const seconds = Math.max(0, Math.round(((run.ended_at ?? now) - run.started_at) / 1e3));
@@ -116,6 +150,22 @@ function createApiClient(options) {
116
150
  return await res.json();
117
151
  }
118
152
  const get = (path) => request("GET", path);
153
+ async function raw(method, path, opts = {}) {
154
+ const headers = { ...opts.headers ?? {} };
155
+ if (options.apiKey) headers.authorization = `Bearer ${options.apiKey}`;
156
+ const body = opts.body instanceof Uint8Array ? opts.body.buffer.slice(opts.body.byteOffset, opts.body.byteOffset + opts.body.byteLength) : opts.body;
157
+ const res = await fetchFn(`${base}${path}`, { method, headers, body });
158
+ if (!res.ok) {
159
+ let parsed = null;
160
+ try {
161
+ parsed = (await res.json()).error ?? null;
162
+ } catch {
163
+ }
164
+ throw new ApiError(res.status, parsed, `${method} ${path} failed: ${res.status}`);
165
+ }
166
+ return res;
167
+ }
168
+ const artifactPath = (issueId, name, suffix = "") => `/api/v1/issues/${issueId}/artifacts/${encodeURIComponent(name)}${suffix}`;
119
169
  return {
120
170
  getTime: () => get("/api/time"),
121
171
  // Projects
@@ -166,8 +216,60 @@ function createApiClient(options) {
166
216
  appendContextItem: (id, body) => request("POST", `/api/v1/context/${id}/append`, body),
167
217
  /** Effective context for an issue: the assembled bundle. */
168
218
  getIssueContext: (issueId) => get(`/api/v1/issues/${issueId}/context`),
219
+ /**
220
+ * Which journal this caller's `tines journal` commands target — the
221
+ * run's launch state for a run key, the issue's current state otherwise.
222
+ */
223
+ getIssueJournal: (issueId) => get(`/api/v1/issues/${issueId}/journal`),
169
224
  /** Launch prompt: stitched context plus the generated issue block. */
170
225
  getIssuePrompt: (issueId) => get(`/api/v1/issues/${issueId}/prompt`),
226
+ // Issue artifacts (name-addressed under the issue)
227
+ listArtifacts: (issueId) => get(`/api/v1/issues/${issueId}/artifacts`),
228
+ getArtifact: (issueId, name) => get(artifactPath(issueId, name)),
229
+ /** JSON upsert for text/link/pr: creates the artifact or appends a version. */
230
+ putArtifact: (issueId, name, body) => request("PUT", artifactPath(issueId, name), body),
231
+ /** Raw-body upload for `file`: creates the artifact or appends a version. */
232
+ uploadArtifactFile: async (issueId, name, bytes, opts) => {
233
+ const res = await raw(
234
+ "PUT",
235
+ artifactPath(issueId, name, `/file?filename=${encodeURIComponent(opts.filename)}`),
236
+ {
237
+ body: bytes,
238
+ headers: { "content-type": opts.contentType }
239
+ }
240
+ );
241
+ return await res.json();
242
+ },
243
+ /**
244
+ * Multipart snapshot upload for `folder`: every file of the new version
245
+ * in one request (path as the part filename, MIME as the part type).
246
+ */
247
+ uploadArtifactFolder: async (issueId, name, files) => {
248
+ const form = new FormData();
249
+ for (const file of files) {
250
+ form.append("file", new Blob([file.bytes], { type: file.contentType }), file.path);
251
+ }
252
+ const res = await raw("PUT", artifactPath(issueId, name, "/folder"), { body: form });
253
+ return await res.json();
254
+ },
255
+ /** Bless the current content as fresh: appends a reaffirming version. */
256
+ reaffirmArtifact: (issueId, name) => request("POST", artifactPath(issueId, name, "/reaffirm")),
257
+ /** Bytes of a version (default: current; `path` selects a folder entry). */
258
+ getArtifactContent: async (issueId, name, opts = {}) => {
259
+ const params = new URLSearchParams();
260
+ if (opts.version !== void 0) params.set("version", String(opts.version));
261
+ if (opts.path !== void 0) params.set("path", opts.path);
262
+ const query2 = params.toString() ? `?${params.toString()}` : "";
263
+ const res = await raw("GET", artifactPath(issueId, name, `/content${query2}`));
264
+ const disposition = res.headers.get("content-disposition") ?? "";
265
+ const filenameMatch = disposition.match(/filename="((?:[^"\\]|\\.)*)"/);
266
+ return {
267
+ bytes: await res.arrayBuffer(),
268
+ content_type: res.headers.get("content-type"),
269
+ filename: filenameMatch ? filenameMatch[1].replaceAll('\\"', '"') : null
270
+ };
271
+ },
272
+ deleteArtifact: (issueId, name) => request("DELETE", artifactPath(issueId, name)),
171
273
  // Events
172
274
  listEvents: (filters = {}) => get(`/api/v1/events${query(filters)}`),
173
275
  // Runners
@@ -766,6 +868,17 @@ function reportError(err) {
766
868
  message2 += actions.length > 0 ? `
767
869
  allowed actions: ${actions.join(", ")}` : "\nallowed actions: none (terminal state)";
768
870
  }
871
+ const unmet = err.details?.unmet;
872
+ if (Array.isArray(unmet)) {
873
+ for (const raw of unmet) {
874
+ const r = raw;
875
+ const spec = [r.type, r.content_type].filter(Boolean).join(", ");
876
+ message2 += `
877
+ requires artifact "${r.artifact}"${spec ? ` (${spec})` : ""}: ${r.status ?? "unmet"}${r.description ? ` \u2014 ${r.description}` : ""}`;
878
+ if (r.fix) message2 += `
879
+ fix: ${r.fix}`;
880
+ }
881
+ }
769
882
  die(message2);
770
883
  }
771
884
  die(err instanceof Error ? err.message : String(err));
@@ -861,6 +974,19 @@ existing sets wholesale when present.
861
974
 
862
975
  Each NEW state should carry a "prompt" \u2014 its initial stage instructions,
863
976
  created as a state-scoped context item \u2014 or pass --no-prompts to skip.
977
+
978
+ A transition may declare artifact requirements ("requires"): it can then only
979
+ be taken once a FRESH artifact with that name \u2014 attached (or reaffirmed)
980
+ since the issue entered its current state \u2014 exists on the issue:
981
+
982
+ { "name": "approve", "from": "In review", "to": "Done",
983
+ "requires": [ { "artifact": "design-doc", "type": "file",
984
+ "content_type": "text/markdown",
985
+ "description": "The approved design for this round" } ] }
986
+
987
+ "type" (file | text | link | pr) and "content_type" (a prefix match, e.g.
988
+ "image/"; file/text only) are optional narrowing; "artifact" is the slot
989
+ name issues must carry (see: tines issues artifacts --help).
864
990
  `;
865
991
  async function resolveProject(api, ref) {
866
992
  const { items } = await api.listProjects();
@@ -971,6 +1097,8 @@ function contextItemSummary(item) {
971
1097
  return `${item.file_count ?? item.files?.length ?? 0} file${(item.file_count ?? item.files?.length ?? 0) === 1 ? "" : "s"}`;
972
1098
  case "repo":
973
1099
  return `${item.repo_url}${item.repo_branch ? `#${item.repo_branch}` : ""}`;
1100
+ case "artifact":
1101
+ return item.artifact_type ?? "artifact";
974
1102
  }
975
1103
  }
976
1104
  function printContextItem(item) {
@@ -978,7 +1106,14 @@ function printContextItem(item) {
978
1106
  if (item.description) console.log(item.description);
979
1107
  console.log(`scope: ${item.scope.label}`);
980
1108
  console.log(`updated: ${timestamp(item.updated_at)} created: ${timestamp(item.created_at)}`);
981
- if (item.kind === "prompt") {
1109
+ if (item.kind === "artifact") {
1110
+ console.log(`
1111
+ type: ${item.artifact_type ?? "artifact"}`);
1112
+ if (item.scope.issue_ref) {
1113
+ const ref = `${item.scope.issue_ref.project_name}/${item.scope.issue_ref.number}`;
1114
+ console.log(`versions and content: tines issues artifacts show ${ref} ${item.name}`);
1115
+ }
1116
+ } else if (item.kind === "prompt") {
982
1117
  console.log(`
983
1118
  ${item.body}`);
984
1119
  } else if (item.kind === "skill") {
@@ -1133,6 +1268,12 @@ function printWorkflowDetail(wf) {
1133
1268
  console.log(
1134
1269
  ` "${t.name}": ${byId.get(t.from_state_id)?.name} \u2192 ${byId.get(t.to_state_id)?.name}`
1135
1270
  );
1271
+ for (const r of t.requires ?? []) {
1272
+ const spec = [r.type, r.content_type].filter(Boolean).join(", ");
1273
+ console.log(
1274
+ ` requires artifact "${r.artifact}"${spec ? ` (${spec})` : ""}${r.description ? ` \u2014 ${r.description}` : ""}`
1275
+ );
1276
+ }
1136
1277
  }
1137
1278
  for (const w of wf.warnings ?? []) console.log(`
1138
1279
  warning: ${w}`);
@@ -1200,8 +1341,16 @@ function eventSummary(ev) {
1200
1341
  return ev.type;
1201
1342
  }
1202
1343
  }
1344
+ function cliVersion() {
1345
+ try {
1346
+ const manifest = new URL("../package.json", import.meta.url);
1347
+ return JSON.parse(readFileSync2(manifest, "utf8")).version ?? "0.0.0-unknown";
1348
+ } catch {
1349
+ return "0.0.0-unknown";
1350
+ }
1351
+ }
1203
1352
  var program = new Command();
1204
- program.name("tines").description("CLI for Tines").version("0.0.1").enablePositionalOptions();
1353
+ program.name("tines").description("CLI for Tines").version(cliVersion()).enablePositionalOptions();
1205
1354
  withCommon(program.command("time").description("Fetch the current time from the Tines API")).action(
1206
1355
  async (opts) => {
1207
1356
  const result = await client(opts).getTime();
@@ -1362,7 +1511,7 @@ withCommon(
1362
1511
  });
1363
1512
  var issues = program.command("issues").description("Work with issues");
1364
1513
  withList(
1365
- issues.command("list").description("List issues across projects (hides done issues unless --all)").option("-p, --project <name>", "filter by project name or id").option("-s, --state <name>", "filter by state name or id").option("-c, --category <cat>", "filter by state category").option("-w, --workflow <id-or-name>", "filter by workflow").option("-a, --all", "include issues in done states").option("--ready", "only issues that are actionable now (not done, not a duplicate, no open blockers)")
1514
+ issues.command("list").description("List issues across projects (hides done issues unless --all)").option("-p, --project <name>", "filter by project name or id").option("-s, --state <name>", "filter by state name or id").option("-c, --category <cat>", "filter by state category").option("-w, --workflow <id-or-name>", "filter by workflow").option("-a, --all", "include issues in done states").option("--ready", "only issues that are actionable now (not done, not a duplicate, no open blockers)").option("-q, --search <text>", "search titles and descriptions")
1366
1515
  ).action(
1367
1516
  async (opts) => {
1368
1517
  const res = await client(opts).listIssues({
@@ -1372,6 +1521,7 @@ withList(
1372
1521
  workflow: opts.workflow,
1373
1522
  hide_done: !opts.all,
1374
1523
  ready: opts.ready,
1524
+ q: opts.search,
1375
1525
  limit: opts.limit,
1376
1526
  cursor: opts.cursor
1377
1527
  });
@@ -1597,6 +1747,274 @@ withCommon(
1597
1747
  if (opts.json) return printJson(prompt);
1598
1748
  console.log(prompt.text);
1599
1749
  });
1750
+ var artifactsCmd = issues.command("artifacts").description("Typed, versioned attachments on an issue \u2014 the work products transition requirements gate on");
1751
+ var MIME_BY_EXT = {
1752
+ md: "text/markdown",
1753
+ markdown: "text/markdown",
1754
+ txt: "text/plain",
1755
+ log: "text/plain",
1756
+ html: "text/html",
1757
+ htm: "text/html",
1758
+ css: "text/css",
1759
+ csv: "text/csv",
1760
+ js: "text/javascript",
1761
+ json: "application/json",
1762
+ pdf: "application/pdf",
1763
+ png: "image/png",
1764
+ jpg: "image/jpeg",
1765
+ jpeg: "image/jpeg",
1766
+ gif: "image/gif",
1767
+ webp: "image/webp",
1768
+ svg: "image/svg+xml",
1769
+ zip: "application/zip",
1770
+ gz: "application/gzip",
1771
+ mp4: "video/mp4",
1772
+ webm: "video/webm"
1773
+ };
1774
+ function sniffContentType(path) {
1775
+ const ext = path.match(/\.([A-Za-z0-9]+)$/)?.[1]?.toLowerCase();
1776
+ return ext && MIME_BY_EXT[ext] || "application/octet-stream";
1777
+ }
1778
+ function prRefLabel(v) {
1779
+ const path = (v.pr_repo_url ?? "").replace(/^https:\/\/github\.com\//, "");
1780
+ return `${path}#${v.pr_number}`;
1781
+ }
1782
+ function artifactSummary(a) {
1783
+ const cv = a.current_version;
1784
+ switch (a.artifact_type) {
1785
+ case "file":
1786
+ return `${cv.filename} (${cv.content_type}, ${cv.size_bytes} bytes)`;
1787
+ case "folder":
1788
+ return `${cv.file_count} file${cv.file_count === 1 ? "" : "s"} (${cv.size_bytes} bytes total)`;
1789
+ case "text":
1790
+ return `${cv.filename} (${cv.content_type})`;
1791
+ case "link":
1792
+ return cv.title ? `${cv.title} \u2014 ${cv.url}` : cv.url ?? "";
1793
+ case "pr":
1794
+ return `${prRefLabel(cv)} \u2014 ${cv.pr_repo_url}/pull/${cv.pr_number}`;
1795
+ }
1796
+ }
1797
+ function walkFolder(dir) {
1798
+ const files = [];
1799
+ const walk = (abs, rel) => {
1800
+ for (const entry of readdirSync(abs, { withFileTypes: true })) {
1801
+ const nextAbs = join3(abs, entry.name);
1802
+ const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
1803
+ if (entry.isDirectory()) walk(nextAbs, nextRel);
1804
+ else if (entry.isFile()) {
1805
+ files.push({ path: nextRel, contentType: sniffContentType(entry.name), bytes: readFileSync2(nextAbs) });
1806
+ }
1807
+ }
1808
+ };
1809
+ walk(dir, "");
1810
+ return files;
1811
+ }
1812
+ withCommon(artifactsCmd.command("list <ref>").description("List the artifacts attached to an issue")).action(
1813
+ async (ref, opts) => {
1814
+ const api = client(opts);
1815
+ const issue = await resolveIssue(api, ref);
1816
+ const res = await api.listArtifacts(issue.id);
1817
+ if (opts.json) return printJson(res);
1818
+ if (res.items.length === 0) return console.log("no artifacts attached");
1819
+ table([
1820
+ ["NAME", "TYPE", "VERSION", "FRESH", "SUMMARY", "ATTACHED"],
1821
+ ...res.items.map((a) => [
1822
+ a.name,
1823
+ a.artifact_type,
1824
+ `v${a.current_version.version}`,
1825
+ a.fresh ? "yes" : "no",
1826
+ artifactSummary(a),
1827
+ timestamp(a.current_version.created_at)
1828
+ ])
1829
+ ]);
1830
+ }
1831
+ );
1832
+ withCommon(
1833
+ artifactsCmd.command("show <ref> <name>").description("Show an artifact with its full version history")
1834
+ ).action(async (ref, name, opts) => {
1835
+ const api = client(opts);
1836
+ const issue = await resolveIssue(api, ref);
1837
+ const artifact = await api.getArtifact(issue.id, name);
1838
+ if (opts.json) return printJson(artifact);
1839
+ console.log(`${artifact.artifact_type} artifact "${artifact.name}" on ${issue.project_name}/${issue.number}`);
1840
+ if (artifact.description) console.log(artifact.description);
1841
+ console.log(
1842
+ `current: v${artifact.current_version.version} (${artifact.fresh ? "fresh" : "attached before the current state \u2014 reaffirm or attach a new version to satisfy gates"})`
1843
+ );
1844
+ console.log(`summary: ${artifactSummary(artifact)}`);
1845
+ console.log("\nversions:");
1846
+ table(
1847
+ artifact.versions.map((v) => [
1848
+ ` v${v.version}`,
1849
+ timestamp(v.created_at),
1850
+ actorLabel(v.actor),
1851
+ v.reaffirmed_from !== null ? `reaffirmed v${v.reaffirmed_from}` : v.file_count !== null ? `${v.file_count} file${v.file_count === 1 ? "" : "s"}` : v.filename ?? v.url ?? (v.pr_repo_url ? prRefLabel(v) : "")
1852
+ ])
1853
+ );
1854
+ const files = artifact.current_version.files;
1855
+ if (files && files.length > 0) {
1856
+ console.log(`
1857
+ files (v${artifact.current_version.version}):`);
1858
+ table(files.map((f) => [` ${f.path}`, f.content_type, `${f.size_bytes} bytes`]));
1859
+ }
1860
+ });
1861
+ withCommon(
1862
+ artifactsCmd.command("attach <ref> <name>").description("Attach content to a named artifact slot (creates it, or appends the next version)").option("-f, --file <path>", "upload a file (MIME sniffed from the extension)").option(
1863
+ "--folder <dir>",
1864
+ "snapshot a directory tree as one version (collect locally, attach once; MIME per file sniffed)"
1865
+ ).option("-t, --text <md|@file>", "inline text document: inline Markdown or @file").option("--url <url>", "link: the URL to attach").option("--pr <spec>", "PR reference: owner/repo#N or a GitHub PR URL").option("--content-type <mime>", "declared MIME type (with --file or --text)").option("--filename <name>", "display filename (with --text; defaults to <name>.md)").option("--title <title>", "display title (with --url)").option("-d, --description <text>", "artifact description, shown in lists and launch prompts"),
1866
+ // --url is the link payload here; the API base comes from TINES_API_URL.
1867
+ { baseUrlFlag: false }
1868
+ ).action(
1869
+ async (ref, name, opts) => {
1870
+ const api = client({ apiKey: opts.apiKey, json: opts.json });
1871
+ const sources = [opts.file, opts.folder, opts.text, opts.url, opts.pr].filter((v) => v !== void 0);
1872
+ if (sources.length !== 1) {
1873
+ die(
1874
+ "pass exactly one content source: --file <path>, --folder <dir>, --text <md|@file>, --url <url>, or --pr <spec>"
1875
+ );
1876
+ }
1877
+ const issue = await resolveIssue(api, ref);
1878
+ let artifact;
1879
+ if (opts.folder !== void 0) {
1880
+ if (!existsSync2(opts.folder) || !statSync(opts.folder).isDirectory()) {
1881
+ die(`--folder needs a directory, got "${opts.folder}"`);
1882
+ }
1883
+ const files = walkFolder(opts.folder);
1884
+ if (files.length === 0) die(`${opts.folder} contains no files to snapshot`);
1885
+ artifact = await api.uploadArtifactFolder(issue.id, name, files);
1886
+ if (opts.description !== void 0) {
1887
+ artifact = await api.putArtifact(issue.id, name, { description: opts.description });
1888
+ }
1889
+ } else if (opts.file !== void 0) {
1890
+ let bytes;
1891
+ try {
1892
+ bytes = readFileSync2(opts.file);
1893
+ } catch (err) {
1894
+ die(`cannot read ${opts.file}: ${err instanceof Error ? err.message : String(err)}`);
1895
+ }
1896
+ artifact = await api.uploadArtifactFile(issue.id, name, bytes, {
1897
+ filename: opts.filename ?? basename(opts.file),
1898
+ contentType: opts.contentType ?? sniffContentType(opts.file)
1899
+ });
1900
+ if (opts.description !== void 0) {
1901
+ artifact = await api.putArtifact(issue.id, name, { description: opts.description });
1902
+ }
1903
+ } else if (opts.text !== void 0) {
1904
+ artifact = await api.putArtifact(issue.id, name, {
1905
+ type: "text",
1906
+ content: readBodyValue(opts.text),
1907
+ ...opts.filename !== void 0 ? { filename: opts.filename } : {},
1908
+ ...opts.contentType !== void 0 ? { content_type: opts.contentType } : {},
1909
+ ...opts.description !== void 0 ? { description: opts.description } : {}
1910
+ });
1911
+ } else if (opts.url !== void 0) {
1912
+ artifact = await api.putArtifact(issue.id, name, {
1913
+ type: "link",
1914
+ url: opts.url,
1915
+ ...opts.title !== void 0 ? { title: opts.title } : {},
1916
+ ...opts.description !== void 0 ? { description: opts.description } : {}
1917
+ });
1918
+ } else {
1919
+ const parsed = parsePrSpec(opts.pr);
1920
+ if (!parsed) {
1921
+ die(`--pr takes owner/repo#N or a GitHub PR URL, got "${opts.pr}"`);
1922
+ }
1923
+ artifact = await api.putArtifact(issue.id, name, {
1924
+ type: "pr",
1925
+ pr_repo_url: parsed.repo_url,
1926
+ pr_number: parsed.number,
1927
+ ...opts.description !== void 0 ? { description: opts.description } : {}
1928
+ });
1929
+ }
1930
+ if (opts.json) return printJson(artifact);
1931
+ console.log(
1932
+ `attached "${artifact.name}" v${artifact.current_version.version} (${artifactSummary(artifact)}) to ${issue.project_name}/${issue.number} \u2014 fresh`
1933
+ );
1934
+ }
1935
+ );
1936
+ withCommon(
1937
+ artifactsCmd.command("reaffirm <ref> <name>").description("Bless the current content as fresh (appends a version reusing the same payload)")
1938
+ ).action(async (ref, name, opts) => {
1939
+ const api = client(opts);
1940
+ const issue = await resolveIssue(api, ref);
1941
+ const artifact = await api.reaffirmArtifact(issue.id, name);
1942
+ if (opts.json) return printJson(artifact);
1943
+ console.log(
1944
+ `reaffirmed "${artifact.name}" on ${issue.project_name}/${issue.number}: v${artifact.current_version.version} reaffirms v${artifact.current_version.reaffirmed_from} \u2014 fresh as of now`
1945
+ );
1946
+ });
1947
+ withCommon(
1948
+ artifactsCmd.command("get <ref> <name>").description("Fetch content (current version by default); a link/pr prints its URL").option("--version <n>", "fetch a specific version from the history", (v) => Number.parseInt(v, 10)).option("--out <path>", "write to this file, or into this directory (keeps the stored filename)")
1949
+ ).action(
1950
+ async (ref, name, opts) => {
1951
+ const api = client(opts);
1952
+ const issue = await resolveIssue(api, ref);
1953
+ const artifact = await api.getArtifact(issue.id, name);
1954
+ const version = opts.version === void 0 ? artifact.current_version : artifact.versions.find((v) => v.version === opts.version);
1955
+ if (!version) {
1956
+ die(
1957
+ `artifact "${name}" has no version ${opts.version} (history: v1\u2013v${artifact.current_version.version})`
1958
+ );
1959
+ }
1960
+ if (artifact.artifact_type === "link" || artifact.artifact_type === "pr") {
1961
+ const url = artifact.artifact_type === "link" ? version.url : `${version.pr_repo_url}/pull/${version.pr_number}`;
1962
+ if (opts.json) return printJson({ url });
1963
+ return console.log(url);
1964
+ }
1965
+ if (artifact.artifact_type === "folder") {
1966
+ if (opts.out === void 0) {
1967
+ die(`artifact "${name}" is a folder \u2014 pass --out <dir> to write its tree`);
1968
+ }
1969
+ if (existsSync2(opts.out) && !statSync(opts.out).isDirectory()) {
1970
+ die(`--out for a folder must be a directory, and "${opts.out}" is a file`);
1971
+ }
1972
+ const files = version.files ?? [];
1973
+ let total = 0;
1974
+ for (const file of files) {
1975
+ const content2 = await api.getArtifactContent(issue.id, name, {
1976
+ version: opts.version,
1977
+ path: file.path
1978
+ });
1979
+ const target2 = join3(opts.out, file.path);
1980
+ mkdirSync3(dirname3(target2), { recursive: true });
1981
+ writeFileSync3(target2, Buffer.from(content2.bytes));
1982
+ total += content2.bytes.byteLength;
1983
+ }
1984
+ return console.log(
1985
+ `wrote ${files.length} file${files.length === 1 ? "" : "s"} (${total} bytes) from "${name}" v${version.version} into ${opts.out}/`
1986
+ );
1987
+ }
1988
+ const content = await api.getArtifactContent(issue.id, name, { version: opts.version });
1989
+ const bytes = Buffer.from(content.bytes);
1990
+ if (opts.out !== void 0) {
1991
+ let target2 = opts.out;
1992
+ if (existsSync2(target2) && statSync(target2).isDirectory()) {
1993
+ target2 = join3(target2, version.filename ?? name);
1994
+ }
1995
+ writeFileSync3(target2, bytes);
1996
+ return console.log(`wrote ${target2} (${bytes.byteLength} bytes, ${content.content_type})`);
1997
+ }
1998
+ if ((content.content_type ?? "").startsWith("text/")) {
1999
+ process.stdout.write(bytes);
2000
+ return;
2001
+ }
2002
+ const target = version.filename ?? name;
2003
+ writeFileSync3(target, bytes);
2004
+ console.log(`wrote ${target} (${bytes.byteLength} bytes, ${content.content_type})`);
2005
+ }
2006
+ );
2007
+ withCommon(
2008
+ artifactsCmd.command("delete <ref> <name>").description("Delete an artifact \u2014 every version and its stored files (history is not recoverable)")
2009
+ ).action(async (ref, name, opts) => {
2010
+ const api = client(opts);
2011
+ const issue = await resolveIssue(api, ref);
2012
+ const artifact = await api.getArtifact(issue.id, name);
2013
+ await api.deleteArtifact(issue.id, name);
2014
+ console.log(
2015
+ `deleted ${artifact.artifact_type} artifact "${name}" from ${issue.project_name}/${issue.number} (${artifact.version_count} version${artifact.version_count === 1 ? "" : "s"})`
2016
+ );
2017
+ });
1600
2018
  withCommon(
1601
2019
  issues.command("assign <ref> [runner]").description("Pin an issue to a runner (<runner>[:tier]) \u2014 replaces routing rules for it; --clear unpins").option("--clear", "remove the pin")
1602
2020
  ).action(async (ref, runnerSpec, opts) => {
@@ -1840,88 +2258,118 @@ withCommon(
1840
2258
  `seeded global "${AGENT_GUIDELINES_NAME}" (${created.id}) \u2014 it now opens every launch prompt; edit it freely`
1841
2259
  );
1842
2260
  });
1843
- var journal = program.command("journal").description("An issue's stage journal: shared notes for its project + current state");
1844
- async function resolveJournal(api, ref) {
1845
- const issue = await resolveIssue(api, ref);
2261
+ var journal = program.command("journal").description("An issue's stage journal: shared notes for its project + the stage your run was launched in");
2262
+ var STATE_FLAG_HELP = "target this state's journal instead of your run's launch stage (options go BEFORE <ref>)";
2263
+ async function journalItemAt(api, scope) {
1846
2264
  const { items } = await api.listContext({
1847
2265
  kind: "prompt",
1848
- project: issue.project_id,
1849
- state: issue.state.id,
2266
+ project: scope.project_id ?? void 0,
2267
+ state: scope.workflow_state_id ?? void 0,
1850
2268
  exact: true,
1851
2269
  limit: 100
1852
2270
  });
1853
- return { issue, item: items.find((i) => i.name === JOURNAL_NAME) ?? null };
2271
+ return items.find((i) => i.name === JOURNAL_NAME) ?? null;
2272
+ }
2273
+ function stateScope(issue, state, workflow) {
2274
+ return {
2275
+ project_id: issue.project_id,
2276
+ project_name: issue.project_name,
2277
+ workflow_state_id: state.id,
2278
+ workflow_state_name: state.name,
2279
+ workflow_id: workflow.id,
2280
+ workflow_name: workflow.name,
2281
+ issue_id: null,
2282
+ issue_ref: null,
2283
+ label: `project ${issue.project_name} \xB7 state ${state.name}`
2284
+ };
2285
+ }
2286
+ async function resolveJournal(api, ref, stateRef) {
2287
+ const issue = await resolveIssue(api, ref);
2288
+ if (stateRef !== void 0) {
2289
+ const { workflow, state } = await resolveStateFlag(api, stateRef);
2290
+ const scope = stateScope(issue, state, workflow);
2291
+ return { issue, scope, note: null, item: await journalItemAt(api, scope) };
2292
+ }
2293
+ try {
2294
+ const { scope, note, item } = await api.getIssueJournal(issue.id);
2295
+ return { issue, scope, note, item };
2296
+ } catch (err) {
2297
+ if (!(err instanceof ApiError) || err.status !== 404) throw err;
2298
+ const scope = stateScope(issue, issue.state, issue.workflow);
2299
+ return { issue, scope, note: null, item: await journalItemAt(api, scope) };
2300
+ }
2301
+ }
2302
+ function printNote(note) {
2303
+ if (note) console.error(`note: ${note}`);
1854
2304
  }
1855
2305
  withCommon(
1856
- journal.command("show <ref>").description("Print the journal for the issue's project and current state")
2306
+ journal.command("show <ref>").description("Print the journal for the stage your run was launched in").option("--state <workflow>/<state>", STATE_FLAG_HELP)
1857
2307
  ).action(async (ref, opts) => {
1858
2308
  const api = client(opts);
1859
- const { issue, item } = await resolveJournal(api, ref);
2309
+ const { issue, scope, note, item } = await resolveJournal(api, ref, opts.state);
2310
+ printNote(note);
1860
2311
  if (!item) {
1861
2312
  die(
1862
- `no journal exists yet for project ${issue.project_name} \xB7 state ${issue.state.name}
2313
+ `no journal exists yet for ${scope.label}
1863
2314
  start one: tines journal append ${issue.project_name}/${issue.number} "- <date>: <lesson>"`
1864
2315
  );
1865
2316
  }
1866
2317
  const full = await api.getContextItem(item.id);
1867
2318
  if (opts.json) return printJson(full);
1868
- console.log(`journal for project ${issue.project_name} \xB7 state ${issue.state.name} (v${full.version})`);
2319
+ console.log(`journal for ${scope.label} (v${full.version})`);
1869
2320
  console.log("");
1870
2321
  console.log(full.body ?? "");
1871
2322
  });
1872
2323
  withCommon(
1873
- journal.command("append <ref> <markdown>").description("Append a lesson (creates the journal on first use)").passThroughOptions()
1874
- ).action(async (ref, markdown, opts, command) => {
1875
- if (helpGuard(command, markdown)) return;
1876
- const api = client(opts);
1877
- const { issue, item } = await resolveJournal(api, ref);
1878
- const scopeLabel = `project ${issue.project_name} \xB7 state ${issue.state.name}`;
1879
- if (item) {
1880
- const updated = await api.appendContextItem(item.id, { text: markdown });
1881
- if (opts.json) return printJson(updated);
1882
- return console.log(`appended to the ${scopeLabel} journal (now v${updated.version})`);
1883
- }
1884
- try {
1885
- const created = await api.createContextItem({
1886
- kind: "prompt",
1887
- name: JOURNAL_NAME,
1888
- project_id: issue.project_id,
1889
- workflow_state_id: issue.state.id,
1890
- body: markdown.trim()
1891
- });
1892
- if (opts.json) return printJson(created);
1893
- console.log(`started the ${scopeLabel} journal (${created.id})`);
1894
- } catch (err) {
1895
- if (!(err instanceof ApiError) || err.code !== "duplicate_context_name") throw err;
1896
- const { item: fresh } = await resolveJournal(api, ref);
1897
- if (!fresh) throw err;
1898
- const updated = await api.appendContextItem(fresh.id, { text: markdown });
1899
- if (opts.json) return printJson(updated);
1900
- console.log(`appended to the ${scopeLabel} journal (now v${updated.version})`);
2324
+ journal.command("append <ref> <markdown>").description("Append a lesson (creates the journal on first use)").option("--state <workflow>/<state>", STATE_FLAG_HELP).passThroughOptions()
2325
+ ).action(
2326
+ async (ref, markdown, opts, command) => {
2327
+ if (helpGuard(command, markdown)) return;
2328
+ const api = client(opts);
2329
+ const { scope, note, item } = await resolveJournal(api, ref, opts.state);
2330
+ printNote(note);
2331
+ if (item) {
2332
+ const updated = await api.appendContextItem(item.id, { text: markdown });
2333
+ if (opts.json) return printJson(updated);
2334
+ return console.log(`appended to the ${scope.label} journal (now v${updated.version})`);
2335
+ }
2336
+ try {
2337
+ const created = await api.createContextItem({
2338
+ kind: "prompt",
2339
+ name: JOURNAL_NAME,
2340
+ project_id: scope.project_id ?? void 0,
2341
+ workflow_state_id: scope.workflow_state_id ?? void 0,
2342
+ body: markdown.trim()
2343
+ });
2344
+ if (opts.json) return printJson(created);
2345
+ console.log(`started the ${scope.label} journal (${created.id})`);
2346
+ } catch (err) {
2347
+ if (!(err instanceof ApiError) || err.code !== "duplicate_context_name") throw err;
2348
+ const { item: fresh } = await resolveJournal(api, ref, opts.state);
2349
+ if (!fresh) throw err;
2350
+ const updated = await api.appendContextItem(fresh.id, { text: markdown });
2351
+ if (opts.json) return printJson(updated);
2352
+ console.log(`appended to the ${scope.label} journal (now v${updated.version})`);
2353
+ }
1901
2354
  }
1902
- });
2355
+ );
1903
2356
  withCommon(
1904
- journal.command("rewrite <ref>").description("Replace the journal body (to fix or prune entries) \u2014 version-checked").requiredOption("--body <md>", "the full new body: inline Markdown or @file").requiredOption(
2357
+ journal.command("rewrite <ref>").description("Replace the journal body (to fix or prune entries) \u2014 version-checked").option("--state <workflow>/<state>", STATE_FLAG_HELP).requiredOption("--body <md>", "the full new body: inline Markdown or @file").requiredOption(
1905
2358
  "--expect-version <n>",
1906
2359
  "the version being replaced (from the prompt or journal show)",
1907
2360
  (v) => Number.parseInt(v, 10)
1908
2361
  )
1909
2362
  ).action(async (ref, opts) => {
1910
2363
  const api = client(opts);
1911
- const { issue, item } = await resolveJournal(api, ref);
1912
- if (!item) {
1913
- die(
1914
- `no journal exists yet for project ${issue.project_name} \xB7 state ${issue.state.name}; nothing to rewrite`
1915
- );
1916
- }
2364
+ const { scope, note, item } = await resolveJournal(api, ref, opts.state);
2365
+ printNote(note);
2366
+ if (!item) die(`no journal exists yet for ${scope.label}; nothing to rewrite`);
1917
2367
  const updated = await api.updateContextItem(item.id, {
1918
2368
  body: readBodyValue(opts.body),
1919
2369
  expected_version: opts.expectVersion
1920
2370
  });
1921
2371
  if (opts.json) return printJson(updated);
1922
- console.log(
1923
- `rewrote the project ${issue.project_name} \xB7 state ${issue.state.name} journal (now v${updated.version})`
1924
- );
2372
+ console.log(`rewrote the ${scope.label} journal (now v${updated.version})`);
1925
2373
  });
1926
2374
  var schedules = program.command("schedules").description("Manage scheduled tasks (addressed as <project>/<name>)");
1927
2375
  function scheduleRef(s) {
@@ -2124,6 +2572,125 @@ withCommon(runners.command("show <name>").description("Show a runner")).action(
2124
2572
  }
2125
2573
  const harness = runner.config.harness;
2126
2574
  if (typeof harness === "string") console.log(`harness: ${harness}`);
2575
+ if (runner.type !== "local") {
2576
+ console.log(`api key: ${runner.has_api_key ? "set (write-only)" : "missing"}`);
2577
+ }
2578
+ if (runner.budget) {
2579
+ const b = runner.budget;
2580
+ const parts = [];
2581
+ if (b.max_run_cost_usd !== void 0) parts.push(`$${b.max_run_cost_usd}/run`);
2582
+ if (b.max_run_tokens !== void 0) parts.push(`${b.max_run_tokens.toLocaleString()} tok/run`);
2583
+ if (b.daily_usd !== void 0) parts.push(`$${b.daily_usd}/day`);
2584
+ if (b.daily_tokens !== void 0) parts.push(`${b.daily_tokens.toLocaleString()} tok/day`);
2585
+ if (parts.length > 0) console.log(`budget: ${parts.join(" ")}`);
2586
+ }
2587
+ printTierTable(runner);
2588
+ }
2589
+ );
2590
+ function printTierTable(runner) {
2591
+ if (!runner.tier_models && !runner.tiers) {
2592
+ console.log("tiers: don't apply to this runner (fixed configuration)");
2593
+ return;
2594
+ }
2595
+ console.log("tiers:");
2596
+ for (const tier of MODEL_TIERS) {
2597
+ const override = runner.tiers?.[tier];
2598
+ const builtin = runner.tier_models?.[tier] ?? null;
2599
+ const model = override?.model ?? builtin ?? "(unknown)";
2600
+ const source = override ? "override" : "built-in";
2601
+ const stale = isStaleTierOverride(builtin, override?.model);
2602
+ const marks = [
2603
+ tier === runner.default_tier ? "default" : null,
2604
+ override?.effort ? `effort ${override.effort}` : null,
2605
+ stale ? `stale \u2014 built-in is now ${builtin}` : null
2606
+ ].filter(Boolean);
2607
+ console.log(` ${tier}: ${model} [${source}]${marks.length > 0 ? ` (${marks.join(", ")})` : ""}`);
2608
+ }
2609
+ }
2610
+ withCommon(
2611
+ runners.command("tiers <name>").description("Show or edit a runner's tier\u2192model mapping").option("--default <tier>", "set the default tier (used by targets without an explicit tier)").option("--set <tier=model...>", "override a tier with an exact model id (repeatable)").option("--unset <tier...>", "drop an override, falling back to the built-in (repeatable)")
2612
+ ).action(
2613
+ async (ref, opts) => {
2614
+ const api = client(opts);
2615
+ const runner = await resolveRunner(api, ref);
2616
+ const patch = {};
2617
+ if (opts.default !== void 0) {
2618
+ if (!MODEL_TIERS.includes(opts.default)) {
2619
+ die(`unknown tier "${opts.default}" (tiers: ${MODEL_TIERS.join(", ")})`);
2620
+ }
2621
+ patch.default_tier = opts.default;
2622
+ }
2623
+ if (opts.set?.length || opts.unset?.length) {
2624
+ const tiers = {
2625
+ ...runner.tiers ?? {}
2626
+ };
2627
+ for (const entry of opts.set ?? []) {
2628
+ const eq = entry.indexOf("=");
2629
+ if (eq === -1) die(`--set takes <tier>=<model-id>, got "${entry}"`);
2630
+ const tier = entry.slice(0, eq);
2631
+ const model = entry.slice(eq + 1);
2632
+ if (!MODEL_TIERS.includes(tier)) {
2633
+ die(`unknown tier "${tier}" (tiers: ${MODEL_TIERS.join(", ")})`);
2634
+ }
2635
+ if (!model) die(`--set ${tier}= needs a model id`);
2636
+ tiers[tier] = { ...tiers[tier], model };
2637
+ }
2638
+ for (const tier of opts.unset ?? []) {
2639
+ if (!MODEL_TIERS.includes(tier)) {
2640
+ die(`unknown tier "${tier}" (tiers: ${MODEL_TIERS.join(", ")})`);
2641
+ }
2642
+ delete tiers[tier];
2643
+ }
2644
+ patch.tiers = Object.keys(tiers).length > 0 ? tiers : null;
2645
+ }
2646
+ const updated = Object.keys(patch).length > 0 ? await api.updateRunner(runner.id, patch) : runner;
2647
+ if (opts.json) return printJson(updated);
2648
+ console.log(`${updated.name} (${updated.type}) default tier: ${updated.default_tier}`);
2649
+ printTierTable(updated);
2650
+ if (Object.keys(patch).length > 0) {
2651
+ console.log("changes apply at the next launch; running work is untouched.");
2652
+ }
2653
+ }
2654
+ );
2655
+ withCommon(
2656
+ runners.command("budget <name>").description("Set or clear a runner's money limits (per-run caps enforce now; daily limits arrive with the budgets milestone)").option("--max-run-usd <n>", "hard per-run cost cap (platform-enforced on Claude runners)").option("--max-run-tokens <n>", "hard per-run token cap (input + output)").option("--daily-usd <n>", "daily USD limit (stored now, enforced by the budgets milestone)").option("--daily-tokens <n>", "daily token limit (stored now, enforced by the budgets milestone)").option("--clear", "remove all limits")
2657
+ ).action(
2658
+ async (ref, opts) => {
2659
+ const api = client(opts);
2660
+ const runner = await resolveRunner(api, ref);
2661
+ const flags = [opts.maxRunUsd, opts.maxRunTokens, opts.dailyUsd, opts.dailyTokens].some(
2662
+ (v) => v !== void 0
2663
+ );
2664
+ if (opts.clear && flags) die("--clear cannot be combined with limit flags");
2665
+ let updated = runner;
2666
+ if (opts.clear) {
2667
+ updated = await api.updateRunner(runner.id, { budget: null });
2668
+ } else if (flags) {
2669
+ const num = (value, flag) => {
2670
+ const n = Number(value);
2671
+ if (!Number.isFinite(n) || n <= 0) die(`${flag} must be a positive number, got "${value}"`);
2672
+ return n;
2673
+ };
2674
+ updated = await api.updateRunner(runner.id, {
2675
+ budget: {
2676
+ ...runner.budget ?? {},
2677
+ ...opts.maxRunUsd !== void 0 ? { max_run_cost_usd: num(opts.maxRunUsd, "--max-run-usd") } : {},
2678
+ ...opts.maxRunTokens !== void 0 ? { max_run_tokens: num(opts.maxRunTokens, "--max-run-tokens") } : {},
2679
+ ...opts.dailyUsd !== void 0 ? { daily_usd: num(opts.dailyUsd, "--daily-usd") } : {},
2680
+ ...opts.dailyTokens !== void 0 ? { daily_tokens: num(opts.dailyTokens, "--daily-tokens") } : {}
2681
+ }
2682
+ });
2683
+ }
2684
+ if (opts.json) return printJson(updated);
2685
+ const b = updated.budget;
2686
+ if (!b) return console.log(`no limits on "${updated.name}"`);
2687
+ console.log(`limits on "${updated.name}":`);
2688
+ if (b.max_run_cost_usd !== void 0) console.log(` $${b.max_run_cost_usd} per run`);
2689
+ if (b.max_run_tokens !== void 0) console.log(` ${b.max_run_tokens.toLocaleString()} tokens per run`);
2690
+ if (b.daily_usd !== void 0) console.log(` $${b.daily_usd} per day (enforced by the budgets milestone)`);
2691
+ if (b.daily_tokens !== void 0) {
2692
+ console.log(` ${b.daily_tokens.toLocaleString()} tokens per day (enforced by the budgets milestone)`);
2693
+ }
2127
2694
  }
2128
2695
  );
2129
2696
  withCommon(
@@ -2204,6 +2771,14 @@ withCommon(
2204
2771
  }
2205
2772
  );
2206
2773
  var runsCmd = program.command("runs").description("Agent runs: attempts at issues by runners");
2774
+ function runCostLabel(run) {
2775
+ const usage = run.usage;
2776
+ if (!usage) return "\u2014";
2777
+ if (usage.cost_usd !== void 0) return `$${usage.cost_usd.toFixed(2)}`;
2778
+ if (usage.cost_source === "none") return "unreported";
2779
+ const tokens = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2780
+ return tokens > 0 ? `${tokens.toLocaleString()} tok` : "\u2014";
2781
+ }
2207
2782
  function runRow(run) {
2208
2783
  return [
2209
2784
  run.id,
@@ -2212,6 +2787,7 @@ function runRow(run) {
2212
2787
  `${run.tier}${run.model ? ` (${run.model})` : ""}`,
2213
2788
  run.status,
2214
2789
  runDurationLabel(run),
2790
+ runCostLabel(run),
2215
2791
  timestamp(run.created_at)
2216
2792
  ];
2217
2793
  }
@@ -2230,7 +2806,7 @@ withList(
2230
2806
  });
2231
2807
  printList(res, opts, (items) => {
2232
2808
  if (items.length === 0) return console.log(opts.active ? "no active runs" : "no runs");
2233
- table([["ID", "ISSUE", "RUNNER", "TIER", "STATUS", "DURATION", "CREATED"], ...items.map(runRow)]);
2809
+ table([["ID", "ISSUE", "RUNNER", "TIER", "STATUS", "DURATION", "COST", "CREATED"], ...items.map(runRow)]);
2234
2810
  });
2235
2811
  });
2236
2812
  withCommon(
@@ -2248,6 +2824,16 @@ withCommon(
2248
2824
  console.log(
2249
2825
  `created: ${timestamp(run.created_at)} started: ${run.started_at ? timestamp(run.started_at) : "\u2014"} ended: ${run.ended_at ? timestamp(run.ended_at) : "\u2014"} duration: ${runDurationLabel(run)}`
2250
2826
  );
2827
+ if (run.usage) {
2828
+ const u = run.usage;
2829
+ const parts = [];
2830
+ if (u.input_tokens !== void 0 || u.output_tokens !== void 0) {
2831
+ parts.push(`${(u.input_tokens ?? 0).toLocaleString()} in / ${(u.output_tokens ?? 0).toLocaleString()} out tokens`);
2832
+ }
2833
+ if (u.cost_usd !== void 0) parts.push(`$${u.cost_usd.toFixed(2)}`);
2834
+ if (u.cost_source) parts.push(`(${u.cost_source === "provider" ? "provider-reported" : u.cost_source})`);
2835
+ if (parts.length > 0) console.log(`usage: ${parts.join(" ")}`);
2836
+ }
2251
2837
  if (run.provider_session_id) console.log(`provider session: ${run.provider_session_id}`);
2252
2838
  if (run.provider_url) console.log(`provider console: ${run.provider_url}`);
2253
2839
  if (run.error) console.log(`error: ${run.error}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.1",
3
+ "version": "0.0.76",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",