tines 0.0.92 → 0.0.93
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/README.md +56 -0
- package/dist/index.js +789 -452
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3434,7 +3434,8 @@ function repoDirFromUrl(url) {
|
|
|
3434
3434
|
const stripped = url.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
3435
3435
|
const lastSlash = Math.max(stripped.lastIndexOf("/"), stripped.lastIndexOf(":"));
|
|
3436
3436
|
const base = stripped.slice(lastSlash + 1).replace(/\.git$/, "");
|
|
3437
|
-
if (!base || base === "." || base === ".." || base.includes("\\") || base.includes("="))
|
|
3437
|
+
if (!base || base === "." || base === ".." || base.includes("\\") || base.includes("="))
|
|
3438
|
+
return "repo";
|
|
3438
3439
|
return base;
|
|
3439
3440
|
}
|
|
3440
3441
|
var ARTIFACT_FILE_MAX_BYTES = 25 * 1024 * 1024;
|
|
@@ -3468,7 +3469,13 @@ var RUN_LOG_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
|
3468
3469
|
var RUN_LOG_RAW_MAX_BYTES = 64 * 1024 * 1024;
|
|
3469
3470
|
var MODEL_PREDECESSORS = {
|
|
3470
3471
|
"claude-fable-5": ["claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
|
|
3471
|
-
"claude-opus-5": [
|
|
3472
|
+
"claude-opus-5": [
|
|
3473
|
+
"claude-opus-4-8",
|
|
3474
|
+
"claude-opus-4-7",
|
|
3475
|
+
"claude-opus-4-6",
|
|
3476
|
+
"claude-opus-4-5",
|
|
3477
|
+
"claude-opus-4-1"
|
|
3478
|
+
],
|
|
3472
3479
|
"claude-sonnet-5": ["claude-sonnet-4-6", "claude-sonnet-4-5", "claude-3-7-sonnet-latest"],
|
|
3473
3480
|
"claude-haiku-4-5": ["claude-3-5-haiku-latest"],
|
|
3474
3481
|
"gemini-2.5-pro": ["gemini-1.5-pro"],
|
|
@@ -3509,7 +3516,9 @@ function utilizationLabel(quota, activeRuns, stateName = (id) => id) {
|
|
|
3509
3516
|
if (!counts.has(stateId)) counts.set(stateId, { name: stateName(stateId), n: 0 });
|
|
3510
3517
|
}
|
|
3511
3518
|
if (counts.size === 0) return `no active runs (roster default ${quota.default_limit} per state)`;
|
|
3512
|
-
return [...counts.entries()].map(
|
|
3519
|
+
return [...counts.entries()].map(
|
|
3520
|
+
([stateId, { name: name2, n }]) => `${name2} ${n}/${quota.overrides[stateId] ?? quota.default_limit}`
|
|
3521
|
+
).join(" \xB7 ");
|
|
3513
3522
|
}
|
|
3514
3523
|
var LIBRARY_MAX_BYTES = 5 * 1024 * 1024;
|
|
3515
3524
|
|
|
@@ -3610,9 +3619,15 @@ var DESCRIBERS = {
|
|
|
3610
3619
|
name(p.runner_name),
|
|
3611
3620
|
text(`fail to launch (${str(p.consecutive_failures)} consecutive): ${str(p.error)}`)
|
|
3612
3621
|
],
|
|
3613
|
-
"routing_rule.created": (ev, p) => [
|
|
3614
|
-
|
|
3615
|
-
|
|
3622
|
+
"routing_rule.created": (ev, p) => [
|
|
3623
|
+
text(`${action(ev.type)} the ${str(p.scope_label)} routing rule`)
|
|
3624
|
+
],
|
|
3625
|
+
"routing_rule.updated": (ev, p) => [
|
|
3626
|
+
text(`${action(ev.type)} the ${str(p.scope_label)} routing rule`)
|
|
3627
|
+
],
|
|
3628
|
+
"routing_rule.deleted": (ev, p) => [
|
|
3629
|
+
text(`${action(ev.type)} the ${str(p.scope_label)} routing rule`)
|
|
3630
|
+
],
|
|
3616
3631
|
"settings.updated": (_ev, p) => [
|
|
3617
3632
|
text(`updated supervisor settings (${joinChanged(p.changed, ", ") || "no changes"})`)
|
|
3618
3633
|
],
|
|
@@ -3720,7 +3735,10 @@ function createApiClient(options) {
|
|
|
3720
3735
|
async function raw(method, path2, opts = {}) {
|
|
3721
3736
|
const headers = { ...opts.headers ?? {} };
|
|
3722
3737
|
if (options.apiKey) headers.authorization = `Bearer ${options.apiKey}`;
|
|
3723
|
-
const body = opts.body instanceof Uint8Array ? opts.body.buffer.slice(
|
|
3738
|
+
const body = opts.body instanceof Uint8Array ? opts.body.buffer.slice(
|
|
3739
|
+
opts.body.byteOffset,
|
|
3740
|
+
opts.body.byteOffset + opts.body.byteLength
|
|
3741
|
+
) : opts.body;
|
|
3724
3742
|
const res = await fetchFn(`${base}${path2}`, { method, headers, body });
|
|
3725
3743
|
if (!res.ok) {
|
|
3726
3744
|
let parsed = null;
|
|
@@ -3816,7 +3834,11 @@ function createApiClient(options) {
|
|
|
3816
3834
|
uploadArtifactFolder: async (issueId, name2, files) => {
|
|
3817
3835
|
const form = new FormData();
|
|
3818
3836
|
for (const file of files) {
|
|
3819
|
-
form.append(
|
|
3837
|
+
form.append(
|
|
3838
|
+
"file",
|
|
3839
|
+
new Blob([file.bytes], { type: file.contentType }),
|
|
3840
|
+
file.path
|
|
3841
|
+
);
|
|
3820
3842
|
}
|
|
3821
3843
|
const res = await raw("PUT", artifactPath(issueId, name2, "/folder"), { body: form });
|
|
3822
3844
|
return await res.json();
|
|
@@ -3871,7 +3893,10 @@ function createApiClient(options) {
|
|
|
3871
3893
|
/** Daemon-only: uploads the raw harness stream for a settled run. */
|
|
3872
3894
|
putRunLogRaw: (id, body) => raw("PUT", `/api/v1/runs/${id}/log/raw`, {
|
|
3873
3895
|
body,
|
|
3874
|
-
headers: {
|
|
3896
|
+
headers: {
|
|
3897
|
+
"content-type": "application/x-ndjson",
|
|
3898
|
+
"content-length": String(body.byteLength)
|
|
3899
|
+
}
|
|
3875
3900
|
}),
|
|
3876
3901
|
cancelRun: (id) => request("POST", `/api/v1/runs/${id}/cancel`),
|
|
3877
3902
|
// Routing rules (one per exact scope; responses carry shadow hints)
|
|
@@ -3887,9 +3912,7 @@ function createApiClient(options) {
|
|
|
3887
3912
|
createApiKey: (body) => request("POST", "/api/v1/api-keys", body),
|
|
3888
3913
|
revokeApiKey: (id) => request("DELETE", `/api/v1/api-keys/${id}`),
|
|
3889
3914
|
// Library export / import (workflows + context; no tracker data, no secrets)
|
|
3890
|
-
exportLibrary: (opts = {}) => get(
|
|
3891
|
-
`/api/v1/export${opts.journals === false ? "?journals=false" : ""}`
|
|
3892
|
-
),
|
|
3915
|
+
exportLibrary: (opts = {}) => get(`/api/v1/export${opts.journals === false ? "?journals=false" : ""}`),
|
|
3893
3916
|
/** Plan-then-apply; `dry_run: true` returns the preview the apply follows. */
|
|
3894
3917
|
importLibrary: (body) => request("POST", "/api/v1/import", body)
|
|
3895
3918
|
};
|
|
@@ -4050,7 +4073,9 @@ function runnerStatusLabel(runner) {
|
|
|
4050
4073
|
}
|
|
4051
4074
|
function ruleTargetsLabel(rule) {
|
|
4052
4075
|
if (rule.targets.length === 0) return "(no targets)";
|
|
4053
|
-
return rule.targets.map(
|
|
4076
|
+
return rule.targets.map(
|
|
4077
|
+
(t) => `${t.runner_name}${t.tier ? `:${t.tier}` : ""}${t.runner_status === "paused" ? " (paused)" : ""}`
|
|
4078
|
+
).join(" \u2192 ");
|
|
4054
4079
|
}
|
|
4055
4080
|
function quotaLabel(quota, stateName) {
|
|
4056
4081
|
if (quota.type === "global_cap") return `global cap: at most ${quota.limit} concurrent runs`;
|
|
@@ -4108,7 +4133,9 @@ function keptWorkspaceRow(kept, sizeBytes, now = Date.now()) {
|
|
|
4108
4133
|
function formatTable(rows) {
|
|
4109
4134
|
if (rows.length === 0) return "";
|
|
4110
4135
|
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
|
|
4111
|
-
return rows.map(
|
|
4136
|
+
return rows.map(
|
|
4137
|
+
(row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd()
|
|
4138
|
+
).join("\n");
|
|
4112
4139
|
}
|
|
4113
4140
|
|
|
4114
4141
|
// src/refs.ts
|
|
@@ -4124,7 +4151,9 @@ function parseJsonObject(raw, source) {
|
|
|
4124
4151
|
try {
|
|
4125
4152
|
value = JSON.parse(raw);
|
|
4126
4153
|
} catch (err) {
|
|
4127
|
-
throw new CliError(
|
|
4154
|
+
throw new CliError(
|
|
4155
|
+
`invalid JSON from ${source}: ${err instanceof Error ? err.message : String(err)}`
|
|
4156
|
+
);
|
|
4128
4157
|
}
|
|
4129
4158
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4130
4159
|
throw new CliError(`expected a JSON object from ${source}`);
|
|
@@ -4161,7 +4190,9 @@ function parseFileSpec(spec) {
|
|
|
4161
4190
|
const path2 = spec.slice(0, sep);
|
|
4162
4191
|
const source = spec.slice(sep + 1);
|
|
4163
4192
|
if (!source.startsWith("@")) {
|
|
4164
|
-
throw new CliError(
|
|
4193
|
+
throw new CliError(
|
|
4194
|
+
`skill file content always comes from a local file: --file ${path2}=@<local-file>`
|
|
4195
|
+
);
|
|
4165
4196
|
}
|
|
4166
4197
|
const file = source.slice(1);
|
|
4167
4198
|
try {
|
|
@@ -4182,13 +4213,57 @@ function parseTargetSpec(spec) {
|
|
|
4182
4213
|
return { name: name2, tier };
|
|
4183
4214
|
}
|
|
4184
4215
|
|
|
4216
|
+
// src/config.ts
|
|
4217
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "node:fs";
|
|
4218
|
+
import { homedir } from "node:os";
|
|
4219
|
+
import { dirname, join } from "node:path";
|
|
4220
|
+
function defaultConfigDir() {
|
|
4221
|
+
return process.env.TINES_CONFIG_DIR ?? join(homedir(), ".config", "tines");
|
|
4222
|
+
}
|
|
4223
|
+
function readJsonFile(path2) {
|
|
4224
|
+
if (!existsSync(path2)) return null;
|
|
4225
|
+
try {
|
|
4226
|
+
return JSON.parse(readFileSync3(path2, "utf8"));
|
|
4227
|
+
} catch {
|
|
4228
|
+
return null;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
function writeJsonFile(path2, value, { secret = false } = {}) {
|
|
4232
|
+
mkdirSync(dirname(path2), { recursive: true });
|
|
4233
|
+
writeFileSync(path2, `${JSON.stringify(value, null, 2)}
|
|
4234
|
+
`, secret ? { mode: 384 } : {});
|
|
4235
|
+
}
|
|
4236
|
+
function configPath(dir = defaultConfigDir()) {
|
|
4237
|
+
return join(dir, "config.json");
|
|
4238
|
+
}
|
|
4239
|
+
function loadCliConfig(dir = defaultConfigDir()) {
|
|
4240
|
+
const raw = readJsonFile(configPath(dir));
|
|
4241
|
+
const config = {};
|
|
4242
|
+
if (typeof raw?.url === "string" && raw.url !== "") config.url = raw.url;
|
|
4243
|
+
if (typeof raw?.api_key === "string" && raw.api_key !== "") config.api_key = raw.api_key;
|
|
4244
|
+
return config;
|
|
4245
|
+
}
|
|
4246
|
+
function saveCliConfig(dir, patch) {
|
|
4247
|
+
const next = { ...loadCliConfig(dir) };
|
|
4248
|
+
if (patch.url !== void 0) next.url = patch.url.replace(/\/+$/, "");
|
|
4249
|
+
if (patch.api_key !== void 0) next.api_key = patch.api_key;
|
|
4250
|
+
writeJsonFile(configPath(dir), next, { secret: true });
|
|
4251
|
+
return next;
|
|
4252
|
+
}
|
|
4253
|
+
function clearCliConfig(dir = defaultConfigDir()) {
|
|
4254
|
+
const path2 = configPath(dir);
|
|
4255
|
+
if (!existsSync(path2)) return false;
|
|
4256
|
+
unlinkSync(path2);
|
|
4257
|
+
return true;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4185
4260
|
// src/common.ts
|
|
4186
4261
|
var DEFAULT_URL = "https://tines.tbuckley.dev";
|
|
4187
4262
|
function withCommon(cmd) {
|
|
4188
4263
|
return cmd.option(
|
|
4189
4264
|
"-u, --url <url>",
|
|
4190
|
-
`base URL of the Tines API (or set TINES_API_URL
|
|
4191
|
-
).option("--api-key <key>", "API key (or set TINES_API_KEY)").option("--json", "output the raw JSON response");
|
|
4265
|
+
`base URL of the Tines API (or set TINES_API_URL, or run \`tines login\`; default ${DEFAULT_URL})`
|
|
4266
|
+
).option("--api-key <key>", "API key (or set TINES_API_KEY, or run `tines login`)").option("--json", "output the raw JSON response");
|
|
4192
4267
|
}
|
|
4193
4268
|
function withList(cmd) {
|
|
4194
4269
|
return withCommon(
|
|
@@ -4204,11 +4279,25 @@ function withList(cmd) {
|
|
|
4204
4279
|
)
|
|
4205
4280
|
);
|
|
4206
4281
|
}
|
|
4282
|
+
function resolveUrlSetting(opts) {
|
|
4283
|
+
if (opts.url) return { value: opts.url, source: "flag" };
|
|
4284
|
+
if (process.env.TINES_API_URL) return { value: process.env.TINES_API_URL, source: "env" };
|
|
4285
|
+
const stored = loadCliConfig().url;
|
|
4286
|
+
if (stored) return { value: stored, source: "config" };
|
|
4287
|
+
return { value: DEFAULT_URL, source: "default" };
|
|
4288
|
+
}
|
|
4289
|
+
function resolveApiKeySetting(opts) {
|
|
4290
|
+
if (opts.apiKey) return { value: opts.apiKey, source: "flag" };
|
|
4291
|
+
if (process.env.TINES_API_KEY) return { value: process.env.TINES_API_KEY, source: "env" };
|
|
4292
|
+
const stored = loadCliConfig().api_key;
|
|
4293
|
+
if (stored) return { value: stored, source: "config" };
|
|
4294
|
+
return { value: void 0, source: "default" };
|
|
4295
|
+
}
|
|
4207
4296
|
function resolveUrl(opts) {
|
|
4208
|
-
return opts.
|
|
4297
|
+
return resolveUrlSetting(opts).value;
|
|
4209
4298
|
}
|
|
4210
4299
|
function resolveApiKey(opts) {
|
|
4211
|
-
return opts.
|
|
4300
|
+
return resolveApiKeySetting(opts).value;
|
|
4212
4301
|
}
|
|
4213
4302
|
function client(opts) {
|
|
4214
4303
|
return createApiClient({ baseUrl: resolveUrl(opts), apiKey: resolveApiKey(opts) });
|
|
@@ -4327,7 +4416,8 @@ async function resolveRunner(api, ref) {
|
|
|
4327
4416
|
async function resolveScopeFlags(api, opts) {
|
|
4328
4417
|
const scope = {};
|
|
4329
4418
|
if (opts.project !== void 0) scope.project_id = (await resolveProject(api, opts.project)).id;
|
|
4330
|
-
if (opts.state !== void 0)
|
|
4419
|
+
if (opts.state !== void 0)
|
|
4420
|
+
scope.workflow_state_id = (await resolveStateFlag(api, opts.state)).state.id;
|
|
4331
4421
|
if (opts.issue !== void 0) scope.issue_id = (await resolveIssue(api, opts.issue)).id;
|
|
4332
4422
|
return scope;
|
|
4333
4423
|
}
|
|
@@ -4357,7 +4447,9 @@ files (seeded at skills/${item.name}/):`);
|
|
|
4357
4447
|
console.log(`
|
|
4358
4448
|
url: ${item.repo_url}`);
|
|
4359
4449
|
if (item.repo_branch) console.log(`branch: ${item.repo_branch}`);
|
|
4360
|
-
console.log(
|
|
4450
|
+
console.log(
|
|
4451
|
+
`dir: ${item.repo_dir ?? `${repoDirFromUrl(item.repo_url ?? "")} (derived from the URL)`}`
|
|
4452
|
+
);
|
|
4361
4453
|
}
|
|
4362
4454
|
}
|
|
4363
4455
|
var SCOPE_FLAGS_HELP = `
|
|
@@ -4370,51 +4462,65 @@ function withScopeFlags(cmd) {
|
|
|
4370
4462
|
return cmd.option("-p, --project <name>", "scope: project name or id").option("-s, --state <workflow/state>", "scope: workflow-qualified state").option("-i, --issue <ref>", "scope: issue (<project>/<number>)").addHelpText("after", SCOPE_FLAGS_HELP);
|
|
4371
4463
|
}
|
|
4372
4464
|
function register(program3) {
|
|
4373
|
-
const context = program3.command("context").description(
|
|
4465
|
+
const context = program3.command("context").description(
|
|
4466
|
+
"Manage context items (prompts, skills, repo pointers) scoped to projects, states, and issues"
|
|
4467
|
+
);
|
|
4374
4468
|
withList(
|
|
4375
4469
|
withScopeFlags(
|
|
4376
|
-
context.command("list").description(
|
|
4470
|
+
context.command("list").description(
|
|
4471
|
+
"List context items (scope filters match every item whose scope includes the element)"
|
|
4472
|
+
).option("-k, --kind <kind>", "filter by kind: prompt, skill, or repo").option("--exact", "only items whose scope sets exactly the given dimensions").option("-q, --search <text>", "search names and descriptions")
|
|
4377
4473
|
)
|
|
4378
|
-
).action(
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
[
|
|
4397
|
-
|
|
4398
|
-
i
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
withCommon(context.command("show <id>").description("Show a context item (skills include their files)")).action(
|
|
4409
|
-
async (id, opts) => {
|
|
4410
|
-
const item = await client(opts).getContextItem(id);
|
|
4411
|
-
if (opts.json) return printJson(item);
|
|
4412
|
-
printContextItem(item);
|
|
4474
|
+
).action(
|
|
4475
|
+
async (opts) => {
|
|
4476
|
+
const api = client(opts);
|
|
4477
|
+
const scope = await resolveScopeFlags(api, opts);
|
|
4478
|
+
const res = await fetchList(
|
|
4479
|
+
opts,
|
|
4480
|
+
(page) => api.listContext({
|
|
4481
|
+
kind: opts.kind,
|
|
4482
|
+
project: scope.project_id ?? void 0,
|
|
4483
|
+
state: scope.workflow_state_id ?? void 0,
|
|
4484
|
+
issue: scope.issue_id ?? void 0,
|
|
4485
|
+
q: opts.search,
|
|
4486
|
+
exact: opts.exact ? true : void 0,
|
|
4487
|
+
...page
|
|
4488
|
+
})
|
|
4489
|
+
);
|
|
4490
|
+
printList(res, opts, (items) => {
|
|
4491
|
+
if (items.length === 0) return console.log("no context items");
|
|
4492
|
+
table([
|
|
4493
|
+
["KIND", "NAME", "SCOPE", "PAYLOAD", "UPDATED", "ID"],
|
|
4494
|
+
...items.map((i) => [
|
|
4495
|
+
i.kind,
|
|
4496
|
+
i.name,
|
|
4497
|
+
i.scope.label,
|
|
4498
|
+
contextItemSummary(i),
|
|
4499
|
+
timestamp(i.updated_at),
|
|
4500
|
+
i.id
|
|
4501
|
+
])
|
|
4502
|
+
]);
|
|
4503
|
+
});
|
|
4413
4504
|
}
|
|
4414
4505
|
);
|
|
4506
|
+
withCommon(
|
|
4507
|
+
context.command("show <id>").description("Show a context item (skills include their files)")
|
|
4508
|
+
).action(async (id, opts) => {
|
|
4509
|
+
const item = await client(opts).getContextItem(id);
|
|
4510
|
+
if (opts.json) return printJson(item);
|
|
4511
|
+
printContextItem(item);
|
|
4512
|
+
});
|
|
4415
4513
|
withCommon(
|
|
4416
4514
|
withScopeFlags(
|
|
4417
|
-
context.command("create").description("Create a context item scoped to a project, state, and/or issue").requiredOption("-k, --kind <kind>", "prompt, skill, or repo").requiredOption(
|
|
4515
|
+
context.command("create").description("Create a context item scoped to a project, state, and/or issue").requiredOption("-k, --kind <kind>", "prompt, skill, or repo").requiredOption(
|
|
4516
|
+
"-n, --name <name>",
|
|
4517
|
+
"item name (slug-like for skills; the dedup/override key)"
|
|
4518
|
+
).option("-d, --description <text>", "one-liner shown in lists").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option(
|
|
4519
|
+
"--file <path>=@<local>",
|
|
4520
|
+
"skill file: workspace path = local file (repeatable)",
|
|
4521
|
+
collect,
|
|
4522
|
+
[]
|
|
4523
|
+
).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch to check out").option("--dir <dir>", "repo: checkout directory (defaults to the URL's basename)")
|
|
4418
4524
|
)
|
|
4419
4525
|
).action(
|
|
4420
4526
|
async (opts) => {
|
|
@@ -4442,7 +4548,12 @@ function register(program3) {
|
|
|
4442
4548
|
);
|
|
4443
4549
|
withCommon(
|
|
4444
4550
|
withScopeFlags(
|
|
4445
|
-
context.command("edit <id>").description("Edit a context item: payload, name, description, or scope").option("-n, --name <name>", "rename the item").option("-d, --description <text>", "set the description").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option("--file <path>=@<local>", "add or replace a skill file (repeatable)", collect, []).option("--remove-file <path>", "remove a skill file (repeatable)", collect, []).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch (empty string clears it)").option("--dir <dir>", "repo: checkout directory (empty string restores the URL default)").option(
|
|
4551
|
+
context.command("edit <id>").description("Edit a context item: payload, name, description, or scope").option("-n, --name <name>", "rename the item").option("-d, --description <text>", "set the description").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option("--file <path>=@<local>", "add or replace a skill file (repeatable)", collect, []).option("--remove-file <path>", "remove a skill file (repeatable)", collect, []).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch (empty string clears it)").option("--dir <dir>", "repo: checkout directory (empty string restores the URL default)").option(
|
|
4552
|
+
"--unset <dimension>",
|
|
4553
|
+
"drop a scope dimension: project, state, or issue (repeatable)",
|
|
4554
|
+
collect,
|
|
4555
|
+
[]
|
|
4556
|
+
).option(
|
|
4446
4557
|
"--expect-version <n>",
|
|
4447
4558
|
"fail (409) unless the item is still at this version",
|
|
4448
4559
|
(v) => Number.parseInt(v, 10)
|
|
@@ -4466,12 +4577,15 @@ function register(program3) {
|
|
|
4466
4577
|
if (opts.body !== void 0) body.body = readBodyValue(opts.body);
|
|
4467
4578
|
if (opts.file.length > 0 || opts.removeFile.length > 0) {
|
|
4468
4579
|
const current = await api.getContextItem(id);
|
|
4469
|
-
if (current.kind !== "skill")
|
|
4580
|
+
if (current.kind !== "skill")
|
|
4581
|
+
die(`--file/--remove-file only apply to skills (this is a ${current.kind})`);
|
|
4470
4582
|
if (body.expected_version === void 0) body.expected_version = current.version;
|
|
4471
4583
|
const files = new Map((current.files ?? []).map((f) => [f.path, f.content]));
|
|
4472
4584
|
for (const path2 of opts.removeFile) {
|
|
4473
4585
|
if (!files.delete(path2)) {
|
|
4474
|
-
die(
|
|
4586
|
+
die(
|
|
4587
|
+
`no file "${path2}" in skill "${current.name}" (have: ${[...files.keys()].join(", ") || "none"})`
|
|
4588
|
+
);
|
|
4475
4589
|
}
|
|
4476
4590
|
}
|
|
4477
4591
|
for (const spec of opts.file) {
|
|
@@ -4484,7 +4598,9 @@ function register(program3) {
|
|
|
4484
4598
|
if (opts.branch !== void 0) body.repo_branch = opts.branch === "" ? null : opts.branch;
|
|
4485
4599
|
if (opts.dir !== void 0) body.repo_dir = opts.dir === "" ? null : opts.dir;
|
|
4486
4600
|
if (Object.keys(body).length === 0) {
|
|
4487
|
-
die(
|
|
4601
|
+
die(
|
|
4602
|
+
"nothing to update: pass payload flags, --name/--description, scope flags, and/or --unset"
|
|
4603
|
+
);
|
|
4488
4604
|
}
|
|
4489
4605
|
const item = await api.updateContextItem(id, body);
|
|
4490
4606
|
if (opts.json) return printJson(item);
|
|
@@ -4503,7 +4619,9 @@ function register(program3) {
|
|
|
4503
4619
|
context.command("init").description('Seed the global "agent-guidelines" prompt (a no-op if it already exists)')
|
|
4504
4620
|
).action(async (opts) => {
|
|
4505
4621
|
const api = client(opts);
|
|
4506
|
-
const items = await listAll(
|
|
4622
|
+
const items = await listAll(
|
|
4623
|
+
(page) => api.listContext({ kind: "prompt", exact: true, ...page })
|
|
4624
|
+
);
|
|
4507
4625
|
const existing = items.find((i) => i.name === AGENT_GUIDELINES_NAME);
|
|
4508
4626
|
if (existing) {
|
|
4509
4627
|
if (opts.json) return printJson(existing);
|
|
@@ -4525,8 +4643,8 @@ function register(program3) {
|
|
|
4525
4643
|
}
|
|
4526
4644
|
|
|
4527
4645
|
// src/commands/issues.ts
|
|
4528
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync as
|
|
4529
|
-
import { basename, dirname, join } from "node:path";
|
|
4646
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4647
|
+
import { basename, dirname as dirname2, join as join2 } from "node:path";
|
|
4530
4648
|
|
|
4531
4649
|
// src/help-guard.ts
|
|
4532
4650
|
function helpGuard(command, markdown) {
|
|
@@ -4565,12 +4683,15 @@ function buildRecurrence(opts) {
|
|
|
4565
4683
|
if (hourly !== void 0) {
|
|
4566
4684
|
if (opts.on !== void 0) throw new CliError("an hourly recurrence does not take --on");
|
|
4567
4685
|
const every = typeof hourly === "number" ? hourly : Number.parseInt(hourly, 10);
|
|
4568
|
-
if (every < 1 || every > 23)
|
|
4686
|
+
if (every < 1 || every > 23)
|
|
4687
|
+
throw new CliError(`--every <N>h needs N between 1 and 23, got "${opts.every}"`);
|
|
4569
4688
|
let minute = 0;
|
|
4570
4689
|
if (opts.at !== void 0) {
|
|
4571
4690
|
const m = opts.at.match(/^:?(\d{1,2})$/);
|
|
4572
4691
|
if (!m || Number.parseInt(m[1], 10) > 59) {
|
|
4573
|
-
throw new CliError(
|
|
4692
|
+
throw new CliError(
|
|
4693
|
+
`with an hourly recurrence, --at is the minute past the hour (0-59 or :MM), got "${opts.at}"`
|
|
4694
|
+
);
|
|
4574
4695
|
}
|
|
4575
4696
|
minute = Number.parseInt(m[1], 10);
|
|
4576
4697
|
}
|
|
@@ -4595,7 +4716,9 @@ function buildRecurrence(opts) {
|
|
|
4595
4716
|
return { preset: { kind: "monthly", time, day_of_month: day } };
|
|
4596
4717
|
}
|
|
4597
4718
|
default:
|
|
4598
|
-
throw new CliError(
|
|
4719
|
+
throw new CliError(
|
|
4720
|
+
`--every must be hourly, <N>h, daily, weekly, or monthly, got "${opts.every}"`
|
|
4721
|
+
);
|
|
4599
4722
|
}
|
|
4600
4723
|
}
|
|
4601
4724
|
|
|
@@ -4604,7 +4727,9 @@ var systemTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
4604
4727
|
function printIssueLinks(links) {
|
|
4605
4728
|
if (links.blocked_by.length > 0) {
|
|
4606
4729
|
console.log("\nblocked by:");
|
|
4607
|
-
table(
|
|
4730
|
+
table(
|
|
4731
|
+
linkRows(links.blocked_by, (e) => e.effective_state.category === "done" ? "" : "(open)")
|
|
4732
|
+
);
|
|
4608
4733
|
}
|
|
4609
4734
|
if (links.blocks.length > 0) {
|
|
4610
4735
|
console.log("\nblocks:");
|
|
@@ -4626,7 +4751,9 @@ function printIssueDetail(issue) {
|
|
|
4626
4751
|
`state: ${issue.effective_state.name} (${issue.effective_state.category})${dup ? ` (via ${issueRef(dup)} \u2014 duplicate)` : ""} workflow: ${issue.workflow.name} updated: ${timestamp(issue.updated_at)}`
|
|
4627
4752
|
);
|
|
4628
4753
|
if (dup) {
|
|
4629
|
-
console.log(
|
|
4754
|
+
console.log(
|
|
4755
|
+
`own state: ${issue.state.name} (${issue.state.category}) \u2014 dormant while this is a duplicate`
|
|
4756
|
+
);
|
|
4630
4757
|
}
|
|
4631
4758
|
console.log(`id: ${issue.id}`);
|
|
4632
4759
|
printIssueLinks(issue.links);
|
|
@@ -4635,8 +4762,10 @@ function printIssueDetail(issue) {
|
|
|
4635
4762
|
${issue.description}`);
|
|
4636
4763
|
}
|
|
4637
4764
|
const allowed = issue.allowed_transitions.map((t) => `"${t.name}" \u2192 ${t.to_state.name}`);
|
|
4638
|
-
console.log(
|
|
4639
|
-
|
|
4765
|
+
console.log(
|
|
4766
|
+
`
|
|
4767
|
+
allowed actions: ${allowed.length ? allowed.join(", ") : "none (terminal state)"}`
|
|
4768
|
+
);
|
|
4640
4769
|
if (issue.comments.length > 0) {
|
|
4641
4770
|
console.log(`
|
|
4642
4771
|
comments (${issue.comments.length}):`);
|
|
@@ -4649,11 +4778,15 @@ function walkFolder(dir) {
|
|
|
4649
4778
|
const files = [];
|
|
4650
4779
|
const walk = (abs, rel) => {
|
|
4651
4780
|
for (const entry of readdirSync(abs, { withFileTypes: true })) {
|
|
4652
|
-
const nextAbs =
|
|
4781
|
+
const nextAbs = join2(abs, entry.name);
|
|
4653
4782
|
const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
4654
4783
|
if (entry.isDirectory()) walk(nextAbs, nextRel);
|
|
4655
4784
|
else if (entry.isFile()) {
|
|
4656
|
-
files.push({
|
|
4785
|
+
files.push({
|
|
4786
|
+
path: nextRel,
|
|
4787
|
+
contentType: sniffContentType(entry.name),
|
|
4788
|
+
bytes: readFileSync4(nextAbs)
|
|
4789
|
+
});
|
|
4657
4790
|
}
|
|
4658
4791
|
}
|
|
4659
4792
|
};
|
|
@@ -4687,7 +4820,9 @@ matched rule: ${ex.matched_rule.scope_label}`);
|
|
|
4687
4820
|
);
|
|
4688
4821
|
}
|
|
4689
4822
|
if (ex.queue_position !== null && ex.queue_position > 0) {
|
|
4690
|
-
console.log(
|
|
4823
|
+
console.log(
|
|
4824
|
+
`queue: ${ex.queue_position} eligible issue${ex.queue_position === 1 ? "" : "s"} ahead of this one`
|
|
4825
|
+
);
|
|
4691
4826
|
}
|
|
4692
4827
|
if (ex.active_run) {
|
|
4693
4828
|
console.log(
|
|
@@ -4703,7 +4838,10 @@ matched rule: ${ex.matched_rule.scope_label}`);
|
|
|
4703
4838
|
function register2(program3) {
|
|
4704
4839
|
const issues = program3.command("issues").description("Work with issues");
|
|
4705
4840
|
withList(
|
|
4706
|
-
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(
|
|
4841
|
+
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(
|
|
4842
|
+
"--ready",
|
|
4843
|
+
"only issues that are actionable now (not done, not a duplicate, no open blockers)"
|
|
4844
|
+
).option("-q, --search <text>", "search titles and descriptions")
|
|
4707
4845
|
).action(
|
|
4708
4846
|
async (opts) => {
|
|
4709
4847
|
const api = client(opts);
|
|
@@ -4739,7 +4877,19 @@ function register2(program3) {
|
|
|
4739
4877
|
}
|
|
4740
4878
|
);
|
|
4741
4879
|
withCommon(
|
|
4742
|
-
issues.command("create <project>").description("Create an issue in a project, optionally with a recurrence (a scheduled task)").requiredOption(
|
|
4880
|
+
issues.command("create <project>").description("Create an issue in a project, optionally with a recurrence (a scheduled task)").requiredOption(
|
|
4881
|
+
"-t, --title <title>",
|
|
4882
|
+
"issue title (doubles as the title template with a recurrence)"
|
|
4883
|
+
).option("-d, --description <markdown>", `issue description (Markdown) \u2014 ${BODY_VALUE_HELP}`).option(
|
|
4884
|
+
"-w, --workflow <id-or-name>",
|
|
4885
|
+
"workflow (defaults to project default, else standard)"
|
|
4886
|
+
).option("-s, --state <name>", "starting state (defaults to the workflow's initial state)").option(
|
|
4887
|
+
"--every <preset>",
|
|
4888
|
+
'repeat hourly (or every N hours: "6h"), daily, weekly, or monthly'
|
|
4889
|
+
).option(
|
|
4890
|
+
"--at <when>",
|
|
4891
|
+
"preset time of day HH:MM (default 09:00); for hourly, the minute past the hour :MM (default :00)"
|
|
4892
|
+
).option("--on <when>", "weekday (weekly) or day of month (monthly)").option("--cron <expr>", "5-field cron expression (alternative to --every/--at/--on)").option("--tz <iana>", "schedule timezone (defaults to the system timezone)").option("--if-closed", "only create a new instance when all previous instances are closed").option("--schedule-name <name>", "schedule name, unique per project (defaults to the title)")
|
|
4743
4893
|
).action(
|
|
4744
4894
|
async (projectRef, opts) => {
|
|
4745
4895
|
const description = opts.description !== void 0 ? readBodyValue(opts.description) : void 0;
|
|
@@ -4795,7 +4945,8 @@ function register2(program3) {
|
|
|
4795
4945
|
if (opts.title !== void 0) body.title = opts.title;
|
|
4796
4946
|
if (description !== void 0) body.description = description;
|
|
4797
4947
|
if (opts.state !== void 0) body.state = opts.state;
|
|
4798
|
-
if (opts.workflow !== void 0)
|
|
4948
|
+
if (opts.workflow !== void 0)
|
|
4949
|
+
body.workflow_id = (await resolveWorkflow(api, opts.workflow)).id;
|
|
4799
4950
|
if (Object.keys(body).length === 0) {
|
|
4800
4951
|
die("nothing to update: pass --title, --description, --state, and/or --workflow");
|
|
4801
4952
|
}
|
|
@@ -4837,15 +4988,17 @@ function register2(program3) {
|
|
|
4837
4988
|
});
|
|
4838
4989
|
withCommon(
|
|
4839
4990
|
issues.command("comment-edit <ref> <comment-id> <markdown>").description(`Replace the body of your own comment \u2014 Markdown body: ${BODY_VALUE_HELP}`).passThroughOptions()
|
|
4840
|
-
).action(
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4991
|
+
).action(
|
|
4992
|
+
async (ref, commentId, markdown, opts, command) => {
|
|
4993
|
+
if (helpGuard(command, markdown)) return;
|
|
4994
|
+
const body = readBodyValue(markdown);
|
|
4995
|
+
const api = client(opts);
|
|
4996
|
+
const issue = await resolveIssue(api, ref);
|
|
4997
|
+
const comment = await api.updateComment(issue.id, commentId, { body });
|
|
4998
|
+
if (opts.json) return printJson(comment);
|
|
4999
|
+
console.log(`edited comment ${comment.id} on ${issue.project_name}/#${issue.number}`);
|
|
5000
|
+
}
|
|
5001
|
+
);
|
|
4849
5002
|
withCommon(
|
|
4850
5003
|
issues.command("comment-delete <ref> <comment-id>").description("Delete your own comment (the event keeps the record of the deletion)")
|
|
4851
5004
|
).action(async (ref, commentId, opts) => {
|
|
@@ -4898,7 +5051,12 @@ function register2(program3) {
|
|
|
4898
5051
|
);
|
|
4899
5052
|
});
|
|
4900
5053
|
withCommon(
|
|
4901
|
-
issues.command("context <ref>").description(
|
|
5054
|
+
issues.command("context <ref>").description(
|
|
5055
|
+
"Print an issue's effective context (the assembled bundle for its current state)"
|
|
5056
|
+
).option(
|
|
5057
|
+
"--out <dir>",
|
|
5058
|
+
"write the bundle to a directory: prompt.md, skills/<name>/\u2026, repos.json"
|
|
5059
|
+
).option("--force", "allow --out into a non-empty directory")
|
|
4902
5060
|
).action(async (ref, opts) => {
|
|
4903
5061
|
const api = client(opts);
|
|
4904
5062
|
const issue = await resolveIssue(api, ref);
|
|
@@ -4911,13 +5069,19 @@ function register2(program3) {
|
|
|
4911
5069
|
skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
4912
5070
|
}
|
|
4913
5071
|
for (const repo of context.repos) {
|
|
4914
|
-
console.log(
|
|
5072
|
+
console.log(
|
|
5073
|
+
`repo: ${repo.name} ${repo.url}${repo.branch ? `#${repo.branch}` : ""} \u2192 ${repo.dir}/`
|
|
5074
|
+
);
|
|
4915
5075
|
}
|
|
4916
5076
|
for (const o of context.overridden) {
|
|
4917
|
-
console.log(
|
|
5077
|
+
console.log(
|
|
5078
|
+
`overridden: ${o.kind} "${o.name}" [${o.scope.label}] (overridden by ${o.overridden_by})`
|
|
5079
|
+
);
|
|
4918
5080
|
}
|
|
4919
5081
|
for (const c of context.conflicts) {
|
|
4920
|
-
console.log(
|
|
5082
|
+
console.log(
|
|
5083
|
+
`conflict: repos ${c.item_ids.join(", ")} all resolve to checkout dir "${c.dir}"`
|
|
5084
|
+
);
|
|
4921
5085
|
}
|
|
4922
5086
|
return;
|
|
4923
5087
|
}
|
|
@@ -4926,20 +5090,23 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
4926
5090
|
`refusing to write: checkout-directory conflict${context.conflicts.length === 1 ? "" : "s"} among the effective repos (${context.conflicts.map((c) => `"${c.dir}": ${c.item_ids.join(", ")}`).join("; ")}); rename or re-dir the items first`
|
|
4927
5091
|
);
|
|
4928
5092
|
}
|
|
4929
|
-
if (
|
|
5093
|
+
if (existsSync2(opts.out) && readdirSync(opts.out).length > 0 && !opts.force) {
|
|
4930
5094
|
die(`refusing to write into non-empty directory ${opts.out} (pass --force to override)`);
|
|
4931
5095
|
}
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
5096
|
+
mkdirSync2(opts.out, { recursive: true });
|
|
5097
|
+
writeFileSync2(
|
|
5098
|
+
join2(opts.out, "prompt.md"),
|
|
5099
|
+
context.prompt.text ? `${context.prompt.text}
|
|
5100
|
+
` : ""
|
|
5101
|
+
);
|
|
4935
5102
|
for (const skill of context.skills) {
|
|
4936
5103
|
for (const file of skill.files) {
|
|
4937
|
-
const target =
|
|
4938
|
-
|
|
4939
|
-
|
|
5104
|
+
const target = join2(opts.out, "skills", skill.name, file.path);
|
|
5105
|
+
mkdirSync2(dirname2(target), { recursive: true });
|
|
5106
|
+
writeFileSync2(target, file.content);
|
|
4940
5107
|
}
|
|
4941
5108
|
}
|
|
4942
|
-
|
|
5109
|
+
writeFileSync2(join2(opts.out, "repos.json"), `${JSON.stringify(context.repos, null, 2)}
|
|
4943
5110
|
`);
|
|
4944
5111
|
console.log(
|
|
4945
5112
|
`wrote ${opts.out}/prompt.md, ${context.skills.length} skill${context.skills.length === 1 ? "" : "s"}, repos.json (${context.repos.length} repo${context.repos.length === 1 ? "" : "s"})`
|
|
@@ -4967,27 +5134,29 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
4967
5134
|
if (opts.json) return printJson(prompt);
|
|
4968
5135
|
console.log(prompt.text);
|
|
4969
5136
|
});
|
|
4970
|
-
const artifactsCmd = issues.command("artifacts").description(
|
|
4971
|
-
|
|
4972
|
-
async (ref, opts) => {
|
|
4973
|
-
const api = client(opts);
|
|
4974
|
-
const issue = await resolveIssue(api, ref);
|
|
4975
|
-
const res = await api.listArtifacts(issue.id);
|
|
4976
|
-
if (opts.json) return printJson(res);
|
|
4977
|
-
if (res.items.length === 0) return console.log("no artifacts attached");
|
|
4978
|
-
table([
|
|
4979
|
-
["NAME", "TYPE", "VERSION", "FRESH", "SUMMARY", "ATTACHED"],
|
|
4980
|
-
...res.items.map((a) => [
|
|
4981
|
-
a.name,
|
|
4982
|
-
a.artifact_type,
|
|
4983
|
-
`v${a.current_version.version}`,
|
|
4984
|
-
a.fresh ? "yes" : "no",
|
|
4985
|
-
artifactSummary(a),
|
|
4986
|
-
timestamp(a.current_version.created_at)
|
|
4987
|
-
])
|
|
4988
|
-
]);
|
|
4989
|
-
}
|
|
5137
|
+
const artifactsCmd = issues.command("artifacts").description(
|
|
5138
|
+
"Typed, versioned attachments on an issue \u2014 the work products transition requirements gate on"
|
|
4990
5139
|
);
|
|
5140
|
+
withCommon(
|
|
5141
|
+
artifactsCmd.command("list <ref>").description("List the artifacts attached to an issue")
|
|
5142
|
+
).action(async (ref, opts) => {
|
|
5143
|
+
const api = client(opts);
|
|
5144
|
+
const issue = await resolveIssue(api, ref);
|
|
5145
|
+
const res = await api.listArtifacts(issue.id);
|
|
5146
|
+
if (opts.json) return printJson(res);
|
|
5147
|
+
if (res.items.length === 0) return console.log("no artifacts attached");
|
|
5148
|
+
table([
|
|
5149
|
+
["NAME", "TYPE", "VERSION", "FRESH", "SUMMARY", "ATTACHED"],
|
|
5150
|
+
...res.items.map((a) => [
|
|
5151
|
+
a.name,
|
|
5152
|
+
a.artifact_type,
|
|
5153
|
+
`v${a.current_version.version}`,
|
|
5154
|
+
a.fresh ? "yes" : "no",
|
|
5155
|
+
artifactSummary(a),
|
|
5156
|
+
timestamp(a.current_version.created_at)
|
|
5157
|
+
])
|
|
5158
|
+
]);
|
|
5159
|
+
});
|
|
4991
5160
|
withCommon(
|
|
4992
5161
|
artifactsCmd.command("show <ref> <name>").description("Show an artifact with its full version history")
|
|
4993
5162
|
).action(async (ref, name2, opts) => {
|
|
@@ -4995,7 +5164,9 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
4995
5164
|
const issue = await resolveIssue(api, ref);
|
|
4996
5165
|
const artifact = await api.getArtifact(issue.id, name2);
|
|
4997
5166
|
if (opts.json) return printJson(artifact);
|
|
4998
|
-
console.log(
|
|
5167
|
+
console.log(
|
|
5168
|
+
`${artifact.artifact_type} artifact "${artifact.name}" on ${issue.project_name}/${issue.number}`
|
|
5169
|
+
);
|
|
4999
5170
|
if (artifact.description) console.log(artifact.description);
|
|
5000
5171
|
console.log(
|
|
5001
5172
|
`current: v${artifact.current_version.version} (${artifact.fresh ? "fresh" : "attached before the current state \u2014 reaffirm or attach a new version to satisfy gates"})`
|
|
@@ -5018,14 +5189,18 @@ files (v${artifact.current_version.version}):`);
|
|
|
5018
5189
|
}
|
|
5019
5190
|
});
|
|
5020
5191
|
withCommon(
|
|
5021
|
-
artifactsCmd.command("attach <ref> <name>").description(
|
|
5192
|
+
artifactsCmd.command("attach <ref> <name>").description(
|
|
5193
|
+
"Attach content to a named artifact slot (creates it, or appends the next version)"
|
|
5194
|
+
).option("-f, --file <path>", "upload a file (MIME sniffed from the extension)").option(
|
|
5022
5195
|
"--folder <dir>",
|
|
5023
5196
|
"snapshot a directory tree as one version (collect locally, attach once; MIME per file sniffed)"
|
|
5024
5197
|
).option("-t, --text <md|@file>", "inline text document: inline Markdown or @file").option("--link <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 --link)").option("-d, --description <text>", "artifact description, shown in lists and launch prompts")
|
|
5025
5198
|
).action(
|
|
5026
5199
|
async (ref, name2, opts) => {
|
|
5027
5200
|
const api = client(opts);
|
|
5028
|
-
const sources = [opts.file, opts.folder, opts.text, opts.link, opts.pr].filter(
|
|
5201
|
+
const sources = [opts.file, opts.folder, opts.text, opts.link, opts.pr].filter(
|
|
5202
|
+
(v) => v !== void 0
|
|
5203
|
+
);
|
|
5029
5204
|
if (sources.length !== 1) {
|
|
5030
5205
|
die(
|
|
5031
5206
|
"pass exactly one content source: --file <path>, --folder <dir>, --text <md|@file>, --link <url>, or --pr <spec> (a link goes in --link; --url is the API base URL)"
|
|
@@ -5034,7 +5209,7 @@ files (v${artifact.current_version.version}):`);
|
|
|
5034
5209
|
const issue = await resolveIssue(api, ref);
|
|
5035
5210
|
let artifact;
|
|
5036
5211
|
if (opts.folder !== void 0) {
|
|
5037
|
-
if (!
|
|
5212
|
+
if (!existsSync2(opts.folder) || !statSync(opts.folder).isDirectory()) {
|
|
5038
5213
|
die(`--folder needs a directory, got "${opts.folder}"`);
|
|
5039
5214
|
}
|
|
5040
5215
|
const files = walkFolder(opts.folder);
|
|
@@ -5046,7 +5221,7 @@ files (v${artifact.current_version.version}):`);
|
|
|
5046
5221
|
} else if (opts.file !== void 0) {
|
|
5047
5222
|
let bytes;
|
|
5048
5223
|
try {
|
|
5049
|
-
bytes =
|
|
5224
|
+
bytes = readFileSync4(opts.file);
|
|
5050
5225
|
} catch (err) {
|
|
5051
5226
|
die(`cannot read ${opts.file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5052
5227
|
}
|
|
@@ -5091,7 +5266,9 @@ files (v${artifact.current_version.version}):`);
|
|
|
5091
5266
|
}
|
|
5092
5267
|
);
|
|
5093
5268
|
withCommon(
|
|
5094
|
-
artifactsCmd.command("reaffirm <ref> <name>").description(
|
|
5269
|
+
artifactsCmd.command("reaffirm <ref> <name>").description(
|
|
5270
|
+
"Bless the current content as fresh (appends a version reusing the same payload)"
|
|
5271
|
+
)
|
|
5095
5272
|
).action(async (ref, name2, opts) => {
|
|
5096
5273
|
const api = client(opts);
|
|
5097
5274
|
const issue = await resolveIssue(api, ref);
|
|
@@ -5102,7 +5279,14 @@ files (v${artifact.current_version.version}):`);
|
|
|
5102
5279
|
);
|
|
5103
5280
|
});
|
|
5104
5281
|
withCommon(
|
|
5105
|
-
artifactsCmd.command("get <ref> <name>").description("Fetch content (current version by default); a link/pr prints its URL").option(
|
|
5282
|
+
artifactsCmd.command("get <ref> <name>").description("Fetch content (current version by default); a link/pr prints its URL").option(
|
|
5283
|
+
"--version <n>",
|
|
5284
|
+
"fetch a specific version from the history",
|
|
5285
|
+
(v) => Number.parseInt(v, 10)
|
|
5286
|
+
).option(
|
|
5287
|
+
"--out <path>",
|
|
5288
|
+
"write to this file, or into this directory (keeps the stored filename)"
|
|
5289
|
+
)
|
|
5106
5290
|
).action(
|
|
5107
5291
|
async (ref, name2, opts) => {
|
|
5108
5292
|
const api = client(opts);
|
|
@@ -5123,7 +5307,7 @@ files (v${artifact.current_version.version}):`);
|
|
|
5123
5307
|
if (opts.out === void 0) {
|
|
5124
5308
|
die(`artifact "${name2}" is a folder \u2014 pass --out <dir> to write its tree`);
|
|
5125
5309
|
}
|
|
5126
|
-
if (
|
|
5310
|
+
if (existsSync2(opts.out) && !statSync(opts.out).isDirectory()) {
|
|
5127
5311
|
die(`--out for a folder must be a directory, and "${opts.out}" is a file`);
|
|
5128
5312
|
}
|
|
5129
5313
|
const files = version.files ?? [];
|
|
@@ -5133,9 +5317,9 @@ files (v${artifact.current_version.version}):`);
|
|
|
5133
5317
|
version: opts.version,
|
|
5134
5318
|
path: file.path
|
|
5135
5319
|
});
|
|
5136
|
-
const target2 =
|
|
5137
|
-
|
|
5138
|
-
|
|
5320
|
+
const target2 = join2(opts.out, file.path);
|
|
5321
|
+
mkdirSync2(dirname2(target2), { recursive: true });
|
|
5322
|
+
writeFileSync2(target2, Buffer.from(content2.bytes));
|
|
5139
5323
|
total += content2.bytes.byteLength;
|
|
5140
5324
|
}
|
|
5141
5325
|
return console.log(
|
|
@@ -5146,10 +5330,10 @@ files (v${artifact.current_version.version}):`);
|
|
|
5146
5330
|
const bytes = Buffer.from(content.bytes);
|
|
5147
5331
|
if (opts.out !== void 0) {
|
|
5148
5332
|
let target2 = opts.out;
|
|
5149
|
-
if (
|
|
5150
|
-
target2 =
|
|
5333
|
+
if (existsSync2(target2) && statSync(target2).isDirectory()) {
|
|
5334
|
+
target2 = join2(target2, version.filename ?? name2);
|
|
5151
5335
|
}
|
|
5152
|
-
|
|
5336
|
+
writeFileSync2(target2, bytes);
|
|
5153
5337
|
return console.log(`wrote ${target2} (${bytes.byteLength} bytes, ${content.content_type})`);
|
|
5154
5338
|
}
|
|
5155
5339
|
if ((content.content_type ?? "").startsWith("text/")) {
|
|
@@ -5157,12 +5341,14 @@ files (v${artifact.current_version.version}):`);
|
|
|
5157
5341
|
return;
|
|
5158
5342
|
}
|
|
5159
5343
|
const target = version.filename ?? name2;
|
|
5160
|
-
|
|
5344
|
+
writeFileSync2(target, bytes);
|
|
5161
5345
|
console.log(`wrote ${target} (${bytes.byteLength} bytes, ${content.content_type})`);
|
|
5162
5346
|
}
|
|
5163
5347
|
);
|
|
5164
5348
|
withCommon(
|
|
5165
|
-
artifactsCmd.command("delete <ref> <name>").description(
|
|
5349
|
+
artifactsCmd.command("delete <ref> <name>").description(
|
|
5350
|
+
"Delete an artifact \u2014 every version and its stored files (history is not recoverable)"
|
|
5351
|
+
)
|
|
5166
5352
|
).action(async (ref, name2, opts) => {
|
|
5167
5353
|
const api = client(opts);
|
|
5168
5354
|
const issue = await resolveIssue(api, ref);
|
|
@@ -5173,30 +5359,38 @@ files (v${artifact.current_version.version}):`);
|
|
|
5173
5359
|
);
|
|
5174
5360
|
});
|
|
5175
5361
|
withCommon(
|
|
5176
|
-
issues.command("assign <ref> [runner]").description(
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
const
|
|
5183
|
-
if (opts.
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5362
|
+
issues.command("assign <ref> [runner]").description(
|
|
5363
|
+
"Pin an issue to a runner (<runner>[:tier]) \u2014 replaces routing rules for it; --clear unpins"
|
|
5364
|
+
).option("--clear", "remove the pin")
|
|
5365
|
+
).action(
|
|
5366
|
+
async (ref, runnerSpec, opts) => {
|
|
5367
|
+
const api = client(opts);
|
|
5368
|
+
const issue = await resolveIssue(api, ref);
|
|
5369
|
+
if (opts.clear) {
|
|
5370
|
+
if (runnerSpec !== void 0) die("--clear does not take a runner");
|
|
5371
|
+
const updated2 = await api.updateIssue(issue.id, { pinned_runner_id: null });
|
|
5372
|
+
if (opts.json) return printJson(updated2);
|
|
5373
|
+
return console.log(
|
|
5374
|
+
`unpinned ${updated2.project_name}/#${updated2.number} \u2014 routing rules apply again`
|
|
5375
|
+
);
|
|
5376
|
+
}
|
|
5377
|
+
if (runnerSpec === void 0) die("pass <runner>[:tier] to pin, or --clear to unpin");
|
|
5378
|
+
const { name: name2, tier } = parseTargetSpec(runnerSpec);
|
|
5379
|
+
const runner = await resolveRunner(api, name2);
|
|
5380
|
+
const updated = await api.updateIssue(issue.id, {
|
|
5381
|
+
pinned_runner_id: runner.id,
|
|
5382
|
+
pinned_tier: tier ?? null
|
|
5383
|
+
});
|
|
5384
|
+
if (opts.json) return printJson(updated);
|
|
5385
|
+
console.log(
|
|
5386
|
+
`pinned ${updated.project_name}/#${updated.number} to ${runner.name}${tier ? ` (tier ${tier})` : ""} \u2014 only this runner will take it`
|
|
5387
|
+
);
|
|
5388
|
+
}
|
|
5389
|
+
);
|
|
5198
5390
|
withCommon(
|
|
5199
|
-
issues.command("dispatch <ref>").description(
|
|
5391
|
+
issues.command("dispatch <ref>").description(
|
|
5392
|
+
"Explain why an issue is (not) dispatching: eligibility, routing, per-runner verdicts"
|
|
5393
|
+
)
|
|
5200
5394
|
).action(async (ref, opts) => {
|
|
5201
5395
|
const api = client(opts);
|
|
5202
5396
|
const issue = await resolveIssue(api, ref);
|
|
@@ -5264,7 +5458,9 @@ function printNote(note) {
|
|
|
5264
5458
|
if (note) console.error(`note: ${note}`);
|
|
5265
5459
|
}
|
|
5266
5460
|
function register3(program3) {
|
|
5267
|
-
const journal = program3.command("journal").description(
|
|
5461
|
+
const journal = program3.command("journal").description(
|
|
5462
|
+
"An issue's stage journal: shared notes for its project + the stage your run was launched in"
|
|
5463
|
+
);
|
|
5268
5464
|
withCommon(
|
|
5269
5465
|
journal.command("show <ref>").description("Print the journal for the stage your run was launched in").option("--state <workflow>/<state>", STATE_FLAG_HELP)
|
|
5270
5466
|
).action(async (ref, opts) => {
|
|
@@ -5323,29 +5519,96 @@ start one: tines journal append ${issue.project_name}/${issue.number} "- <date>:
|
|
|
5323
5519
|
"the version being replaced (from the prompt or journal show)",
|
|
5324
5520
|
(v) => Number.parseInt(v, 10)
|
|
5325
5521
|
)
|
|
5326
|
-
).action(
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5522
|
+
).action(
|
|
5523
|
+
async (ref, opts) => {
|
|
5524
|
+
const api = client(opts);
|
|
5525
|
+
const { scope, note, item } = await resolveJournal(api, ref, opts.state);
|
|
5526
|
+
printNote(note);
|
|
5527
|
+
if (!item) die(`no journal exists yet for ${scope.label}; nothing to rewrite`);
|
|
5528
|
+
const updated = await api.updateContextItem(item.id, {
|
|
5529
|
+
body: readBodyValue(opts.body),
|
|
5530
|
+
expected_version: opts.expectVersion
|
|
5531
|
+
});
|
|
5532
|
+
if (opts.json) return printJson(updated);
|
|
5533
|
+
console.log(`rewrote the ${scope.label} journal (now v${updated.version})`);
|
|
5534
|
+
}
|
|
5535
|
+
);
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5538
|
+
// src/commands/login.ts
|
|
5539
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
5540
|
+
function maskKey(key) {
|
|
5541
|
+
return `${key.slice(0, 14)}\u2026`;
|
|
5542
|
+
}
|
|
5543
|
+
function readKeyArg(value) {
|
|
5544
|
+
if (value !== "-") return value;
|
|
5545
|
+
const key = readFileSync5(0, "utf8").trim();
|
|
5546
|
+
if (!key) die("no API key on stdin");
|
|
5547
|
+
return key;
|
|
5548
|
+
}
|
|
5549
|
+
function register4(program3) {
|
|
5550
|
+
program3.command("login").description("Store the API URL and key for this machine, so commands need no env vars").option("-u, --url <url>", `base URL of the Tines API to store (default ${DEFAULT_URL})`).option("--api-key <key>", 'API key from Settings \u2192 API keys ("-" reads it from stdin)').option("--no-verify", "store without checking the key against the API").action(async (opts) => {
|
|
5551
|
+
if (!opts.url && !opts.apiKey) die("pass --url and/or --api-key");
|
|
5552
|
+
const dir = defaultConfigDir();
|
|
5553
|
+
const apiKey = opts.apiKey ? readKeyArg(opts.apiKey) : void 0;
|
|
5554
|
+
const url = (opts.url ?? resolveUrlSetting({}).value ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
5555
|
+
const key = apiKey ?? resolveApiKeySetting({}).value;
|
|
5556
|
+
if (opts.verify && key) {
|
|
5557
|
+
try {
|
|
5558
|
+
await createApiClient({ baseUrl: url, apiKey: key }).listProjects({ limit: 1 });
|
|
5559
|
+
} catch (err) {
|
|
5560
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
5561
|
+
die(`${url} rejected the API key (${err.message}); nothing stored`);
|
|
5562
|
+
}
|
|
5563
|
+
if (err instanceof ApiError) throw err;
|
|
5564
|
+
const cause = err.cause?.code;
|
|
5565
|
+
die(
|
|
5566
|
+
`could not reach ${url}${cause ? ` (${cause})` : ""}; nothing stored (pass --no-verify to store anyway)`
|
|
5567
|
+
);
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
const stored = saveCliConfig(dir, { url: opts.url, api_key: apiKey });
|
|
5571
|
+
console.log(`stored in ${configPath(dir)}`);
|
|
5572
|
+
console.log(`url: ${stored.url ?? `${DEFAULT_URL} (default)`}`);
|
|
5573
|
+
console.log(`api key: ${stored.api_key ? maskKey(stored.api_key) : "none"}`);
|
|
5574
|
+
});
|
|
5575
|
+
program3.command("logout").description("Forget the stored API URL and key").action(() => {
|
|
5576
|
+
const dir = defaultConfigDir();
|
|
5577
|
+
console.log(
|
|
5578
|
+
clearCliConfig(dir) ? `removed ${configPath(dir)}` : `nothing stored in ${configPath(dir)}`
|
|
5579
|
+
);
|
|
5580
|
+
});
|
|
5581
|
+
withCommon(
|
|
5582
|
+
program3.command("config").description("Show the API URL and key in effect, and where each comes from")
|
|
5583
|
+
).action((opts) => {
|
|
5584
|
+
const url = resolveUrlSetting(opts);
|
|
5585
|
+
const key = resolveApiKeySetting(opts);
|
|
5586
|
+
const report = {
|
|
5587
|
+
url: { value: url.value, source: url.source },
|
|
5588
|
+
api_key: {
|
|
5589
|
+
value: key.value ? maskKey(key.value) : null,
|
|
5590
|
+
source: key.value ? key.source : null
|
|
5591
|
+
},
|
|
5592
|
+
config_path: configPath(defaultConfigDir())
|
|
5593
|
+
};
|
|
5594
|
+
if (opts.json) return printJson(report);
|
|
5595
|
+
console.log(`url: ${report.url.value} (${report.url.source})`);
|
|
5596
|
+
console.log(
|
|
5597
|
+
report.api_key.value ? `api key: ${report.api_key.value} (${report.api_key.source})` : "api key: none (pass --api-key, set TINES_API_KEY, or run `tines login`)"
|
|
5598
|
+
);
|
|
5599
|
+
console.log(`config file: ${report.config_path}`);
|
|
5337
5600
|
});
|
|
5338
5601
|
}
|
|
5339
5602
|
|
|
5340
5603
|
// src/commands/misc.ts
|
|
5341
5604
|
function registerTime(program3) {
|
|
5342
|
-
withCommon(
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
}
|
|
5348
|
-
);
|
|
5605
|
+
withCommon(
|
|
5606
|
+
program3.command("time").description("Fetch the current time from the Tines API")
|
|
5607
|
+
).action(async (opts) => {
|
|
5608
|
+
const result = await client(opts).getTime();
|
|
5609
|
+
if (opts.json) printJson(result);
|
|
5610
|
+
else console.log(`Server time: ${result.time} (unix ${result.unix})`);
|
|
5611
|
+
});
|
|
5349
5612
|
}
|
|
5350
5613
|
function registerEvents(program3) {
|
|
5351
5614
|
const events = program3.command("events").description("Read the activity log");
|
|
@@ -5374,7 +5637,7 @@ function registerEvents(program3) {
|
|
|
5374
5637
|
}
|
|
5375
5638
|
|
|
5376
5639
|
// src/commands/projects.ts
|
|
5377
|
-
function
|
|
5640
|
+
function register5(program3) {
|
|
5378
5641
|
const projects = program3.command("projects").description("Manage projects");
|
|
5379
5642
|
withList(projects.command("list").description("List projects")).action(async (opts) => {
|
|
5380
5643
|
const api = client(opts);
|
|
@@ -5388,7 +5651,10 @@ function register4(program3) {
|
|
|
5388
5651
|
});
|
|
5389
5652
|
});
|
|
5390
5653
|
withCommon(
|
|
5391
|
-
projects.command("create <name>").description("Create a project (with its initial context prompt)").option("-d, --description <text>", "project description").option("-w, --default-workflow <id-or-name>", "default workflow for new issues").option(
|
|
5654
|
+
projects.command("create <name>").description("Create a project (with its initial context prompt)").option("-d, --description <text>", "project description").option("-w, --default-workflow <id-or-name>", "default workflow for new issues").option(
|
|
5655
|
+
"--prompt <md>",
|
|
5656
|
+
"initial conventions prompt, stitched into every issue's agent prompt: inline Markdown or @file"
|
|
5657
|
+
).option("--no-prompt", "create without an initial prompt")
|
|
5392
5658
|
).action(
|
|
5393
5659
|
async (name2, opts) => {
|
|
5394
5660
|
if (opts.prompt === void 0 || opts.prompt === true) {
|
|
@@ -5465,8 +5731,10 @@ async function resolveRoutingScope(api, opts) {
|
|
|
5465
5731
|
if (opts.state) parts.push(`state ${opts.state}`);
|
|
5466
5732
|
return { projectId, stateId, label: parts.length > 0 ? parts.join(" \xB7 ") : "global" };
|
|
5467
5733
|
}
|
|
5468
|
-
function
|
|
5469
|
-
const routing = program3.command("routing").description(
|
|
5734
|
+
function register6(program3) {
|
|
5735
|
+
const routing = program3.command("routing").description(
|
|
5736
|
+
"Scoped routing rules: which runner takes which issues (most specific scope wins)"
|
|
5737
|
+
);
|
|
5470
5738
|
withCommon(routing.command("list").description("List routing rules, most specific first")).action(
|
|
5471
5739
|
async (opts) => {
|
|
5472
5740
|
const res = await client(opts).listRoutingRules();
|
|
@@ -5479,7 +5747,9 @@ function register5(program3) {
|
|
|
5479
5747
|
}
|
|
5480
5748
|
);
|
|
5481
5749
|
withCommon(
|
|
5482
|
-
routing.command("set <target...>").description(
|
|
5750
|
+
routing.command("set <target...>").description(
|
|
5751
|
+
"Create or replace the rule at a scope: an ordered list of <runner>[:tier] targets"
|
|
5752
|
+
).option("-p, --project <name>", "scope: project name or id").option("-s, --state <workflow/state>", "scope: workflow-qualified state")
|
|
5483
5753
|
).action(async (targetSpecs, opts) => {
|
|
5484
5754
|
const api = client(opts);
|
|
5485
5755
|
const scope = await resolveRoutingScope(api, opts);
|
|
@@ -5493,7 +5763,11 @@ function register5(program3) {
|
|
|
5493
5763
|
const existing = items.find(
|
|
5494
5764
|
(r) => r.scope.project_id === scope.projectId && r.scope.workflow_state_id === scope.stateId
|
|
5495
5765
|
);
|
|
5496
|
-
const rule = existing ? await api.updateRoutingRule(existing.id, { targets }) : await api.createRoutingRule({
|
|
5766
|
+
const rule = existing ? await api.updateRoutingRule(existing.id, { targets }) : await api.createRoutingRule({
|
|
5767
|
+
project_id: scope.projectId,
|
|
5768
|
+
workflow_state_id: scope.stateId,
|
|
5769
|
+
targets
|
|
5770
|
+
});
|
|
5497
5771
|
if (opts.json) return printJson(rule);
|
|
5498
5772
|
console.log(
|
|
5499
5773
|
`${existing ? "updated" : "created"} the ${rule.scope.label} rule: ${ruleTargetsLabel(rule)}`
|
|
@@ -5524,21 +5798,21 @@ import {
|
|
|
5524
5798
|
createWriteStream,
|
|
5525
5799
|
existsSync as existsSync4,
|
|
5526
5800
|
mkdirSync as mkdirSync4,
|
|
5527
|
-
readFileSync as
|
|
5801
|
+
readFileSync as readFileSync9,
|
|
5528
5802
|
rmSync as rmSync2,
|
|
5529
5803
|
statSync as statSync3,
|
|
5530
|
-
unlinkSync,
|
|
5804
|
+
unlinkSync as unlinkSync2,
|
|
5531
5805
|
writeFileSync as writeFileSync3
|
|
5532
5806
|
} from "node:fs";
|
|
5533
5807
|
import { hostname, platform, arch } from "node:os";
|
|
5534
|
-
import { dirname as dirname4, join as
|
|
5808
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
5535
5809
|
|
|
5536
5810
|
// src/version.ts
|
|
5537
|
-
import { readFileSync as
|
|
5811
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
5538
5812
|
function cliVersion() {
|
|
5539
5813
|
try {
|
|
5540
5814
|
const manifest = new URL("../package.json", import.meta.url);
|
|
5541
|
-
return JSON.parse(
|
|
5815
|
+
return JSON.parse(readFileSync6(manifest, "utf8")).version ?? "0.0.0-unknown";
|
|
5542
5816
|
} catch {
|
|
5543
5817
|
return "0.0.0-unknown";
|
|
5544
5818
|
}
|
|
@@ -5586,7 +5860,8 @@ function renderStreamEvent(event) {
|
|
|
5586
5860
|
case "result": {
|
|
5587
5861
|
const parts = [];
|
|
5588
5862
|
if (typeof event.num_turns === "number") parts.push(`${event.num_turns} turns`);
|
|
5589
|
-
if (typeof event.total_cost_usd === "number")
|
|
5863
|
+
if (typeof event.total_cost_usd === "number")
|
|
5864
|
+
parts.push(`$${event.total_cost_usd.toFixed(2)}`);
|
|
5590
5865
|
const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
5591
5866
|
const lines = [`[session] result: ${event.subtype ?? "done"}${detail}`];
|
|
5592
5867
|
if (event.is_error && event.result) lines.push(`[error] ${clip(event.result, 2e3)}`);
|
|
@@ -5641,8 +5916,8 @@ var ClaudeStreamRenderer = class {
|
|
|
5641
5916
|
|
|
5642
5917
|
// src/daemon/cli-refresh.ts
|
|
5643
5918
|
import { spawn } from "node:child_process";
|
|
5644
|
-
import { existsSync as
|
|
5645
|
-
import { dirname as
|
|
5919
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
5920
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
5646
5921
|
|
|
5647
5922
|
// src/daemon/support.ts
|
|
5648
5923
|
import { delimiter } from "node:path";
|
|
@@ -5917,14 +6192,14 @@ function buildSpawnEnv(base, opts) {
|
|
|
5917
6192
|
var PACKAGE = "tines";
|
|
5918
6193
|
var NPM_ARGS = ["--min-release-age=0", "--no-audit", "--no-fund", "--loglevel=error"];
|
|
5919
6194
|
function agentCliPrefix(configDir) {
|
|
5920
|
-
return
|
|
6195
|
+
return join3(configDir, "cli");
|
|
5921
6196
|
}
|
|
5922
6197
|
function binDirOf(prefix) {
|
|
5923
|
-
return
|
|
6198
|
+
return join3(prefix, "node_modules", ".bin");
|
|
5924
6199
|
}
|
|
5925
6200
|
function installedVersion(prefix) {
|
|
5926
6201
|
try {
|
|
5927
|
-
const pkg =
|
|
6202
|
+
const pkg = readFileSync7(join3(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
|
|
5928
6203
|
const version = JSON.parse(pkg).version;
|
|
5929
6204
|
return typeof version === "string" ? version : null;
|
|
5930
6205
|
} catch {
|
|
@@ -5933,7 +6208,7 @@ function installedVersion(prefix) {
|
|
|
5933
6208
|
}
|
|
5934
6209
|
function lastGood(prefix) {
|
|
5935
6210
|
const binDir = binDirOf(prefix);
|
|
5936
|
-
if (!
|
|
6211
|
+
if (!existsSync3(join3(binDir, PACKAGE))) return AMBIENT_CLI;
|
|
5937
6212
|
return { binDir, version: installedVersion(prefix), source: "stale" };
|
|
5938
6213
|
}
|
|
5939
6214
|
function runNpm(file, args, cwd, timeoutMs) {
|
|
@@ -5970,7 +6245,7 @@ async function installAgentCli(opts) {
|
|
|
5970
6245
|
const prefix = agentCliPrefix(opts.configDir);
|
|
5971
6246
|
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
5972
6247
|
try {
|
|
5973
|
-
|
|
6248
|
+
mkdirSync3(prefix, { recursive: true });
|
|
5974
6249
|
} catch (err) {
|
|
5975
6250
|
opts.log(`agent CLI refresh: cannot create ${prefix} (${message(err)})`);
|
|
5976
6251
|
return AMBIENT_CLI;
|
|
@@ -5978,8 +6253,8 @@ async function installAgentCli(opts) {
|
|
|
5978
6253
|
const args = ["install", "--prefix", prefix, `${PACKAGE}@latest`, ...NPM_ARGS];
|
|
5979
6254
|
let result = await runNpm("npm", args, prefix, timeoutMs);
|
|
5980
6255
|
if (result.spawnError?.code === "ENOENT") {
|
|
5981
|
-
const sibling =
|
|
5982
|
-
if (
|
|
6256
|
+
const sibling = join3(dirname3(process.execPath), "npm");
|
|
6257
|
+
if (existsSync3(sibling)) result = await runNpm(sibling, args, prefix, timeoutMs);
|
|
5983
6258
|
}
|
|
5984
6259
|
if (result.code !== 0) {
|
|
5985
6260
|
const fallback = lastGood(prefix);
|
|
@@ -6001,38 +6276,13 @@ function message(err) {
|
|
|
6001
6276
|
}
|
|
6002
6277
|
|
|
6003
6278
|
// src/daemon/store.ts
|
|
6004
|
-
import {
|
|
6005
|
-
|
|
6006
|
-
mkdirSync as mkdirSync3,
|
|
6007
|
-
readdirSync as readdirSync2,
|
|
6008
|
-
readFileSync as readFileSync6,
|
|
6009
|
-
rmSync,
|
|
6010
|
-
statSync as statSync2,
|
|
6011
|
-
writeFileSync as writeFileSync2
|
|
6012
|
-
} from "node:fs";
|
|
6013
|
-
import { homedir } from "node:os";
|
|
6014
|
-
import { dirname as dirname3, join as join3 } from "node:path";
|
|
6015
|
-
function defaultConfigDir() {
|
|
6016
|
-
return process.env.TINES_CONFIG_DIR ?? join3(homedir(), ".config", "tines");
|
|
6017
|
-
}
|
|
6018
|
-
function readJsonFile(path2) {
|
|
6019
|
-
if (!existsSync3(path2)) return null;
|
|
6020
|
-
try {
|
|
6021
|
-
return JSON.parse(readFileSync6(path2, "utf8"));
|
|
6022
|
-
} catch {
|
|
6023
|
-
return null;
|
|
6024
|
-
}
|
|
6025
|
-
}
|
|
6026
|
-
function writeJsonFile(path2, value, { secret = false } = {}) {
|
|
6027
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
6028
|
-
writeFileSync2(path2, `${JSON.stringify(value, null, 2)}
|
|
6029
|
-
`, secret ? { mode: 384 } : {});
|
|
6030
|
-
}
|
|
6279
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync, statSync as statSync2 } from "node:fs";
|
|
6280
|
+
import { join as join4 } from "node:path";
|
|
6031
6281
|
function credentialsKey(url, name2) {
|
|
6032
6282
|
return `${url.replace(/\/+$/, "")}#${name2}`;
|
|
6033
6283
|
}
|
|
6034
6284
|
function credentialsPath(dir) {
|
|
6035
|
-
return
|
|
6285
|
+
return join4(dir, "runners.json");
|
|
6036
6286
|
}
|
|
6037
6287
|
function loadRunnerCredentials(dir, url, name2) {
|
|
6038
6288
|
const all = readJsonFile(credentialsPath(dir));
|
|
@@ -6055,7 +6305,7 @@ function clearRunnerCredentials(dir, url, name2) {
|
|
|
6055
6305
|
writeJsonFile(path2, all, { secret: true });
|
|
6056
6306
|
}
|
|
6057
6307
|
function daemonStatePath(dir, runnerId) {
|
|
6058
|
-
return
|
|
6308
|
+
return join4(dir, `daemon-state-${runnerId}.json`);
|
|
6059
6309
|
}
|
|
6060
6310
|
function loadDaemonState(path2) {
|
|
6061
6311
|
const state = readJsonFile(path2);
|
|
@@ -6069,10 +6319,10 @@ function saveDaemonState(path2, runs) {
|
|
|
6069
6319
|
}
|
|
6070
6320
|
function processStartTimeMs(pid) {
|
|
6071
6321
|
try {
|
|
6072
|
-
const stat =
|
|
6322
|
+
const stat = readFileSync8(`/proc/${pid}/stat`, "utf8");
|
|
6073
6323
|
const afterComm = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
6074
6324
|
const startTicks = Number(afterComm[19]);
|
|
6075
|
-
const btimeLine =
|
|
6325
|
+
const btimeLine = readFileSync8("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
|
|
6076
6326
|
const btime = Number(btimeLine?.slice("btime ".length));
|
|
6077
6327
|
if (!Number.isFinite(startTicks) || !Number.isFinite(btime)) return null;
|
|
6078
6328
|
return btime * 1e3 + startTicks / 100 * 1e3;
|
|
@@ -6081,10 +6331,10 @@ function processStartTimeMs(pid) {
|
|
|
6081
6331
|
}
|
|
6082
6332
|
}
|
|
6083
6333
|
function workspacesDir(configDir) {
|
|
6084
|
-
return
|
|
6334
|
+
return join4(configDir, "workspaces");
|
|
6085
6335
|
}
|
|
6086
6336
|
function keptMarkerPath(workspace) {
|
|
6087
|
-
return
|
|
6337
|
+
return join4(workspace, "kept.json");
|
|
6088
6338
|
}
|
|
6089
6339
|
function writeKeptMarker(workspace, marker) {
|
|
6090
6340
|
writeJsonFile(keptMarkerPath(workspace), marker);
|
|
@@ -6106,7 +6356,7 @@ function listKeptWorkspaces(configDir) {
|
|
|
6106
6356
|
}
|
|
6107
6357
|
const kept = [];
|
|
6108
6358
|
for (const name2 of names) {
|
|
6109
|
-
const path2 =
|
|
6359
|
+
const path2 = join4(root, name2);
|
|
6110
6360
|
const marker = readKeptMarker(path2);
|
|
6111
6361
|
if (marker) kept.push({ ...marker, path: path2 });
|
|
6112
6362
|
}
|
|
@@ -6143,7 +6393,7 @@ function directorySizeBytes(path2) {
|
|
|
6143
6393
|
return 0;
|
|
6144
6394
|
}
|
|
6145
6395
|
for (const entry of entries) {
|
|
6146
|
-
const child =
|
|
6396
|
+
const child = join4(path2, entry.name);
|
|
6147
6397
|
if (entry.isDirectory()) total += directorySizeBytes(child);
|
|
6148
6398
|
else if (entry.isFile()) {
|
|
6149
6399
|
try {
|
|
@@ -6166,20 +6416,23 @@ async function uploadRawLog(run) {
|
|
|
6166
6416
|
await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
|
|
6167
6417
|
const size = statSync3(path2).size;
|
|
6168
6418
|
if (size > 0) {
|
|
6169
|
-
let body =
|
|
6419
|
+
let body = readFileSync9(path2);
|
|
6170
6420
|
if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
|
|
6171
6421
|
const marker = Buffer.from(
|
|
6172
6422
|
`{"type":"tines_truncated","dropped_bytes":${body.byteLength - RUN_LOG_RAW_MAX_BYTES}}
|
|
6173
6423
|
`
|
|
6174
6424
|
);
|
|
6175
|
-
body = Buffer.concat([
|
|
6425
|
+
body = Buffer.concat([
|
|
6426
|
+
marker,
|
|
6427
|
+
body.subarray(body.byteLength - RUN_LOG_RAW_MAX_BYTES + marker.byteLength)
|
|
6428
|
+
]);
|
|
6176
6429
|
}
|
|
6177
6430
|
await run.rawUpload(body);
|
|
6178
6431
|
}
|
|
6179
6432
|
} catch {
|
|
6180
6433
|
} finally {
|
|
6181
6434
|
try {
|
|
6182
|
-
|
|
6435
|
+
unlinkSync2(path2);
|
|
6183
6436
|
} catch {
|
|
6184
6437
|
}
|
|
6185
6438
|
}
|
|
@@ -6236,7 +6489,9 @@ async function runDaemon(opts) {
|
|
|
6236
6489
|
};
|
|
6237
6490
|
let creds = loadRunnerCredentials(opts.configDir, opts.url, opts.name);
|
|
6238
6491
|
if (creds) {
|
|
6239
|
-
log(
|
|
6492
|
+
log(
|
|
6493
|
+
`reconnecting as runner "${opts.name}" (${creds.runner_id}) \u2014 token from ${opts.configDir}`
|
|
6494
|
+
);
|
|
6240
6495
|
} else {
|
|
6241
6496
|
if (!opts.apiKey) {
|
|
6242
6497
|
throw new Error(
|
|
@@ -6265,37 +6520,40 @@ async function runDaemon(opts) {
|
|
|
6265
6520
|
const ensureCli = () => opts.cliRefresh ? refresher.ensure() : Promise.resolve(AMBIENT_CLI);
|
|
6266
6521
|
const cliLabel = (cli) => cli.source === "ambient" ? `ambient PATH (${opts.cliRefresh ? "refresh failed" : "refresh disabled"})` : `tines ${cli.version ?? "unknown"} (daemon-managed${cli.source === "stale" ? ", last-good copy" : ""})`;
|
|
6267
6522
|
log(`agent CLI: ${cliLabel(await ensureCli())}`);
|
|
6268
|
-
const table2 = new RunTable(
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6523
|
+
const table2 = new RunTable(
|
|
6524
|
+
{
|
|
6525
|
+
finish: async (run, status, error) => {
|
|
6526
|
+
await client2.finishRun(run.runId, { status, ...error ? { error } : {} });
|
|
6527
|
+
},
|
|
6528
|
+
release: (run, { keep, outcome }) => {
|
|
6529
|
+
if (run.timeout) clearTimeout(run.timeout);
|
|
6530
|
+
run.renderer?.finish();
|
|
6531
|
+
settleWorkspace(run.workspace, keep, {
|
|
6532
|
+
run_id: run.runId,
|
|
6533
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {},
|
|
6534
|
+
status: outcome,
|
|
6535
|
+
...run.endNote ? { error: run.endNote } : {}
|
|
6536
|
+
});
|
|
6537
|
+
void uploadRawLog(run);
|
|
6538
|
+
sweepKeptWorkspaces();
|
|
6539
|
+
},
|
|
6540
|
+
noteKept: (run) => run.batcher.append(`workspace kept at ${run.workspace}
|
|
6285
6541
|
`),
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6542
|
+
persist: () => {
|
|
6543
|
+
const entries = table2.values().filter((run) => run.child?.pid !== void 0).map((run) => ({
|
|
6544
|
+
run_id: run.runId,
|
|
6545
|
+
pid: run.child.pid,
|
|
6546
|
+
workspace: run.workspace,
|
|
6547
|
+
key_fingerprint: run.keyFingerprint,
|
|
6548
|
+
started_at: run.spawnedAt,
|
|
6549
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {}
|
|
6550
|
+
}));
|
|
6551
|
+
saveDaemonState(statePath, entries);
|
|
6552
|
+
},
|
|
6553
|
+
log
|
|
6296
6554
|
},
|
|
6297
|
-
|
|
6298
|
-
|
|
6555
|
+
{ keep: (outcome) => keepWorkspace(opts.keepWorkspaces, outcome) }
|
|
6556
|
+
);
|
|
6299
6557
|
for (const orphan of loadDaemonState(statePath)) {
|
|
6300
6558
|
if (pidAlive(orphan.pid)) {
|
|
6301
6559
|
const processStart = processStartTimeMs(orphan.pid);
|
|
@@ -6303,7 +6561,9 @@ async function runDaemon(opts) {
|
|
|
6303
6561
|
if (reused) {
|
|
6304
6562
|
log(`state-file pid ${orphan.pid} (run ${orphan.run_id}) was recycled; not killing it`);
|
|
6305
6563
|
} else {
|
|
6306
|
-
log(
|
|
6564
|
+
log(
|
|
6565
|
+
`killing orphaned harness from a previous life: run ${orphan.run_id} (pid ${orphan.pid})`
|
|
6566
|
+
);
|
|
6307
6567
|
killTree(orphan.pid, "SIGKILL");
|
|
6308
6568
|
}
|
|
6309
6569
|
}
|
|
@@ -6336,7 +6596,7 @@ async function runDaemon(opts) {
|
|
|
6336
6596
|
const launch = async (assignment) => {
|
|
6337
6597
|
const runId = assignment.run.id;
|
|
6338
6598
|
if (table2.has(runId)) return;
|
|
6339
|
-
const workspace =
|
|
6599
|
+
const workspace = join5(workspacesDir(opts.configDir), runId);
|
|
6340
6600
|
const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
|
|
6341
6601
|
const run = {
|
|
6342
6602
|
runId,
|
|
@@ -6354,32 +6614,43 @@ async function runDaemon(opts) {
|
|
|
6354
6614
|
};
|
|
6355
6615
|
run.flush = () => run.batcher.flush();
|
|
6356
6616
|
table2.track(run);
|
|
6357
|
-
log(
|
|
6617
|
+
log(
|
|
6618
|
+
`run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); materializing workspace`
|
|
6619
|
+
);
|
|
6358
6620
|
try {
|
|
6359
6621
|
rmSync2(workspace, { recursive: true, force: true });
|
|
6360
6622
|
mkdirSync4(workspace, { recursive: true });
|
|
6361
|
-
writeFileSync3(
|
|
6623
|
+
writeFileSync3(join5(workspace, "prompt.md"), `${assignment.prompt}
|
|
6362
6624
|
`);
|
|
6363
6625
|
for (const skill of assignment.bundle.skills) {
|
|
6364
6626
|
for (const file of skill.files) {
|
|
6365
|
-
const target =
|
|
6627
|
+
const target = join5(workspace, "skills", skill.name, file.path);
|
|
6366
6628
|
mkdirSync4(dirname4(target), { recursive: true });
|
|
6367
6629
|
writeFileSync3(target, file.content);
|
|
6368
6630
|
}
|
|
6369
6631
|
}
|
|
6370
6632
|
writeFileSync3(
|
|
6371
|
-
|
|
6633
|
+
join5(workspace, "repos.json"),
|
|
6372
6634
|
`${JSON.stringify(assignment.bundle.repos, null, 2)}
|
|
6373
6635
|
`
|
|
6374
6636
|
);
|
|
6375
6637
|
for (const repo of assignment.bundle.repos) {
|
|
6376
6638
|
if (run.settled) return table2.cleanup(run);
|
|
6377
|
-
const args = [
|
|
6639
|
+
const args = [
|
|
6640
|
+
"clone",
|
|
6641
|
+
...repo.branch ? ["--branch", repo.branch] : [],
|
|
6642
|
+
repo.url,
|
|
6643
|
+
repo.dir
|
|
6644
|
+
];
|
|
6378
6645
|
run.batcher.append(`$ git ${args.join(" ")}
|
|
6379
6646
|
`);
|
|
6380
6647
|
const result = await runGit(args, workspace, run.batcher);
|
|
6381
6648
|
if (result !== 0) {
|
|
6382
|
-
return table2.finishAndCleanup(
|
|
6649
|
+
return table2.finishAndCleanup(
|
|
6650
|
+
run,
|
|
6651
|
+
"failed",
|
|
6652
|
+
`git clone failed for ${repo.url} (exit ${result})`
|
|
6653
|
+
);
|
|
6383
6654
|
}
|
|
6384
6655
|
}
|
|
6385
6656
|
if (run.settled) return table2.cleanup(run);
|
|
@@ -6393,7 +6664,7 @@ async function runDaemon(opts) {
|
|
|
6393
6664
|
);
|
|
6394
6665
|
const harnessInput = {
|
|
6395
6666
|
workspace,
|
|
6396
|
-
promptFile:
|
|
6667
|
+
promptFile: join5(workspace, "prompt.md"),
|
|
6397
6668
|
prompt: assignment.prompt,
|
|
6398
6669
|
model: assignment.run.model
|
|
6399
6670
|
};
|
|
@@ -6426,7 +6697,7 @@ async function runDaemon(opts) {
|
|
|
6426
6697
|
const renderer = new ClaudeStreamRenderer((line) => run.batcher.append(line));
|
|
6427
6698
|
run.renderer = renderer;
|
|
6428
6699
|
run.drain = () => renderer.finish();
|
|
6429
|
-
const spoolPath =
|
|
6700
|
+
const spoolPath = join5(opts.configDir, "rawlogs", `${runId}.ndjson`);
|
|
6430
6701
|
mkdirSync4(dirname4(spoolPath), { recursive: true });
|
|
6431
6702
|
run.rawSpoolPath = spoolPath;
|
|
6432
6703
|
run.rawSpool = createWriteStream(spoolPath);
|
|
@@ -6439,16 +6710,13 @@ async function runDaemon(opts) {
|
|
|
6439
6710
|
child.stdout?.on("data", (data) => run.batcher.append(data.toString("utf8")));
|
|
6440
6711
|
}
|
|
6441
6712
|
child.stderr?.on("data", (data) => run.batcher.append(data.toString("utf8")));
|
|
6442
|
-
run.timeout = setTimeout(
|
|
6443
|
-
()
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
},
|
|
6450
|
-
assignment.timeout_minutes * 6e4
|
|
6451
|
-
);
|
|
6713
|
+
run.timeout = setTimeout(() => {
|
|
6714
|
+
if (run.settled) return;
|
|
6715
|
+
log(`run ${runId} hit its ${assignment.timeout_minutes}m timeout; killing`);
|
|
6716
|
+
run.timedOut = true;
|
|
6717
|
+
if (child.pid) killTree(child.pid, "SIGTERM");
|
|
6718
|
+
if (child.pid) setTimeout(() => killTree(child.pid, "SIGKILL"), 5e3).unref?.();
|
|
6719
|
+
}, assignment.timeout_minutes * 6e4);
|
|
6452
6720
|
child.on("error", (err) => {
|
|
6453
6721
|
void table2.finishAndCleanup(run, "failed", `failed to launch harness: ${message2(err)}`);
|
|
6454
6722
|
});
|
|
@@ -6572,27 +6840,31 @@ function printTierTable(runner) {
|
|
|
6572
6840
|
override?.effort ? `effort ${override.effort}` : null,
|
|
6573
6841
|
stale ? `stale \u2014 built-in is now ${builtin}` : null
|
|
6574
6842
|
].filter(Boolean);
|
|
6575
|
-
console.log(
|
|
6843
|
+
console.log(
|
|
6844
|
+
` ${tier}: ${model} [${source}]${marks.length > 0 ? ` (${marks.join(", ")})` : ""}`
|
|
6845
|
+
);
|
|
6576
6846
|
}
|
|
6577
6847
|
}
|
|
6578
|
-
function
|
|
6848
|
+
function register7(program3) {
|
|
6579
6849
|
const runners = program3.command("runners").description("Manage the runner registry");
|
|
6580
|
-
withCommon(runners.command("list").description("List runners")).action(
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
[
|
|
6586
|
-
|
|
6587
|
-
r
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6850
|
+
withCommon(runners.command("list").description("List runners")).action(
|
|
6851
|
+
async (opts) => {
|
|
6852
|
+
const res = await client(opts).listRunners();
|
|
6853
|
+
if (opts.json) return printJson(res);
|
|
6854
|
+
if (res.items.length === 0) return console.log("no runners");
|
|
6855
|
+
table([
|
|
6856
|
+
["NAME", "TYPE", "STATUS", "RUNS", "TIER", "LAST SEEN"],
|
|
6857
|
+
...res.items.map((r) => [
|
|
6858
|
+
r.name,
|
|
6859
|
+
r.type,
|
|
6860
|
+
runnerStatusLabel(r),
|
|
6861
|
+
`${r.active_runs}/${r.max_concurrent}`,
|
|
6862
|
+
r.default_tier,
|
|
6863
|
+
r.last_seen_at ? timestamp(r.last_seen_at) : "\u2014"
|
|
6864
|
+
])
|
|
6865
|
+
]);
|
|
6866
|
+
}
|
|
6867
|
+
);
|
|
6596
6868
|
withCommon(runners.command("show <name>").description("Show a runner")).action(
|
|
6597
6869
|
async (ref, opts) => {
|
|
6598
6870
|
const runner = await resolveRunner(client(opts), ref);
|
|
@@ -6616,7 +6888,8 @@ function register6(program3) {
|
|
|
6616
6888
|
const b = runner.budget;
|
|
6617
6889
|
const parts = [];
|
|
6618
6890
|
if (b.max_run_cost_usd !== void 0) parts.push(`$${b.max_run_cost_usd}/run`);
|
|
6619
|
-
if (b.max_run_tokens !== void 0)
|
|
6891
|
+
if (b.max_run_tokens !== void 0)
|
|
6892
|
+
parts.push(`${b.max_run_tokens.toLocaleString()} tok/run`);
|
|
6620
6893
|
if (b.daily_usd !== void 0) parts.push(`$${b.daily_usd}/day`);
|
|
6621
6894
|
if (b.daily_tokens !== void 0) parts.push(`${b.daily_tokens.toLocaleString()} tok/day`);
|
|
6622
6895
|
if (parts.length > 0) console.log(`budget: ${parts.join(" ")}`);
|
|
@@ -6670,7 +6943,12 @@ function register6(program3) {
|
|
|
6670
6943
|
}
|
|
6671
6944
|
);
|
|
6672
6945
|
withCommon(
|
|
6673
|
-
runners.command("budget <name>").description(
|
|
6946
|
+
runners.command("budget <name>").description(
|
|
6947
|
+
"Set or clear a runner's money limits (per-run caps enforce now; daily limits arrive with the budgets milestone)"
|
|
6948
|
+
).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(
|
|
6949
|
+
"--daily-tokens <n>",
|
|
6950
|
+
"daily token limit (stored now, enforced by the budgets milestone)"
|
|
6951
|
+
).option("--clear", "remove all limits")
|
|
6674
6952
|
).action(
|
|
6675
6953
|
async (ref, opts) => {
|
|
6676
6954
|
const api = client(opts);
|
|
@@ -6685,7 +6963,8 @@ function register6(program3) {
|
|
|
6685
6963
|
} else if (flags) {
|
|
6686
6964
|
const num = (value, flag) => {
|
|
6687
6965
|
const n = Number(value);
|
|
6688
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
6966
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
6967
|
+
die(`${flag} must be a positive number, got "${value}"`);
|
|
6689
6968
|
return n;
|
|
6690
6969
|
};
|
|
6691
6970
|
updated = await api.updateRunner(runner.id, {
|
|
@@ -6703,10 +6982,14 @@ function register6(program3) {
|
|
|
6703
6982
|
if (!b) return console.log(`no limits on "${updated.name}"`);
|
|
6704
6983
|
console.log(`limits on "${updated.name}":`);
|
|
6705
6984
|
if (b.max_run_cost_usd !== void 0) console.log(` $${b.max_run_cost_usd} per run`);
|
|
6706
|
-
if (b.max_run_tokens !== void 0)
|
|
6707
|
-
|
|
6985
|
+
if (b.max_run_tokens !== void 0)
|
|
6986
|
+
console.log(` ${b.max_run_tokens.toLocaleString()} tokens per run`);
|
|
6987
|
+
if (b.daily_usd !== void 0)
|
|
6988
|
+
console.log(` $${b.daily_usd} per day (enforced by the budgets milestone)`);
|
|
6708
6989
|
if (b.daily_tokens !== void 0) {
|
|
6709
|
-
console.log(
|
|
6990
|
+
console.log(
|
|
6991
|
+
` ${b.daily_tokens.toLocaleString()} tokens per day (enforced by the budgets milestone)`
|
|
6992
|
+
);
|
|
6710
6993
|
}
|
|
6711
6994
|
}
|
|
6712
6995
|
);
|
|
@@ -6729,7 +7012,12 @@ function register6(program3) {
|
|
|
6729
7012
|
}
|
|
6730
7013
|
);
|
|
6731
7014
|
withCommon(
|
|
6732
|
-
runners.command("remove <name>").description(
|
|
7015
|
+
runners.command("remove <name>").description(
|
|
7016
|
+
"Remove a runner (refused while routing rules or pins reference it, unless --force)"
|
|
7017
|
+
).option(
|
|
7018
|
+
"--force",
|
|
7019
|
+
"strip the runner from routing rules and clear issue pins (emptied rules are kept, flagged)"
|
|
7020
|
+
)
|
|
6733
7021
|
).action(async (ref, opts) => {
|
|
6734
7022
|
const api = client(opts);
|
|
6735
7023
|
const runner = await resolveRunner(api, ref);
|
|
@@ -6751,14 +7039,28 @@ function register6(program3) {
|
|
|
6751
7039
|
runner_id: rotated.runner.id,
|
|
6752
7040
|
token: rotated.runner_token
|
|
6753
7041
|
});
|
|
6754
|
-
console.log(
|
|
7042
|
+
console.log(
|
|
7043
|
+
`stored it for the daemon on this machine (${defaultConfigDir()}); restart the daemon to adopt it.`
|
|
7044
|
+
);
|
|
6755
7045
|
} else {
|
|
6756
|
-
console.log(
|
|
7046
|
+
console.log(
|
|
7047
|
+
"drop it into the daemon machine's config \u2014 its next poll gets a 401 until it adopts the new token."
|
|
7048
|
+
);
|
|
6757
7049
|
}
|
|
6758
7050
|
});
|
|
6759
7051
|
const runnerCmd = program3.command("runner").description("The local runner daemon");
|
|
6760
7052
|
withCommon(
|
|
6761
|
-
runnerCmd.command("daemon").description(
|
|
7053
|
+
runnerCmd.command("daemon").description(
|
|
7054
|
+
"Run the local runner daemon: register/reconnect, poll for assigned runs, execute them"
|
|
7055
|
+
).option("--name <name>", "runner name, unique per user (default: this hostname)").option("--harness <harness>", "claude-code | codex | custom", "claude-code").option(
|
|
7056
|
+
"--command <template>",
|
|
7057
|
+
"custom harness command template ({prompt_file}, {workspace}, {model})"
|
|
7058
|
+
).option("--max-concurrent <n>", "maximum simultaneous runs", (v) => Number.parseInt(v, 10), 1).option(
|
|
7059
|
+
"--poll-interval <seconds>",
|
|
7060
|
+
"seconds between polls",
|
|
7061
|
+
(v) => Number.parseInt(v, 10),
|
|
7062
|
+
15
|
|
7063
|
+
).option(
|
|
6762
7064
|
"--no-cli-refresh",
|
|
6763
7065
|
"do not install/refresh the agent-facing tines CLI from npm (harnesses use the ambient PATH)"
|
|
6764
7066
|
).option(
|
|
@@ -6783,7 +7085,9 @@ function register6(program3) {
|
|
|
6783
7085
|
die(`--harness must be claude-code, codex, or custom, got "${opts.harness}"`);
|
|
6784
7086
|
}
|
|
6785
7087
|
if (harness === "custom" && !opts.command) {
|
|
6786
|
-
die(
|
|
7088
|
+
die(
|
|
7089
|
+
'the custom harness needs --command "<template>" ({prompt_file}, {workspace}, {model})'
|
|
7090
|
+
);
|
|
6787
7091
|
}
|
|
6788
7092
|
if (harness !== "custom" && opts.command) die("--command only applies to --harness custom");
|
|
6789
7093
|
if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent < 1 || opts.maxConcurrent > 100) {
|
|
@@ -6794,7 +7098,9 @@ function register6(program3) {
|
|
|
6794
7098
|
}
|
|
6795
7099
|
const keepWorkspaces = opts.keepWorkspaces;
|
|
6796
7100
|
if (!KEEP_WORKSPACES_MODES.includes(keepWorkspaces)) {
|
|
6797
|
-
die(
|
|
7101
|
+
die(
|
|
7102
|
+
`--keep-workspaces must be ${KEEP_WORKSPACES_MODES.join(", ")}, got "${opts.keepWorkspaces}"`
|
|
7103
|
+
);
|
|
6798
7104
|
}
|
|
6799
7105
|
if (!Number.isFinite(opts.keepWorkspacesFor) || opts.keepWorkspacesFor <= 0) {
|
|
6800
7106
|
die("--keep-workspaces-for must be a positive number of hours");
|
|
@@ -6825,9 +7131,7 @@ function register6(program3) {
|
|
|
6825
7131
|
if (opts.json) return printJson({ items: sized });
|
|
6826
7132
|
if (sized.length === 0) {
|
|
6827
7133
|
console.log(`no kept workspaces in ${workspacesDir(configDir)}`);
|
|
6828
|
-
return console.log(
|
|
6829
|
-
"the daemon keeps them only with --keep-workspaces failed (or always)."
|
|
6830
|
-
);
|
|
7134
|
+
return console.log("the daemon keeps them only with --keep-workspaces failed (or always).");
|
|
6831
7135
|
}
|
|
6832
7136
|
table([
|
|
6833
7137
|
["RUN", "ISSUE", "STATUS", "AGE", "SIZE", "PATH"],
|
|
@@ -6838,7 +7142,8 @@ function register6(program3) {
|
|
|
6838
7142
|
if (opts.all === void 0 && opts.olderThan === void 0) {
|
|
6839
7143
|
die("pass --all or --older-than <hours>");
|
|
6840
7144
|
}
|
|
6841
|
-
if (opts.all && opts.olderThan !== void 0)
|
|
7145
|
+
if (opts.all && opts.olderThan !== void 0)
|
|
7146
|
+
die("--all cannot be combined with --older-than");
|
|
6842
7147
|
if (opts.olderThan !== void 0 && (!Number.isFinite(opts.olderThan) || opts.olderThan < 0)) {
|
|
6843
7148
|
die("--older-than must be a non-negative number of hours");
|
|
6844
7149
|
}
|
|
@@ -6868,61 +7173,69 @@ function register6(program3) {
|
|
|
6868
7173
|
);
|
|
6869
7174
|
printList(res, opts, (items) => {
|
|
6870
7175
|
if (items.length === 0) return console.log(opts.active ? "no active runs" : "no runs");
|
|
6871
|
-
table([
|
|
7176
|
+
table([
|
|
7177
|
+
["ID", "ISSUE", "RUNNER", "TIER", "STATUS", "DURATION", "COST", "CREATED"],
|
|
7178
|
+
...items.map(runRow)
|
|
7179
|
+
]);
|
|
6872
7180
|
});
|
|
6873
7181
|
});
|
|
6874
7182
|
withCommon(
|
|
6875
7183
|
runsCmd.command("show <id>").description("Show a run; --logs prints the captured log tail, --logs --full the whole log").option("--logs", "print the log tail").option("--full", "with --logs: print the complete log, not the 256 KB tail").option("--raw", "with --logs --full: print the unrendered harness stream instead")
|
|
6876
|
-
).action(
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
if (parts.length > 0) console.log(`usage: ${parts.join(" ")}`);
|
|
6898
|
-
}
|
|
6899
|
-
if (run.provider_session_id) console.log(`provider session: ${run.provider_session_id}`);
|
|
6900
|
-
if (run.provider_url) console.log(`provider console: ${run.provider_url}`);
|
|
6901
|
-
if (run.error) console.log(`error: ${run.error}`);
|
|
6902
|
-
if (opts.logs) {
|
|
6903
|
-
console.log("");
|
|
6904
|
-
if (opts.full || opts.raw) {
|
|
6905
|
-
const res = await api.getRunLogFull(id, { raw: opts.raw });
|
|
6906
|
-
const body = res.body;
|
|
6907
|
-
if (!body) return;
|
|
6908
|
-
const reader = body.getReader();
|
|
6909
|
-
const decoder = new TextDecoder();
|
|
6910
|
-
for (; ; ) {
|
|
6911
|
-
const { done, value } = await reader.read();
|
|
6912
|
-
if (done) break;
|
|
6913
|
-
if (value) process.stdout.write(decoder.decode(value, { stream: true }));
|
|
7184
|
+
).action(
|
|
7185
|
+
async (id, opts) => {
|
|
7186
|
+
const api = client(opts);
|
|
7187
|
+
const run = await api.getRun(id);
|
|
7188
|
+
if (opts.json) return printJson(run);
|
|
7189
|
+
console.log(`${run.id} ${run.status} on ${run.runner_name}`);
|
|
7190
|
+
if (run.issue_ref) console.log(`issue: ${issueRef(run.issue_ref)} \u2014 ${run.issue_ref.title}`);
|
|
7191
|
+
console.log(`tier: ${run.tier} model: ${run.model ?? "(n/a)"}`);
|
|
7192
|
+
console.log(
|
|
7193
|
+
`states: ${run.state_at_start_name ?? run.state_id_at_start} \u2192 ${run.state_at_end_name ?? run.state_id_at_end ?? "\u2026"}`
|
|
7194
|
+
);
|
|
7195
|
+
console.log(
|
|
7196
|
+
`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)}`
|
|
7197
|
+
);
|
|
7198
|
+
if (run.usage) {
|
|
7199
|
+
const u = run.usage;
|
|
7200
|
+
const parts = [];
|
|
7201
|
+
if (u.input_tokens !== void 0 || u.output_tokens !== void 0) {
|
|
7202
|
+
parts.push(
|
|
7203
|
+
`${(u.input_tokens ?? 0).toLocaleString()} in / ${(u.output_tokens ?? 0).toLocaleString()} out tokens`
|
|
7204
|
+
);
|
|
6914
7205
|
}
|
|
6915
|
-
|
|
6916
|
-
|
|
7206
|
+
if (u.cost_usd !== void 0) parts.push(`$${u.cost_usd.toFixed(2)}`);
|
|
7207
|
+
if (u.cost_source)
|
|
7208
|
+
parts.push(`(${u.cost_source === "provider" ? "provider-reported" : u.cost_source})`);
|
|
7209
|
+
if (parts.length > 0) console.log(`usage: ${parts.join(" ")}`);
|
|
6917
7210
|
}
|
|
6918
|
-
if (run.
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
7211
|
+
if (run.provider_session_id) console.log(`provider session: ${run.provider_session_id}`);
|
|
7212
|
+
if (run.provider_url) console.log(`provider console: ${run.provider_url}`);
|
|
7213
|
+
if (run.error) console.log(`error: ${run.error}`);
|
|
7214
|
+
if (opts.logs) {
|
|
7215
|
+
console.log("");
|
|
7216
|
+
if (opts.full || opts.raw) {
|
|
7217
|
+
const res = await api.getRunLogFull(id, { raw: opts.raw });
|
|
7218
|
+
const body = res.body;
|
|
7219
|
+
if (!body) return;
|
|
7220
|
+
const reader = body.getReader();
|
|
7221
|
+
const decoder = new TextDecoder();
|
|
7222
|
+
for (; ; ) {
|
|
7223
|
+
const { done, value } = await reader.read();
|
|
7224
|
+
if (done) break;
|
|
7225
|
+
if (value) process.stdout.write(decoder.decode(value, { stream: true }));
|
|
7226
|
+
}
|
|
7227
|
+
process.stdout.write(decoder.decode());
|
|
7228
|
+
return;
|
|
7229
|
+
}
|
|
7230
|
+
if (run.log_bytes_dropped > 0) {
|
|
7231
|
+
console.log(
|
|
7232
|
+
`[${Math.round(run.log_bytes_dropped / 1024)} KB truncated from the head \u2014 ` + (run.log_expired ? "past its retention window; only this tail remains]" : `run \`tines runs show ${run.id} --logs --full\` for the complete ${Math.round(run.log_full_bytes / 1024)} KB log]`)
|
|
7233
|
+
);
|
|
7234
|
+
}
|
|
7235
|
+
console.log(run.log || "(no log output captured)");
|
|
6922
7236
|
}
|
|
6923
|
-
console.log(run.log || "(no log output captured)");
|
|
6924
7237
|
}
|
|
6925
|
-
|
|
7238
|
+
);
|
|
6926
7239
|
withCommon(
|
|
6927
7240
|
runsCmd.command("cancel <id>").description("Cancel a run (judged like any other end: usually a strike)")
|
|
6928
7241
|
).action(async (id, opts) => {
|
|
@@ -6965,7 +7278,7 @@ title template: ${s.title_template}`);
|
|
|
6965
7278
|
for (const line of s.description_template.split("\n")) console.log(` ${line}`);
|
|
6966
7279
|
}
|
|
6967
7280
|
}
|
|
6968
|
-
function
|
|
7281
|
+
function register8(program3) {
|
|
6969
7282
|
const schedules = program3.command("schedules").description("Manage scheduled tasks (addressed as <project>/<name>)");
|
|
6970
7283
|
withList(
|
|
6971
7284
|
schedules.command("list").description("List scheduled tasks (hides paused schedules unless --all)").option("-p, --project <name>", "filter by project name or id").option("-a, --all", "include paused schedules")
|
|
@@ -7016,7 +7329,9 @@ recent instances:`);
|
|
|
7016
7329
|
}
|
|
7017
7330
|
});
|
|
7018
7331
|
withCommon(
|
|
7019
|
-
schedules.command("edit <ref>").description(
|
|
7332
|
+
schedules.command("edit <ref>").description(
|
|
7333
|
+
"Edit a schedule: templates, workflow, start state, recurrence, timezone, gate, or name"
|
|
7334
|
+
).option("-t, --title <template>", "set the title template").option(
|
|
7020
7335
|
"-d, --description <markdown>",
|
|
7021
7336
|
`set the description template (Markdown) \u2014 ${BODY_VALUE_HELP}`
|
|
7022
7337
|
).option(
|
|
@@ -7025,7 +7340,13 @@ recent instances:`);
|
|
|
7025
7340
|
).option(
|
|
7026
7341
|
"-s, --state <id-or-name>",
|
|
7027
7342
|
"start state for future instances (the workflow's initial state = the default)"
|
|
7028
|
-
).option(
|
|
7343
|
+
).option(
|
|
7344
|
+
"--every <preset>",
|
|
7345
|
+
'repeat hourly (or every N hours: "6h"), daily, weekly, or monthly'
|
|
7346
|
+
).option(
|
|
7347
|
+
"--at <when>",
|
|
7348
|
+
"preset time of day HH:MM (default 09:00); for hourly, the minute past the hour :MM (default :00)"
|
|
7349
|
+
).option("--on <when>", "weekday (weekly) or day of month (monthly)").option("--cron <expr>", "5-field cron expression (alternative to --every/--at/--on)").option("--tz <iana>", "set the schedule timezone").option("--if-closed", "only create a new instance when all previous instances are closed").option("--no-if-closed", "clear the only-when-closed gate").option("--name <new-name>", "rename the schedule")
|
|
7029
7350
|
).action(
|
|
7030
7351
|
async (ref, opts) => {
|
|
7031
7352
|
const descriptionTemplate = opts.description !== void 0 ? readBodyValue(opts.description) : void 0;
|
|
@@ -7034,7 +7355,8 @@ recent instances:`);
|
|
|
7034
7355
|
const body = {};
|
|
7035
7356
|
if (opts.title !== void 0) body.title_template = opts.title;
|
|
7036
7357
|
if (descriptionTemplate !== void 0) body.description_template = descriptionTemplate;
|
|
7037
|
-
if (opts.workflow !== void 0)
|
|
7358
|
+
if (opts.workflow !== void 0)
|
|
7359
|
+
body.workflow_id = (await resolveWorkflow(api, opts.workflow)).id;
|
|
7038
7360
|
if (opts.state !== void 0) body.state = opts.state;
|
|
7039
7361
|
const recurrence = buildRecurrence(opts);
|
|
7040
7362
|
if (recurrence?.preset) body.preset = recurrence.preset;
|
|
@@ -7054,15 +7376,15 @@ recent instances:`);
|
|
|
7054
7376
|
printScheduleDetail(updated);
|
|
7055
7377
|
}
|
|
7056
7378
|
);
|
|
7057
|
-
withCommon(
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
}
|
|
7065
|
-
);
|
|
7379
|
+
withCommon(
|
|
7380
|
+
schedules.command("pause <ref>").description("Pause a schedule (keeps config and history)")
|
|
7381
|
+
).action(async (ref, opts) => {
|
|
7382
|
+
const api = client(opts);
|
|
7383
|
+
const schedule = await resolveSchedule(api, ref);
|
|
7384
|
+
const updated = await api.updateSchedule(schedule.id, { enabled: false });
|
|
7385
|
+
if (opts.json) return printJson(updated);
|
|
7386
|
+
console.log(`paused schedule "${scheduleRef(updated)}"`);
|
|
7387
|
+
});
|
|
7066
7388
|
withCommon(
|
|
7067
7389
|
schedules.command("resume <ref>").description("Resume a paused schedule (recomputes the next occurrence from now)")
|
|
7068
7390
|
).action(async (ref, opts) => {
|
|
@@ -7070,7 +7392,9 @@ recent instances:`);
|
|
|
7070
7392
|
const schedule = await resolveSchedule(api, ref);
|
|
7071
7393
|
const updated = await api.updateSchedule(schedule.id, { enabled: true });
|
|
7072
7394
|
if (opts.json) return printJson(updated);
|
|
7073
|
-
console.log(
|
|
7395
|
+
console.log(
|
|
7396
|
+
`resumed schedule "${scheduleRef(updated)}" \u2014 next run ${timestamp(updated.next_run_at)}`
|
|
7397
|
+
);
|
|
7074
7398
|
});
|
|
7075
7399
|
withCommon(
|
|
7076
7400
|
schedules.command("run <ref>").description("Create an instance now (respects the only-when-closed gate)")
|
|
@@ -7102,57 +7426,61 @@ recent instances:`);
|
|
|
7102
7426
|
}
|
|
7103
7427
|
|
|
7104
7428
|
// src/commands/supervisor.ts
|
|
7105
|
-
function
|
|
7429
|
+
function register9(program3) {
|
|
7106
7430
|
const supervisor = program3.command("supervisor").description("The automation kill switch, quota policy, and attempt limit");
|
|
7107
|
-
withCommon(
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
}
|
|
7119
|
-
const stateNames = /* @__PURE__ */ new Map();
|
|
7120
|
-
for (const wf of workflows.items) {
|
|
7121
|
-
for (const s of wf.states) stateNames.set(s.id, `${wf.name}/${s.name}`);
|
|
7122
|
-
}
|
|
7123
|
-
console.log(`automation: ${settings.enabled ? "ON" : "OFF (kill switch \u2014 nothing dispatches)"}`);
|
|
7124
|
-
console.log(quotaLabel(settings.quota, (id) => stateNames.get(id) ?? id));
|
|
7125
|
-
console.log(`utilization: ${utilizationLabel(settings.quota, activeRunItems, (id) => stateNames.get(id) ?? id)}`);
|
|
7126
|
-
console.log(`attempt limit: ${settings.attempt_limit} strikes, then the issue parks`);
|
|
7127
|
-
if (runnersRes.items.length === 0) {
|
|
7128
|
-
console.log("runners: none");
|
|
7129
|
-
} else {
|
|
7130
|
-
console.log("runners:");
|
|
7131
|
-
table(
|
|
7132
|
-
runnersRes.items.map((r) => [
|
|
7133
|
-
` ${r.name}`,
|
|
7134
|
-
r.type,
|
|
7135
|
-
runnerStatusLabel(r),
|
|
7136
|
-
`${r.active_runs}/${r.max_concurrent}`
|
|
7137
|
-
])
|
|
7138
|
-
);
|
|
7139
|
-
}
|
|
7431
|
+
withCommon(
|
|
7432
|
+
supervisor.command("status").description("One-screen overview: kill switch, quota, utilization, runners")
|
|
7433
|
+
).action(async (opts) => {
|
|
7434
|
+
const api = client(opts);
|
|
7435
|
+
const [settings, runnersRes, workflows, activeRunItems] = await Promise.all([
|
|
7436
|
+
api.getSupervisorSettings(),
|
|
7437
|
+
api.listRunners(),
|
|
7438
|
+
api.listWorkflows(),
|
|
7439
|
+
listAll((page) => api.listRuns({ active: true, ...page }))
|
|
7440
|
+
]);
|
|
7441
|
+
if (opts.json) {
|
|
7442
|
+
return printJson({ settings, runners: runnersRes.items, active_runs: activeRunItems });
|
|
7140
7443
|
}
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
const settings = await client(opts).updateSupervisorSettings({ enabled: true });
|
|
7145
|
-
if (opts.json) return printJson(settings);
|
|
7146
|
-
console.log("automation is ON \u2014 eligible issues with a matching rule will dispatch");
|
|
7444
|
+
const stateNames = /* @__PURE__ */ new Map();
|
|
7445
|
+
for (const wf of workflows.items) {
|
|
7446
|
+
for (const s of wf.states) stateNames.set(s.id, `${wf.name}/${s.name}`);
|
|
7147
7447
|
}
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7448
|
+
console.log(
|
|
7449
|
+
`automation: ${settings.enabled ? "ON" : "OFF (kill switch \u2014 nothing dispatches)"}`
|
|
7450
|
+
);
|
|
7451
|
+
console.log(quotaLabel(settings.quota, (id) => stateNames.get(id) ?? id));
|
|
7452
|
+
console.log(
|
|
7453
|
+
`utilization: ${utilizationLabel(settings.quota, activeRunItems, (id) => stateNames.get(id) ?? id)}`
|
|
7454
|
+
);
|
|
7455
|
+
console.log(`attempt limit: ${settings.attempt_limit} strikes, then the issue parks`);
|
|
7456
|
+
if (runnersRes.items.length === 0) {
|
|
7457
|
+
console.log("runners: none");
|
|
7458
|
+
} else {
|
|
7459
|
+
console.log("runners:");
|
|
7460
|
+
table(
|
|
7461
|
+
runnersRes.items.map((r) => [
|
|
7462
|
+
` ${r.name}`,
|
|
7463
|
+
r.type,
|
|
7464
|
+
runnerStatusLabel(r),
|
|
7465
|
+
`${r.active_runs}/${r.max_concurrent}`
|
|
7466
|
+
])
|
|
7467
|
+
);
|
|
7154
7468
|
}
|
|
7155
|
-
);
|
|
7469
|
+
});
|
|
7470
|
+
withCommon(
|
|
7471
|
+
supervisor.command("enable").description("Arm automation (the kill switch on)")
|
|
7472
|
+
).action(async (opts) => {
|
|
7473
|
+
const settings = await client(opts).updateSupervisorSettings({ enabled: true });
|
|
7474
|
+
if (opts.json) return printJson(settings);
|
|
7475
|
+
console.log("automation is ON \u2014 eligible issues with a matching rule will dispatch");
|
|
7476
|
+
});
|
|
7477
|
+
withCommon(
|
|
7478
|
+
supervisor.command("disable").description("Pause all automation at once (the kill switch off)")
|
|
7479
|
+
).action(async (opts) => {
|
|
7480
|
+
const settings = await client(opts).updateSupervisorSettings({ enabled: false });
|
|
7481
|
+
if (opts.json) return printJson(settings);
|
|
7482
|
+
console.log("automation is OFF \u2014 nothing new dispatches until re-enabled");
|
|
7483
|
+
});
|
|
7156
7484
|
const quota = supervisor.command("quota").description("Pick and configure the quota policy");
|
|
7157
7485
|
withCommon(
|
|
7158
7486
|
quota.command("global <n>").description("Use the global cap: at most <n> concurrent runs in total")
|
|
@@ -7165,7 +7493,11 @@ function register8(program3) {
|
|
|
7165
7493
|
console.log(quotaLabel(settings.quota));
|
|
7166
7494
|
});
|
|
7167
7495
|
withCommon(
|
|
7168
|
-
quota.command("roster").description("Use the per-state roster: at most N concurrent runs per workflow state").requiredOption(
|
|
7496
|
+
quota.command("roster").description("Use the per-state roster: at most N concurrent runs per workflow state").requiredOption(
|
|
7497
|
+
"--default <n>",
|
|
7498
|
+
"limit for states without an override",
|
|
7499
|
+
(v) => Number.parseInt(v, 10)
|
|
7500
|
+
).option(
|
|
7169
7501
|
"--state <workflow/state=n>",
|
|
7170
7502
|
"per-state override (repeatable), counted by the state a run started in",
|
|
7171
7503
|
collect,
|
|
@@ -7197,7 +7529,7 @@ function register8(program3) {
|
|
|
7197
7529
|
}
|
|
7198
7530
|
|
|
7199
7531
|
// src/commands/workflows.ts
|
|
7200
|
-
import { readFileSync as
|
|
7532
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7201
7533
|
function readJsonBody(inline, file) {
|
|
7202
7534
|
if (inline !== void 0 && file !== void 0) {
|
|
7203
7535
|
die("pass the JSON inline or with --file, not both");
|
|
@@ -7206,14 +7538,14 @@ function readJsonBody(inline, file) {
|
|
|
7206
7538
|
if (file !== void 0 && file !== "-") {
|
|
7207
7539
|
let raw;
|
|
7208
7540
|
try {
|
|
7209
|
-
raw =
|
|
7541
|
+
raw = readFileSync10(file, "utf8");
|
|
7210
7542
|
} catch (err) {
|
|
7211
7543
|
die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7212
7544
|
}
|
|
7213
7545
|
return parseJsonObject(raw, file);
|
|
7214
7546
|
}
|
|
7215
7547
|
if (file === "-" || !process.stdin.isTTY) {
|
|
7216
|
-
const raw =
|
|
7548
|
+
const raw = readFileSync10(0, "utf8");
|
|
7217
7549
|
if (raw.trim() === "") {
|
|
7218
7550
|
if (file === "-") die("no JSON on stdin");
|
|
7219
7551
|
return void 0;
|
|
@@ -7289,7 +7621,7 @@ function printWorkflowDetail(wf) {
|
|
|
7289
7621
|
for (const w of wf.warnings ?? []) console.log(`
|
|
7290
7622
|
warning: ${w}`);
|
|
7291
7623
|
}
|
|
7292
|
-
function
|
|
7624
|
+
function register10(program3) {
|
|
7293
7625
|
const workflows = program3.command("workflows").description("Manage the workflow library");
|
|
7294
7626
|
withList(workflows.command("list").description("List the workflow library")).action(
|
|
7295
7627
|
async (opts) => {
|
|
@@ -7319,22 +7651,26 @@ function register9(program3) {
|
|
|
7319
7651
|
printWorkflowDetail(wf);
|
|
7320
7652
|
});
|
|
7321
7653
|
withCommon(
|
|
7322
|
-
workflows.command("create [json]").description(
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7654
|
+
workflows.command("create [json]").description(
|
|
7655
|
+
'Create a workflow from a JSON definition (states carry initial "prompt" instructions)'
|
|
7656
|
+
).option("-f, --file <path>", 'read the JSON definition from a file ("-" for stdin)').option("--no-prompts", 'allow states without initial "prompt" instructions').addHelpText("after", WORKFLOW_JSON_HELP)
|
|
7657
|
+
).action(
|
|
7658
|
+
async (inline, opts) => {
|
|
7659
|
+
const body = readJsonBody(inline, opts.file);
|
|
7660
|
+
if (!body) {
|
|
7661
|
+
die(
|
|
7662
|
+
`missing workflow JSON: pass it inline, with --file <path>, or pipe it on stdin
|
|
7328
7663
|
see \`tines workflows create --help\` for the expected shape`
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7664
|
+
);
|
|
7665
|
+
}
|
|
7666
|
+
assertNewStatesHavePrompts(body.states, opts.prompts);
|
|
7667
|
+
const wf = await client(opts).createWorkflow(body);
|
|
7668
|
+
if (opts.json) return printJson(wf);
|
|
7669
|
+
console.log(`created workflow "${wf.name}" (${wf.id})
|
|
7335
7670
|
`);
|
|
7336
|
-
|
|
7337
|
-
|
|
7671
|
+
printWorkflowDetail(wf);
|
|
7672
|
+
}
|
|
7673
|
+
);
|
|
7338
7674
|
withCommon(
|
|
7339
7675
|
workflows.command("edit <id-or-name> [json]").description("Update a workflow from a JSON definition and/or flags").option("-f, --file <path>", 'read the JSON definition from a file ("-" for stdin)').option("-n, --name <name>", "rename the workflow").option("-d, --description <text>", "set the description").option("--initial-state <id-or-name>", "set the initial state").option("--no-prompts", 'allow new states without initial "prompt" instructions').addHelpText("after", WORKFLOW_JSON_HELP)
|
|
7340
7676
|
).action(
|
|
@@ -7371,14 +7707,15 @@ var program2 = new Command();
|
|
|
7371
7707
|
program2.name("tines").description("CLI for Tines").version(cliVersion()).enablePositionalOptions();
|
|
7372
7708
|
registerTime(program2);
|
|
7373
7709
|
register4(program2);
|
|
7374
|
-
|
|
7710
|
+
register5(program2);
|
|
7711
|
+
register10(program2);
|
|
7375
7712
|
register2(program2);
|
|
7376
7713
|
register(program2);
|
|
7377
7714
|
register3(program2);
|
|
7715
|
+
register8(program2);
|
|
7378
7716
|
register7(program2);
|
|
7379
7717
|
register6(program2);
|
|
7380
|
-
|
|
7381
|
-
register8(program2);
|
|
7718
|
+
register9(program2);
|
|
7382
7719
|
registerEvents(program2);
|
|
7383
7720
|
|
|
7384
7721
|
// src/index.ts
|