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.
- package/dist/index.js +641 -55
- 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 === "
|
|
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(
|
|
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 +
|
|
1844
|
-
|
|
1845
|
-
|
|
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:
|
|
1849
|
-
state:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
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 {
|
|
1912
|
-
|
|
1913
|
-
|
|
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}`);
|