@agentsky/sky 2.18.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-MUHFB3LC.js → chunk-BHW2NAXP.js} +1 -1
- package/dist/{chunk-ZJVFWFOY.js → chunk-DC3R6BV2.js} +2777 -1150
- package/dist/{chunk-PL5KNPCX.js → chunk-ICSGMEG5.js} +2 -2
- package/dist/index.js +922 -27
- package/dist/products.d.ts +3 -0
- package/dist/products.js +1 -1
- package/dist/{src-TJJKOFXZ.js → src-3HXHE2S6.js} +1 -1
- package/dist/{trace-QLJH7VK6.js → trace-DI2SQI7M.js} +2 -2
- package/dist/{trace-library-FS5PCBJC.js → trace-library-4WWKCSVU.js} +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
resolveSessionRef,
|
|
10
10
|
streamEvents,
|
|
11
11
|
uploadFile
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-ICSGMEG5.js";
|
|
13
13
|
import {
|
|
14
14
|
CLI_VERSION,
|
|
15
15
|
USER_AGENT,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
currentUniverse,
|
|
21
21
|
loadConfig,
|
|
22
22
|
saveConfig
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-BHW2NAXP.js";
|
|
24
24
|
import {
|
|
25
25
|
AGENT_TYPES,
|
|
26
26
|
MODEL_SUBSCRIPTION_PLAN_PHRASE,
|
|
@@ -37,15 +37,16 @@ import {
|
|
|
37
37
|
modelSubscriptionProviderFor,
|
|
38
38
|
reasoningEffortDocLine,
|
|
39
39
|
redactLine,
|
|
40
|
+
redactPlainText,
|
|
40
41
|
redactSecretValues,
|
|
41
42
|
skillRefSchema,
|
|
42
43
|
splitMcpServers,
|
|
43
44
|
supersededBy
|
|
44
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-DC3R6BV2.js";
|
|
45
46
|
import "./chunk-MLKGABMK.js";
|
|
46
47
|
|
|
47
48
|
// src/index.ts
|
|
48
|
-
import { existsSync as existsSync10, readFileSync as readFileSync15, writeFileSync as
|
|
49
|
+
import { existsSync as existsSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
|
|
49
50
|
import { basename as basename8, resolve as resolve9 } from "path";
|
|
50
51
|
import { createInterface as createInterface6 } from "readline";
|
|
51
52
|
import { Command } from "commander";
|
|
@@ -4101,6 +4102,355 @@ function renderRequest(request) {
|
|
|
4101
4102
|
return `${request.id} ${request.kind} from ${request.session_id}
|
|
4102
4103
|
${request.question}${options}`;
|
|
4103
4104
|
}
|
|
4105
|
+
async function getSkillUsage(since) {
|
|
4106
|
+
const qs = since ? `?since=${encodeURIComponent(since)}` : "";
|
|
4107
|
+
return api("GET", `/skills/usage${qs}`);
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
// src/task-export.ts
|
|
4111
|
+
import { createHash as createHash6 } from "crypto";
|
|
4112
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
4113
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
4114
|
+
import { stringify as stringifyToml2 } from "smol-toml";
|
|
4115
|
+
var TEXT_MAX = 2e4;
|
|
4116
|
+
function scrub(text, names2) {
|
|
4117
|
+
let out = redactPlainText(text).text;
|
|
4118
|
+
for (const { name, alias } of names2) {
|
|
4119
|
+
if (!name) continue;
|
|
4120
|
+
out = out.split(name).join(alias);
|
|
4121
|
+
}
|
|
4122
|
+
return out.length > TEXT_MAX ? `${out.slice(0, TEXT_MAX)}
|
|
4123
|
+
\u2026[cut]` : out;
|
|
4124
|
+
}
|
|
4125
|
+
function textOf2(event) {
|
|
4126
|
+
if (typeof event.text === "string" && event.text.trim()) return event.text;
|
|
4127
|
+
if (Array.isArray(event.content)) {
|
|
4128
|
+
return event.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").filter(Boolean).join("\n");
|
|
4129
|
+
}
|
|
4130
|
+
return "";
|
|
4131
|
+
}
|
|
4132
|
+
function firstAsk(record) {
|
|
4133
|
+
const root = record.threads.find((thread2) => thread2.relation === "primary") ?? record.threads[0];
|
|
4134
|
+
if (!root) return null;
|
|
4135
|
+
for (const event of record.events[root.id] ?? []) {
|
|
4136
|
+
if (event.type === "user.message") {
|
|
4137
|
+
const text = textOf2(event).trim();
|
|
4138
|
+
if (text) return text;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
return null;
|
|
4142
|
+
}
|
|
4143
|
+
function decisionsOf(record) {
|
|
4144
|
+
const out = [];
|
|
4145
|
+
for (const thread2 of record.threads) {
|
|
4146
|
+
for (const event of record.events[thread2.id] ?? []) {
|
|
4147
|
+
if (event.type === "human.decision" || event.type === "outcome.observed") out.push({ ...event, thread_id: thread2.id });
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
return out.sort((a, b) => (a.at ?? "").localeCompare(b.at ?? ""));
|
|
4151
|
+
}
|
|
4152
|
+
function adoptedSession(record) {
|
|
4153
|
+
const decisions = decisionsOf(record).filter((event) => event.type === "human.decision");
|
|
4154
|
+
for (const event of [...decisions].reverse()) {
|
|
4155
|
+
if (event.kind === "adopt") return event.target_session_id ?? event.thread_id;
|
|
4156
|
+
if (event.kind === "vote" && event.target_session_id) return event.target_session_id;
|
|
4157
|
+
}
|
|
4158
|
+
return null;
|
|
4159
|
+
}
|
|
4160
|
+
function stepsOf(record, names2) {
|
|
4161
|
+
const root = record.threads.find((thread2) => thread2.relation === "primary") ?? record.threads[0];
|
|
4162
|
+
if (!root) return [];
|
|
4163
|
+
const events = record.events[root.id] ?? [];
|
|
4164
|
+
const steps = [];
|
|
4165
|
+
events.forEach((event, index) => {
|
|
4166
|
+
if (event.type !== "human.decision" || event.kind !== "revise" && event.kind !== "rerun") return;
|
|
4167
|
+
const next = events.slice(index + 1).find((later) => later.type === "user.message" && textOf2(later).trim());
|
|
4168
|
+
const words = next ? textOf2(next) : event.reason ?? "";
|
|
4169
|
+
if (!words.trim()) return;
|
|
4170
|
+
steps.push({ at: event.at ?? "", instruction: scrub(words, names2) });
|
|
4171
|
+
});
|
|
4172
|
+
return steps;
|
|
4173
|
+
}
|
|
4174
|
+
function rubricOf(record, instruction, names2) {
|
|
4175
|
+
if (record.goal && record.goal.criteria.length) {
|
|
4176
|
+
const verdicts = new Map((record.goal.last_evaluation?.criteria ?? []).map((row) => [row.id, row.verdict]));
|
|
4177
|
+
return {
|
|
4178
|
+
version: 1,
|
|
4179
|
+
source: "goal",
|
|
4180
|
+
criteria: record.goal.criteria.map((criterion) => ({
|
|
4181
|
+
id: criterion.id,
|
|
4182
|
+
text: scrub(criterion.text, names2),
|
|
4183
|
+
must: true,
|
|
4184
|
+
source: `goal criterion ${criterion.id}`,
|
|
4185
|
+
...criterion.command ? { command: criterion.command } : {},
|
|
4186
|
+
...verdicts.has(criterion.id) ? { last_verdict: verdicts.get(criterion.id) } : {}
|
|
4187
|
+
}))
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
return {
|
|
4191
|
+
version: 1,
|
|
4192
|
+
source: "instruction",
|
|
4193
|
+
criteria: [{ id: "c1", text: `The deliverable does what the instruction asks: ${instruction.split("\n")[0]?.slice(0, 300) ?? ""}`, must: true, source: "derived from the instruction; no goal was set" }]
|
|
4194
|
+
};
|
|
4195
|
+
}
|
|
4196
|
+
var JUDGE_PY = String.raw`#!/usr/bin/env python3
|
|
4197
|
+
"""Harbor verifier: grade the agent's output against tests/rubric.json with an LLM judge.
|
|
4198
|
+
|
|
4199
|
+
Reads every file under /workspace/outputs (falling back to /workspace) and asks the judge
|
|
4200
|
+
model, criterion by criterion, for a yes/no with quoted evidence. Writes
|
|
4201
|
+
/logs/verifier/reward.json ({"reward": 0|1, "criteria": [...]}) and reward.txt.
|
|
4202
|
+
Model and key come from the environment (JUDGE_MODEL, JUDGE_API_KEY, JUDGE_BASE_URL);
|
|
4203
|
+
nothing secret is in this file. Reward is 1 only when every MUST criterion passes.
|
|
4204
|
+
"""
|
|
4205
|
+
import json, os, pathlib, sys, urllib.request
|
|
4206
|
+
|
|
4207
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
4208
|
+
RUBRIC = json.loads((HERE / "rubric.json").read_text())
|
|
4209
|
+
OUT_DIR = pathlib.Path(os.environ.get("HARBOR_OUTPUTS", "/workspace/outputs"))
|
|
4210
|
+
if not OUT_DIR.exists():
|
|
4211
|
+
OUT_DIR = pathlib.Path("/workspace")
|
|
4212
|
+
LOGS = pathlib.Path(os.environ.get("HARBOR_VERIFIER_LOGS", "/logs/verifier"))
|
|
4213
|
+
LOGS.mkdir(parents=True, exist_ok=True)
|
|
4214
|
+
|
|
4215
|
+
def collect(limit=200_000):
|
|
4216
|
+
parts, size = [], 0
|
|
4217
|
+
for path in sorted(p for p in OUT_DIR.rglob("*") if p.is_file()):
|
|
4218
|
+
if any(s in path.parts for s in (".git", "node_modules", "__pycache__")):
|
|
4219
|
+
continue
|
|
4220
|
+
try:
|
|
4221
|
+
text = path.read_text(errors="replace")
|
|
4222
|
+
except OSError:
|
|
4223
|
+
continue
|
|
4224
|
+
chunk = f"\n===== {path.relative_to(OUT_DIR)} =====\n{text}"
|
|
4225
|
+
if size + len(chunk) > limit:
|
|
4226
|
+
break
|
|
4227
|
+
parts.append(chunk); size += len(chunk)
|
|
4228
|
+
return "".join(parts)
|
|
4229
|
+
|
|
4230
|
+
def ask(prompt):
|
|
4231
|
+
base = os.environ.get("JUDGE_BASE_URL", "https://api.openai.com/v1").rstrip("/")
|
|
4232
|
+
body = json.dumps({"model": os.environ.get("JUDGE_MODEL", "gpt-5.5"), "temperature": 0,
|
|
4233
|
+
"messages": [{"role": "user", "content": prompt}]}).encode()
|
|
4234
|
+
req = urllib.request.Request(f"{base}/chat/completions", data=body, headers={
|
|
4235
|
+
"content-type": "application/json", "authorization": f"Bearer {os.environ['JUDGE_API_KEY']}"})
|
|
4236
|
+
with urllib.request.urlopen(req, timeout=120) as res:
|
|
4237
|
+
return json.load(res)["choices"][0]["message"]["content"]
|
|
4238
|
+
|
|
4239
|
+
def grade(criterion, evidence):
|
|
4240
|
+
prompt = (
|
|
4241
|
+
"You grade one requirement of a task against the agent's output.\n"
|
|
4242
|
+
f"Requirement: {criterion['text']}\n"
|
|
4243
|
+
"Answer as JSON only: {\"pass\": true|false, \"evidence\": \"<quote from the output, or why it is missing>\"}.\n"
|
|
4244
|
+
"Pass only when the output itself shows the requirement met.\n\nOUTPUT:\n" + evidence)
|
|
4245
|
+
raw = ask(prompt).strip()
|
|
4246
|
+
start, end = raw.find("{"), raw.rfind("}")
|
|
4247
|
+
verdict = json.loads(raw[start:end + 1]) if start >= 0 else {"pass": False, "evidence": raw[:300]}
|
|
4248
|
+
return {"id": criterion["id"], "must": criterion.get("must", True), "pass": bool(verdict.get("pass")), "evidence": str(verdict.get("evidence", ""))[:1000]}
|
|
4249
|
+
|
|
4250
|
+
def main():
|
|
4251
|
+
evidence = collect()
|
|
4252
|
+
results = [grade(c, evidence) for c in RUBRIC["criteria"]]
|
|
4253
|
+
reward = 1 if results and all(r["pass"] for r in results if r["must"]) else 0
|
|
4254
|
+
(LOGS / "reward.json").write_text(json.dumps({"reward": reward, "criteria": results}, indent=2))
|
|
4255
|
+
(LOGS / "reward.txt").write_text(f"{reward}\n")
|
|
4256
|
+
print(json.dumps({"reward": reward, "passed": sum(r["pass"] for r in results), "total": len(results)}))
|
|
4257
|
+
return 0
|
|
4258
|
+
|
|
4259
|
+
if __name__ == "__main__":
|
|
4260
|
+
sys.exit(main())
|
|
4261
|
+
`;
|
|
4262
|
+
var TEST_SH = `#!/usr/bin/env bash
|
|
4263
|
+
# Harbor verifier entry point: the judge reads tests/rubric.json and writes /logs/verifier/reward.json.
|
|
4264
|
+
set -euo pipefail
|
|
4265
|
+
cd "$(dirname "$0")"
|
|
4266
|
+
exec python3 judge.py
|
|
4267
|
+
`;
|
|
4268
|
+
var SOLVE_SH = `#!/usr/bin/env bash
|
|
4269
|
+
# Reference solution: the files the task's sessions produced, as the person left them.
|
|
4270
|
+
set -euo pipefail
|
|
4271
|
+
cd "$(dirname "$0")"
|
|
4272
|
+
mkdir -p /workspace/outputs
|
|
4273
|
+
if [ -d files ]; then cp -R files/. /workspace/outputs/; fi
|
|
4274
|
+
`;
|
|
4275
|
+
function buildHarborTask(record, opts) {
|
|
4276
|
+
const names2 = opts.redactNames;
|
|
4277
|
+
const out = /* @__PURE__ */ new Map();
|
|
4278
|
+
const ask2 = record.goal?.objective ?? firstAsk(record) ?? record.title ?? "Complete the task described by the workspace.";
|
|
4279
|
+
const instruction = scrub(ask2, names2);
|
|
4280
|
+
const root = record.threads.find((thread2) => thread2.relation === "primary") ?? record.threads[0];
|
|
4281
|
+
const harnesses = [...new Set(record.threads.map((thread2) => thread2.agent.harness))];
|
|
4282
|
+
const decisions = decisionsOf(record);
|
|
4283
|
+
const adopted = adoptedSession(record);
|
|
4284
|
+
const steps = stepsOf(record, names2);
|
|
4285
|
+
const idHash = createHash6("sha256").update(record.task_id).digest("hex").slice(0, 16);
|
|
4286
|
+
out.set("instruction.md", `${instruction.trim()}
|
|
4287
|
+
|
|
4288
|
+
Write your deliverables under /workspace/outputs.
|
|
4289
|
+
`);
|
|
4290
|
+
out.set(
|
|
4291
|
+
"task.toml",
|
|
4292
|
+
stringifyToml2({
|
|
4293
|
+
metadata: {
|
|
4294
|
+
name: `agentsky-${idHash}`,
|
|
4295
|
+
tier: opts.tier,
|
|
4296
|
+
source: "agentsky-session-record",
|
|
4297
|
+
source_task: idHash,
|
|
4298
|
+
exported_at: record.exported_at,
|
|
4299
|
+
harnesses,
|
|
4300
|
+
has_goal: Boolean(record.goal),
|
|
4301
|
+
decisions: decisions.length,
|
|
4302
|
+
steps: steps.length,
|
|
4303
|
+
adopted_session: adopted ?? "",
|
|
4304
|
+
did_the_job: record.completion_answer?.did_the_job ?? "unknown"
|
|
4305
|
+
},
|
|
4306
|
+
verifier: { kind: "rubric_judge", rubric: "tests/rubric.json", reward: "/logs/verifier/reward.json" }
|
|
4307
|
+
})
|
|
4308
|
+
);
|
|
4309
|
+
const env = record.environment ?? root?.environment ?? null;
|
|
4310
|
+
if (env && typeof env.repo === "string" && typeof env.commit === "string") {
|
|
4311
|
+
out.set(
|
|
4312
|
+
"environment/Dockerfile",
|
|
4313
|
+
`FROM node:22-bookworm
|
|
4314
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git python3 && rm -rf /var/lib/apt/lists/*
|
|
4315
|
+
WORKDIR /workspace
|
|
4316
|
+
RUN git clone ${env.repo} repo && cd repo && git checkout ${env.commit}
|
|
4317
|
+
`
|
|
4318
|
+
);
|
|
4319
|
+
} else {
|
|
4320
|
+
out.set(
|
|
4321
|
+
"environment/README.md",
|
|
4322
|
+
"This task is a real execution, not a resettable environment: the record carries no repository and commit to rebuild from.\nAuthor environment/Dockerfile by hand from solution/files (the task's own files at export) before running it with Harbor, or keep it as trajectory data.\n"
|
|
4323
|
+
);
|
|
4324
|
+
}
|
|
4325
|
+
const rubric = rubricOf(record, instruction, names2);
|
|
4326
|
+
out.set("tests/rubric.json", `${JSON.stringify(rubric, null, 2)}
|
|
4327
|
+
`);
|
|
4328
|
+
out.set("tests/judge.py", JUDGE_PY);
|
|
4329
|
+
out.set("tests/test.sh", TEST_SH);
|
|
4330
|
+
out.set("solution/solve.sh", SOLVE_SH);
|
|
4331
|
+
out.set(
|
|
4332
|
+
"solution/deliverables.json",
|
|
4333
|
+
`${JSON.stringify({ files: record.files.map((file) => ({ path: file.path, bytes: file.size_bytes ?? null })), adopted_session: adopted }, null, 2)}
|
|
4334
|
+
`
|
|
4335
|
+
);
|
|
4336
|
+
for (const [path, bytes] of opts.fileBytes ?? []) out.set(`solution/files/${path}`, bytes);
|
|
4337
|
+
const scrubbedEvents = {};
|
|
4338
|
+
for (const [threadId, events] of Object.entries(record.events)) {
|
|
4339
|
+
scrubbedEvents[threadId] = events.map((event) => ({
|
|
4340
|
+
...event,
|
|
4341
|
+
...typeof event.text === "string" ? { text: scrub(event.text, names2) } : {},
|
|
4342
|
+
...Array.isArray(event.content) ? { content: event.content.map((block) => typeof block.text === "string" ? { ...block, text: scrub(block.text, names2) } : block) } : {},
|
|
4343
|
+
...typeof event.reason === "string" ? { reason: scrub(event.reason, names2) } : {},
|
|
4344
|
+
..."parts" in event ? { parts: void 0 } : {}
|
|
4345
|
+
}));
|
|
4346
|
+
}
|
|
4347
|
+
out.set("rollout/record.json", `${JSON.stringify({ ...record, title: record.title ? scrub(record.title, names2) : null, events: scrubbedEvents, goal: record.goal ? { ...record.goal, objective: scrub(record.goal.objective, names2) } : null }, null, 2)}
|
|
4348
|
+
`);
|
|
4349
|
+
out.set("rollout/decisions.json", `${JSON.stringify(decisions.map((event) => ({ ...event, reason: typeof event.reason === "string" ? scrub(event.reason, names2) : event.reason })), null, 2)}
|
|
4350
|
+
`);
|
|
4351
|
+
steps.forEach((step, index) => {
|
|
4352
|
+
out.set(`steps/step-${index + 1}/instruction.md`, `${step.instruction.trim()}
|
|
4353
|
+
`);
|
|
4354
|
+
});
|
|
4355
|
+
return out;
|
|
4356
|
+
}
|
|
4357
|
+
async function pagedEvents(sessionId) {
|
|
4358
|
+
const events = [];
|
|
4359
|
+
let cursor = null;
|
|
4360
|
+
for (; ; ) {
|
|
4361
|
+
const query = new URLSearchParams({ limit: "500" });
|
|
4362
|
+
if (cursor) query.set("cursor", cursor);
|
|
4363
|
+
const page = await api("GET", `/sessions/${encodeURIComponent(sessionId)}/events?${query}`);
|
|
4364
|
+
events.push(...page.events);
|
|
4365
|
+
if (!page.hasMore || !page.cursor) return events;
|
|
4366
|
+
cursor = page.cursor;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
async function fetchTaskRecord(taskRef) {
|
|
4370
|
+
const { task: task2 } = await api("GET", `/tasks/${encodeURIComponent(taskRef)}`);
|
|
4371
|
+
const { threads } = await api("GET", `/sessions/${encodeURIComponent(task2.id)}/threads`);
|
|
4372
|
+
const events = {};
|
|
4373
|
+
for (const thread2 of threads) events[thread2.id] = await pagedEvents(thread2.id);
|
|
4374
|
+
let goal2 = null;
|
|
4375
|
+
try {
|
|
4376
|
+
goal2 = (await api("GET", `/sessions/${encodeURIComponent(task2.id)}/goal`)).goal;
|
|
4377
|
+
} catch (error) {
|
|
4378
|
+
if (!(error instanceof ApiError && error.status === 404)) throw error;
|
|
4379
|
+
}
|
|
4380
|
+
let files2 = [];
|
|
4381
|
+
try {
|
|
4382
|
+
files2 = await listTaskFiles(task2.id);
|
|
4383
|
+
} catch (error) {
|
|
4384
|
+
if (!(error instanceof ApiError && (error.status === 404 || error.status === 503))) throw error;
|
|
4385
|
+
}
|
|
4386
|
+
const root = threads.find((thread2) => thread2.relation === "primary") ?? threads[0];
|
|
4387
|
+
return { task_id: task2.id, exported_at: (/* @__PURE__ */ new Date()).toISOString(), title: task2.title ?? null, threads, events, goal: goal2, files: files2, completion_answer: task2.completion_answer ?? null, environment: root?.environment ?? null };
|
|
4388
|
+
}
|
|
4389
|
+
var FILE_BYTES_MAX = 50 * 1024 * 1024;
|
|
4390
|
+
async function exportTaskHarbor(taskRef, outDir, opts) {
|
|
4391
|
+
const record = await fetchTaskRecord(taskRef);
|
|
4392
|
+
const fileBytes = /* @__PURE__ */ new Map();
|
|
4393
|
+
let total = 0;
|
|
4394
|
+
for (const file of record.files) {
|
|
4395
|
+
if (total + (file.size_bytes ?? 0) > FILE_BYTES_MAX) break;
|
|
4396
|
+
const bytes = await readTaskFile(record.task_id, file.path);
|
|
4397
|
+
total += bytes.byteLength;
|
|
4398
|
+
fileBytes.set(file.path, bytes);
|
|
4399
|
+
}
|
|
4400
|
+
const files2 = buildHarborTask(record, { ...opts, fileBytes });
|
|
4401
|
+
const written = [];
|
|
4402
|
+
for (const [path, content] of files2) {
|
|
4403
|
+
const target = join15(outDir, path);
|
|
4404
|
+
mkdirSync4(dirname7(target), { recursive: true });
|
|
4405
|
+
writeFileSync6(target, content, typeof content === "string" ? { mode: path.endsWith(".sh") || path.endsWith(".py") ? 493 : 420 } : void 0);
|
|
4406
|
+
written.push(path);
|
|
4407
|
+
}
|
|
4408
|
+
return { written, record };
|
|
4409
|
+
}
|
|
4410
|
+
function parseRedactNames(values) {
|
|
4411
|
+
return (values ?? []).map((value) => {
|
|
4412
|
+
const index = value.indexOf("=");
|
|
4413
|
+
if (index < 0) return { name: value.trim(), alias: "[redacted]" };
|
|
4414
|
+
return { name: value.slice(0, index).trim(), alias: value.slice(index + 1).trim() || "[redacted]" };
|
|
4415
|
+
}).filter((pair) => pair.name);
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
// src/task-record.ts
|
|
4419
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4420
|
+
var DECISION_KINDS = ["adopt", "rerun", "revise", "discard", "vote", "did_the_job", "approve"];
|
|
4421
|
+
async function listThreads(taskRef) {
|
|
4422
|
+
return (await api("GET", `/sessions/${encodeURIComponent(taskRef)}/threads`)).threads;
|
|
4423
|
+
}
|
|
4424
|
+
function renderThread(thread2) {
|
|
4425
|
+
const spend = (Number(thread2.usage.spent_usd_micros) / 1e6).toFixed(4);
|
|
4426
|
+
const env = thread2.environment?.repo ? ` ${thread2.environment.repo}${thread2.environment.commit ? `@${thread2.environment.commit.slice(0, 7)}` : ""}` : "";
|
|
4427
|
+
return `${thread2.id} ${thread2.relation.padEnd(7)} ${thread2.source.padEnd(12)} ${thread2.agent.harness}/${thread2.agent.model} ${thread2.status} $${spend}${env}`;
|
|
4428
|
+
}
|
|
4429
|
+
async function recordDecision(sessionRef, kind, opts = {}) {
|
|
4430
|
+
if (!DECISION_KINDS.includes(kind)) throw new Error(`kind must be one of ${DECISION_KINDS.join(", ")}`);
|
|
4431
|
+
if ((kind === "did_the_job" || kind === "approve") && typeof opts.value !== "boolean") throw new Error(`${kind} needs --yes or --no`);
|
|
4432
|
+
const key = opts.idempotencyKey ?? randomUUID2();
|
|
4433
|
+
const res = await fetchRecord(sessionRef, {
|
|
4434
|
+
type: "human.decision",
|
|
4435
|
+
payload: {
|
|
4436
|
+
kind,
|
|
4437
|
+
...opts.target ? { target_session_id: opts.target } : {},
|
|
4438
|
+
...typeof opts.value === "boolean" ? { value: opts.value } : {},
|
|
4439
|
+
...opts.reason ? { reason: opts.reason } : {},
|
|
4440
|
+
surface: opts.surface ?? "cli"
|
|
4441
|
+
}
|
|
4442
|
+
}, key);
|
|
4443
|
+
return res;
|
|
4444
|
+
}
|
|
4445
|
+
var OUTCOME_KINDS = ["pr_merged", "deployed", "reopened", "later_failure", "shipped"];
|
|
4446
|
+
async function recordOutcome(sessionRef, kind, opts = {}) {
|
|
4447
|
+
if (!OUTCOME_KINDS.includes(kind)) throw new Error(`kind must be one of ${OUTCOME_KINDS.join(", ")}`);
|
|
4448
|
+
return fetchRecord(sessionRef, { type: "outcome.observed", payload: { kind, ...opts.ref ? { ref: opts.ref } : {}, observer: opts.observer ?? "cli" } }, opts.idempotencyKey ?? randomUUID2());
|
|
4449
|
+
}
|
|
4450
|
+
async function fetchRecord(sessionRef, body, idempotencyKey) {
|
|
4451
|
+
const result = await api("POST", `/sessions/${encodeURIComponent(sessionRef)}/record`, body, { idempotencyKey });
|
|
4452
|
+
return { event: result.event, created: true };
|
|
4453
|
+
}
|
|
4104
4454
|
|
|
4105
4455
|
// src/workspace.ts
|
|
4106
4456
|
import { createInterface as createInterface4 } from "readline";
|
|
@@ -4321,12 +4671,77 @@ function confirmPrompt(question) {
|
|
|
4321
4671
|
});
|
|
4322
4672
|
}
|
|
4323
4673
|
|
|
4674
|
+
// src/browser-profiles.ts
|
|
4675
|
+
async function listBrowserProfiles() {
|
|
4676
|
+
const { data } = await api("GET", "/browser_profiles");
|
|
4677
|
+
return data;
|
|
4678
|
+
}
|
|
4679
|
+
async function getBrowserProfile(scope) {
|
|
4680
|
+
const { profile } = await api("GET", `/browser_profiles/${encodeURIComponent(scope)}`);
|
|
4681
|
+
return profile;
|
|
4682
|
+
}
|
|
4683
|
+
async function importBrowserProfileState(scope, state) {
|
|
4684
|
+
return api("POST", `/browser_profiles/${encodeURIComponent(scope)}/imports`, state);
|
|
4685
|
+
}
|
|
4686
|
+
async function listBrowserProfileSites(scope) {
|
|
4687
|
+
const profile = await getBrowserProfile(scope);
|
|
4688
|
+
return profile.sites;
|
|
4689
|
+
}
|
|
4690
|
+
async function removeBrowserProfileSite(scope, site) {
|
|
4691
|
+
await api("DELETE", `/browser_profiles/${encodeURIComponent(scope)}/sites/${encodeURIComponent(site)}`);
|
|
4692
|
+
}
|
|
4693
|
+
async function moveBrowserProfileSite(scope, site, to) {
|
|
4694
|
+
return api("POST", `/browser_profiles/${encodeURIComponent(scope)}/sites/${encodeURIComponent(site)}/move`, { to });
|
|
4695
|
+
}
|
|
4696
|
+
function renderBrowserProfileSite(site) {
|
|
4697
|
+
const who = site.signed_in_by?.email ?? site.signed_in_by?.name ?? "unknown";
|
|
4698
|
+
const used = site.last_used_at ? `used ${site.last_used_at.slice(0, 10)}` : "never used";
|
|
4699
|
+
return `${site.site.padEnd(40)} ${site.status.padEnd(14)} ${who} ${used}`;
|
|
4700
|
+
}
|
|
4701
|
+
function renderBrowserProfile(profile) {
|
|
4702
|
+
const lines = [`scope: ${profile.scope} revision: ${profile.revision} sites: ${profile.sites.length}`];
|
|
4703
|
+
for (const site of profile.sites) lines.push(` ${renderBrowserProfileSite(site)}`);
|
|
4704
|
+
return lines.join("\n");
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
// src/compare.ts
|
|
4708
|
+
async function listComparisons(opts = {}) {
|
|
4709
|
+
const qs = opts.archived ? "?archived=true" : "";
|
|
4710
|
+
const { comparisons } = await api("GET", `/comparisons${qs}`);
|
|
4711
|
+
return comparisons;
|
|
4712
|
+
}
|
|
4713
|
+
async function createComparison(body) {
|
|
4714
|
+
return api("POST", "/comparisons", body);
|
|
4715
|
+
}
|
|
4716
|
+
async function getComparison(id) {
|
|
4717
|
+
return api("GET", `/comparisons/${encodeURIComponent(id)}`);
|
|
4718
|
+
}
|
|
4719
|
+
async function pickWinner(id, opts) {
|
|
4720
|
+
return api("POST", `/comparisons/${encodeURIComponent(id)}/pick`, opts);
|
|
4721
|
+
}
|
|
4722
|
+
async function listThreads2(opts = {}) {
|
|
4723
|
+
const qs = opts.filter ? `?filter=${opts.filter}` : "";
|
|
4724
|
+
const { threads } = await api("GET", `/threads${qs}`);
|
|
4725
|
+
return threads;
|
|
4726
|
+
}
|
|
4727
|
+
function renderComparisonSummary(c) {
|
|
4728
|
+
const agents = c.agents.map((a) => `${a.agentType}/${a.llm}`).join(" vs ");
|
|
4729
|
+
const status = c.statusLine ?? c.attention ?? (c.archived ? "archived" : "active");
|
|
4730
|
+
return `${c.id.slice(0, 12)} ${c.title.slice(0, 40).padEnd(42)} [${agents}] ${status}`;
|
|
4731
|
+
}
|
|
4732
|
+
function renderThreadSummary(t) {
|
|
4733
|
+
const agents = t.agents.map((a) => `${a.agentType}/${a.llm}`).join(", ");
|
|
4734
|
+
const flag = t.pinned ? "\u{1F4CC} " : "";
|
|
4735
|
+
const status = t.working ? "working" : t.attention ?? (t.archived ? "archived" : "idle");
|
|
4736
|
+
return `${t.id.slice(0, 12)} ${flag}${t.title.slice(0, 40).padEnd(44)} [${agents}] ${status}`;
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4324
4739
|
// src/harness.ts
|
|
4325
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
4326
|
-
import { dirname as
|
|
4740
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
|
|
4741
|
+
import { dirname as dirname8, join as join16, resolve as resolve8 } from "path";
|
|
4327
4742
|
var HARNESS_FILE = "harness.json";
|
|
4328
4743
|
function harnessFilePath(dir) {
|
|
4329
|
-
return
|
|
4744
|
+
return join16(resolve8(dir), HARNESS_FILE);
|
|
4330
4745
|
}
|
|
4331
4746
|
function readHarnessFile(dir) {
|
|
4332
4747
|
const path = harnessFilePath(dir);
|
|
@@ -4364,8 +4779,8 @@ function harnessInit(dir) {
|
|
|
4364
4779
|
if (existsSync9(path)) {
|
|
4365
4780
|
throw new Error(`${path} already exists \u2014 delete it to re-init`);
|
|
4366
4781
|
}
|
|
4367
|
-
|
|
4368
|
-
|
|
4782
|
+
mkdirSync5(dirname8(path), { recursive: true });
|
|
4783
|
+
writeFileSync7(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
|
|
4369
4784
|
`);
|
|
4370
4785
|
console.log(`Created ${path}`);
|
|
4371
4786
|
console.log(INIT_COMMENT);
|
|
@@ -5070,7 +5485,7 @@ sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1
|
|
|
5070
5485
|
run(async (ref, path, opts) => {
|
|
5071
5486
|
const bytes = await readFile(await resolveSessionRef(ref), path);
|
|
5072
5487
|
if (opts.out) {
|
|
5073
|
-
|
|
5488
|
+
writeFileSync8(opts.out, bytes);
|
|
5074
5489
|
console.error(`saved ${bytes.length} bytes to ${opts.out}`);
|
|
5075
5490
|
} else process.stdout.write(bytes);
|
|
5076
5491
|
})
|
|
@@ -5087,7 +5502,7 @@ sessionWorkspace.command("patch <ref> [repo]").description("the work in a reposi
|
|
|
5087
5502
|
console.error(`${meta.repo}: ${meta.commits.length} commit${meta.commits.length === 1 ? "" : "s"} since ${meta.base_branch || "the imported copy"} (${meta.base_commit.slice(0, 12)})`);
|
|
5088
5503
|
if (!patch.length) console.error("nothing changed");
|
|
5089
5504
|
else if (opts.out) {
|
|
5090
|
-
|
|
5505
|
+
writeFileSync8(opts.out, patch);
|
|
5091
5506
|
console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
|
|
5092
5507
|
} else process.stdout.write(patch);
|
|
5093
5508
|
})
|
|
@@ -5098,6 +5513,43 @@ sessionWorkspace.command("import <ref> <dir>").description("copy a local folder
|
|
|
5098
5513
|
console.log(`${imported.path} (${imported.files} files)`);
|
|
5099
5514
|
})
|
|
5100
5515
|
);
|
|
5516
|
+
var sessionBrowser = session.command("browser").description("the sandbox browser for a session (start, show status, stop)");
|
|
5517
|
+
sessionBrowser.command("show <ref>").description("show the sandbox browser status for a session").option("--json", "print as JSON").action(
|
|
5518
|
+
run(async (ref, opts) => {
|
|
5519
|
+
const id = await resolveSessionRef(ref);
|
|
5520
|
+
const { browser } = await api("GET", `/sessions/${encodeURIComponent(id)}/browser`);
|
|
5521
|
+
if (opts.json) {
|
|
5522
|
+
console.log(JSON.stringify(browser, null, 2));
|
|
5523
|
+
return;
|
|
5524
|
+
}
|
|
5525
|
+
if (!browser) {
|
|
5526
|
+
console.log("no browser");
|
|
5527
|
+
return;
|
|
5528
|
+
}
|
|
5529
|
+
const b = browser;
|
|
5530
|
+
console.log(`${b.status ?? "unknown"}${b.url ? ` ${b.url}` : ""}${b.entry_url ? ` entry: ${b.entry_url}` : ""}`);
|
|
5531
|
+
})
|
|
5532
|
+
);
|
|
5533
|
+
sessionBrowser.command("start <ref>").description("start a browser for a session (provisions it if not running)").option("--json", "print as JSON").action(
|
|
5534
|
+
run(async (ref, opts) => {
|
|
5535
|
+
const id = await resolveSessionRef(ref);
|
|
5536
|
+
const { browser } = await api("POST", `/sessions/${encodeURIComponent(id)}/browser`, {});
|
|
5537
|
+
if (opts.json) {
|
|
5538
|
+
console.log(JSON.stringify(browser, null, 2));
|
|
5539
|
+
return;
|
|
5540
|
+
}
|
|
5541
|
+
const b = browser;
|
|
5542
|
+
console.log(`${b.status ?? "starting"}${b.entry_url ? ` entry: ${b.entry_url}` : ""}`);
|
|
5543
|
+
})
|
|
5544
|
+
);
|
|
5545
|
+
sessionBrowser.command("stop <ref>").description("stop the browser for a session").action(
|
|
5546
|
+
run(async (ref) => {
|
|
5547
|
+
const id = await resolveSessionRef(ref);
|
|
5548
|
+
const { browser } = await api("DELETE", `/sessions/${encodeURIComponent(id)}/browser`);
|
|
5549
|
+
const b = browser;
|
|
5550
|
+
console.log(b ? b.status ?? "stopped" : "stopped");
|
|
5551
|
+
})
|
|
5552
|
+
);
|
|
5101
5553
|
function interactionMode(opts) {
|
|
5102
5554
|
if (opts.plan && opts.build) throw new ApiError(400, "invalid_request", "pass --plan or --build, not both");
|
|
5103
5555
|
return opts.plan ? "plan" : opts.build ? "build" : void 0;
|
|
@@ -5558,7 +6010,7 @@ task.command("get <task> <path>").description("read one task file; prints it, or
|
|
|
5558
6010
|
run(async (taskId, path, opts) => {
|
|
5559
6011
|
const bytes = await readTaskFile(taskId, path);
|
|
5560
6012
|
if (opts.out) {
|
|
5561
|
-
|
|
6013
|
+
writeFileSync8(opts.out, bytes);
|
|
5562
6014
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
5563
6015
|
} else process.stdout.write(bytes);
|
|
5564
6016
|
})
|
|
@@ -5575,6 +6027,15 @@ task.command("rm <task> <path>").description("remove a path from the task's file
|
|
|
5575
6027
|
console.log(`Removed ${path}`);
|
|
5576
6028
|
})
|
|
5577
6029
|
);
|
|
6030
|
+
task.command("export <task>").description("write the task's session record as a Harbor task directory: instruction, rubric + judge, files, rollout, and one step per revision").option("-o, --out <dir>", "where to write the directory (default ./harbor-<task>)").option("--tier <tier>", "gold | silver | exploratory (recorded in task.toml)", "silver").option("--redact <name=alias...>", "replace a name everywhere in the export (repeatable)").action(
|
|
6031
|
+
run(async (taskId, opts) => {
|
|
6032
|
+
const tier = opts.tier === "gold" || opts.tier === "exploratory" ? opts.tier : "silver";
|
|
6033
|
+
const outDir = opts.out ?? `./harbor-${taskId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
6034
|
+
const { written, record } = await exportTaskHarbor(taskId, outDir, { tier, redactNames: parseRedactNames(opts.redact) });
|
|
6035
|
+
console.log(`Wrote ${written.length} files to ${outDir} (${record.threads.length} thread${record.threads.length === 1 ? "" : "s"}, ${record.files.length} task file${record.files.length === 1 ? "" : "s"}${record.goal ? ", goal rubric" : ", no goal: derived rubric"})`);
|
|
6036
|
+
if (!written.includes("environment/Dockerfile")) console.log("environment/: not resettable from the record \u2014 see environment/README.md before running with Harbor");
|
|
6037
|
+
})
|
|
6038
|
+
);
|
|
5578
6039
|
task.command("show <task>").description("the task's sessions and the requests waiting on you (questions, approvals, apps to connect)").option("--json", "print the task as JSON").action(
|
|
5579
6040
|
run(async (taskId, opts) => {
|
|
5580
6041
|
const found = await getTask(taskId);
|
|
@@ -5593,6 +6054,42 @@ task.command("answer <task> <request>").description("answer an open request; the
|
|
|
5593
6054
|
console.log(`Answered ${requestId}`);
|
|
5594
6055
|
})
|
|
5595
6056
|
);
|
|
6057
|
+
task.command("threads <task>").description("the task's threads from the session record: root first, then every helper, handoff and child with its source, agent, status and spend").option("--json", "print the threads as JSON").action(
|
|
6058
|
+
run(async (taskId, opts) => {
|
|
6059
|
+
const threads = await listThreads(taskId);
|
|
6060
|
+
if (opts.json) {
|
|
6061
|
+
console.log(JSON.stringify({ threads }));
|
|
6062
|
+
return;
|
|
6063
|
+
}
|
|
6064
|
+
for (const thread2 of threads) console.log(renderThread(thread2));
|
|
6065
|
+
})
|
|
6066
|
+
);
|
|
6067
|
+
task.command("decide <session> <kind>").description("record what you did with a session's result: adopt | rerun | revise | discard | vote | did_the_job | approve").option("--target <session>", "the session the decision names (a vote's winner, the option you adopted)").option("--yes", "the value for did_the_job and approve").option("--no", "the value for did_the_job and approve").option("--reason <text>", "your own words, kept with the decision").option("--key <key>", "an idempotency key, so a repeat records nothing new").action(
|
|
6068
|
+
run(async (sessionId, kind, opts) => {
|
|
6069
|
+
const value = opts.yes ? true : opts.no ? false : void 0;
|
|
6070
|
+
const { event } = await recordDecision(sessionId, kind, { target: opts.target, value, reason: opts.reason, idempotencyKey: opts.key });
|
|
6071
|
+
console.log(`Recorded ${kind} on ${event.sessionId} (${event.id})`);
|
|
6072
|
+
})
|
|
6073
|
+
);
|
|
6074
|
+
task.command("outcome <session> <kind>").description("record what became of a session's result: pr_merged | deployed | reopened | later_failure | shipped").option("--ref <ref>", "the PR, deployment or task this refers to").option("--key <key>", "an idempotency key, so a repeat records nothing new").action(
|
|
6075
|
+
run(async (sessionId, kind, opts) => {
|
|
6076
|
+
const { event } = await recordOutcome(sessionId, kind, { ref: opts.ref, idempotencyKey: opts.key });
|
|
6077
|
+
console.log(`Recorded ${kind} on ${event.sessionId} (${event.id})`);
|
|
6078
|
+
})
|
|
6079
|
+
);
|
|
6080
|
+
task.command("dispute <task>").description("dispute an agent fee: tell the platform the task was not completed (allowed within 72 hours of the result)").requiredOption("--reason <text>", "why the task was not completed (up to 1000 characters)").option("--session <id>", "the specific session whose fee you dispute (required when the task has more than one)").action(
|
|
6081
|
+
run(async (taskId, opts) => {
|
|
6082
|
+
const { task: updated } = await api("POST", `/tasks/${encodeURIComponent(taskId)}/agent-fee/dispute`, { reason: opts.reason, ...opts.session ? { session_id: opts.session } : {} });
|
|
6083
|
+
console.log(`Disputed fee on task ${updated.id}`);
|
|
6084
|
+
})
|
|
6085
|
+
);
|
|
6086
|
+
task.command("verdict <task> <did-the-job>").description("record whether the task did the job: `yes` or `no` (replaces an earlier answer)").action(
|
|
6087
|
+
run(async (taskId, value) => {
|
|
6088
|
+
if (value !== "yes" && value !== "no") throw new Error("value must be yes or no");
|
|
6089
|
+
const { task: updated } = await api("PUT", `/tasks/${encodeURIComponent(taskId)}/completion_answer`, { did_the_job: value === "yes" });
|
|
6090
|
+
console.log(`Recorded verdict on task ${updated.id}: did_the_job=${value === "yes"}`);
|
|
6091
|
+
})
|
|
6092
|
+
);
|
|
5596
6093
|
var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent by id");
|
|
5597
6094
|
skills.command("upload <dir>").description("upload a skill directory (SKILL.md at its root) as a new skill").option("--name <name>", "display name (default: the directory's name)").option("--json", "print the stored skill as JSON").action(
|
|
5598
6095
|
run(async (dir, opts) => {
|
|
@@ -5614,6 +6111,27 @@ skills.command("list").description("the workspace's stored skills").action(
|
|
|
5614
6111
|
for (const skill of list) console.log(`${skill.id} ${skill.displayName ?? ""}`);
|
|
5615
6112
|
})
|
|
5616
6113
|
);
|
|
6114
|
+
skills.command("usage").description("view and miss counts per skill name in this workspace").option("--since <iso>", `start date (ISO 8601); default is the last 30 days`).option("--json", "print JSON").action(
|
|
6115
|
+
run(async (opts) => {
|
|
6116
|
+
const summary = await getSkillUsage(opts.since);
|
|
6117
|
+
if (opts.json) {
|
|
6118
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
6119
|
+
return;
|
|
6120
|
+
}
|
|
6121
|
+
if (!summary.skills.length) {
|
|
6122
|
+
console.log("No skill usage recorded in this period.");
|
|
6123
|
+
} else {
|
|
6124
|
+
console.log(`${"NAME".padEnd(32)} ${"VIEWS".padStart(6)} ${"MISSES".padStart(7)} LAST SEEN`);
|
|
6125
|
+
for (const s of summary.skills) {
|
|
6126
|
+
console.log(`${s.name.padEnd(32)} ${String(s.views).padStart(6)} ${String(s.misses).padStart(7)} ${s.lastSeen.slice(0, 10)}`);
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
if (summary.topMissed.length) {
|
|
6130
|
+
console.log("\nTop missed:");
|
|
6131
|
+
for (const m of summary.topMissed) console.log(` ${m.name} (${m.misses})`);
|
|
6132
|
+
}
|
|
6133
|
+
})
|
|
6134
|
+
);
|
|
5617
6135
|
async function decideReflection(id, verb) {
|
|
5618
6136
|
const { reflection_proposal: p } = await api("POST", `/reflection_proposals/${encodeURIComponent(id)}/${verb}`);
|
|
5619
6137
|
console.log(`${p.id} ${p.status}`);
|
|
@@ -5653,7 +6171,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
|
|
|
5653
6171
|
run(async (id, path, opts) => {
|
|
5654
6172
|
const bytes = await readArtifactFile(id, path);
|
|
5655
6173
|
if (opts.out) {
|
|
5656
|
-
|
|
6174
|
+
writeFileSync8(opts.out, bytes);
|
|
5657
6175
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
5658
6176
|
} else process.stdout.write(bytes);
|
|
5659
6177
|
})
|
|
@@ -5951,7 +6469,7 @@ usageOptions(
|
|
|
5951
6469
|
run(async (opts) => {
|
|
5952
6470
|
const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
|
|
5953
6471
|
if (opts.out) {
|
|
5954
|
-
|
|
6472
|
+
writeFileSync8(opts.out, bytes);
|
|
5955
6473
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
5956
6474
|
} else {
|
|
5957
6475
|
process.stdout.write(bytes);
|
|
@@ -6161,6 +6679,226 @@ connector.command("shared").description("everything you share with workspaces, a
|
|
|
6161
6679
|
for (const s of shared) console.log(`${s.connection_id} ${s.app_name} (${s.label}) \u2192 ${s.workspace.slug} ${s.use_count} call${s.use_count === 1 ? "" : "s"}`);
|
|
6162
6680
|
})
|
|
6163
6681
|
);
|
|
6682
|
+
var hookRule = program.command("hook-rule").description("workspace hook rules: run a webhook or block at platform-controlled points");
|
|
6683
|
+
hookRule.command("list").description("the workspace's hook rules in evaluation order, or a specific workspace's with --workspace").option("--workspace <slug>", "list the workspace-level hooks for this workspace (PUT/GET /universes/{slug}/hooks)").option("--json", "print as JSON").action(
|
|
6684
|
+
run(async (opts) => {
|
|
6685
|
+
const slug = opts.workspace ?? await workspaceSlug();
|
|
6686
|
+
let data;
|
|
6687
|
+
if (opts.workspace) {
|
|
6688
|
+
data = await api("GET", `/universes/${encodeURIComponent(slug)}/hooks`);
|
|
6689
|
+
} else {
|
|
6690
|
+
data = await api("GET", "/hook_rules");
|
|
6691
|
+
}
|
|
6692
|
+
console.log(opts.json ? JSON.stringify(data, null, 2) : JSON.stringify(data, null, 2));
|
|
6693
|
+
})
|
|
6694
|
+
);
|
|
6695
|
+
hookRule.command("set").description("replace the entire ordered list of workspace-level hook rules (owner only); pass JSON rules on stdin or --rules").option("--workspace <slug>", "act on this workspace's hooks instead of the top-level hook_rules").option("--rules <json>", "JSON array of rule objects (overrides stdin)").action(
|
|
6696
|
+
run(async (opts) => {
|
|
6697
|
+
const raw = opts.rules ?? await new Promise((resolve10) => {
|
|
6698
|
+
const chunks = [];
|
|
6699
|
+
process.stdin.on("data", (d) => chunks.push(d));
|
|
6700
|
+
process.stdin.on("end", () => resolve10(Buffer.concat(chunks).toString()));
|
|
6701
|
+
});
|
|
6702
|
+
const rules = JSON.parse(raw);
|
|
6703
|
+
const slug = opts.workspace ?? await workspaceSlug();
|
|
6704
|
+
let result;
|
|
6705
|
+
if (opts.workspace) {
|
|
6706
|
+
result = await api("PUT", `/universes/${encodeURIComponent(slug)}/hooks`, { hooks: rules });
|
|
6707
|
+
} else {
|
|
6708
|
+
result = await api("PUT", "/hook_rules", { rules });
|
|
6709
|
+
}
|
|
6710
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6711
|
+
})
|
|
6712
|
+
);
|
|
6713
|
+
var vendorLine = (a) => `${a.id} ${a.vendor_name.padEnd(18)} ${a.status.padEnd(14)} ${a.label}${a.plan ? ` (${a.plan})` : ""} used ${a.last_used_at?.slice(0, 10) ?? "never"}`;
|
|
6714
|
+
var vendor = program.command("vendor").description("vendor accounts: third-party API keys and agent logins your agents use");
|
|
6715
|
+
vendor.command("list").description("your workspace's vendor accounts").option("--json", "print as JSON").action(
|
|
6716
|
+
run(async (opts) => {
|
|
6717
|
+
const { external_accounts: list } = await api("GET", "/external_accounts");
|
|
6718
|
+
if (opts.json) {
|
|
6719
|
+
console.log(JSON.stringify(list, null, 2));
|
|
6720
|
+
return;
|
|
6721
|
+
}
|
|
6722
|
+
if (!list.length) console.log("no vendor accounts");
|
|
6723
|
+
for (const a of list) console.log(vendorLine(a));
|
|
6724
|
+
})
|
|
6725
|
+
);
|
|
6726
|
+
vendor.command("add <vendor>").description("connect a vendor account by pasting its key or settings JSON").option("--label <label>", "a name for this account").option("--credential <value>", "the key or JSON settings (prompts if omitted)").action(
|
|
6727
|
+
run(async (vendorId, opts) => {
|
|
6728
|
+
let credential = opts.credential ?? "";
|
|
6729
|
+
if (!credential) {
|
|
6730
|
+
credential = await new Promise((resolve10) => {
|
|
6731
|
+
const rl = createInterface6({ input: process.stdin, output: process.stderr });
|
|
6732
|
+
rl.question(`${vendorId} credential: `, (a) => {
|
|
6733
|
+
rl.close();
|
|
6734
|
+
resolve10(a.trim());
|
|
6735
|
+
});
|
|
6736
|
+
});
|
|
6737
|
+
}
|
|
6738
|
+
let parsed = credential;
|
|
6739
|
+
try {
|
|
6740
|
+
parsed = JSON.parse(credential);
|
|
6741
|
+
} catch {
|
|
6742
|
+
}
|
|
6743
|
+
const { external_account: created } = await api("POST", "/external_accounts", { vendor: vendorId, label: opts.label, credential: parsed });
|
|
6744
|
+
console.log(vendorLine(created));
|
|
6745
|
+
})
|
|
6746
|
+
);
|
|
6747
|
+
vendor.command("rm <id>").description("disconnect a vendor account").action(
|
|
6748
|
+
run(async (id) => {
|
|
6749
|
+
await api("DELETE", `/external_accounts/${encodeURIComponent(id)}`);
|
|
6750
|
+
console.log(`Removed ${id}`);
|
|
6751
|
+
})
|
|
6752
|
+
);
|
|
6753
|
+
vendor.command("usage <id>").description("the agents and tasks that used this vendor account").option("--json", "print as JSON").action(
|
|
6754
|
+
run(async (id, opts) => {
|
|
6755
|
+
const { usage } = await api("GET", `/external_accounts/${encodeURIComponent(id)}/usage`);
|
|
6756
|
+
if (opts.json) {
|
|
6757
|
+
console.log(JSON.stringify(usage, null, 2));
|
|
6758
|
+
return;
|
|
6759
|
+
}
|
|
6760
|
+
console.log(`${usage.agents.length} agent${usage.agents.length === 1 ? "" : "s"}, ${usage.tasks.length} task${usage.tasks.length === 1 ? "" : "s"}`);
|
|
6761
|
+
const agents = usage.agents;
|
|
6762
|
+
for (const a of agents) console.log(` agent ${a.id} ${a.name}`);
|
|
6763
|
+
const tasks = usage.tasks;
|
|
6764
|
+
for (const t of tasks) console.log(` task ${t.session_id} ${t.title ?? "(untitled)"} ${t.last_used_at.slice(0, 10)}`);
|
|
6765
|
+
})
|
|
6766
|
+
);
|
|
6767
|
+
var browserProfile = program.command("browser-profile").description("saved-login profiles: the sites your agents stay signed in to");
|
|
6768
|
+
browserProfile.command("list").description("your workspace's browser profiles: the sites your agents stay signed in to").option("--json", "print as JSON").action(
|
|
6769
|
+
run(async (opts) => {
|
|
6770
|
+
const profiles = await listBrowserProfiles();
|
|
6771
|
+
if (opts.json) {
|
|
6772
|
+
console.log(JSON.stringify(profiles, null, 2));
|
|
6773
|
+
return;
|
|
6774
|
+
}
|
|
6775
|
+
if (!profiles.length) console.log("no browser profiles");
|
|
6776
|
+
for (const p of profiles) console.log(renderBrowserProfile(p));
|
|
6777
|
+
})
|
|
6778
|
+
);
|
|
6779
|
+
browserProfile.command("sites <scope>").description("list the sites in a profile: workspace or private").option("--json", "print as JSON").action(
|
|
6780
|
+
run(async (scope, opts) => {
|
|
6781
|
+
const sites = await listBrowserProfileSites(scope);
|
|
6782
|
+
if (opts.json) {
|
|
6783
|
+
console.log(JSON.stringify(sites, null, 2));
|
|
6784
|
+
return;
|
|
6785
|
+
}
|
|
6786
|
+
if (!sites.length) console.log("no sites");
|
|
6787
|
+
console.log(`${"SITE".padEnd(40)} ${"STATUS".padEnd(14)} SIGNED IN BY LAST USED`);
|
|
6788
|
+
for (const s of sites) console.log(renderBrowserProfileSite(s));
|
|
6789
|
+
})
|
|
6790
|
+
);
|
|
6791
|
+
browserProfile.command("import <scope>").description("import saved browser state into a profile (workspace or private); reads Playwright storage state JSON from stdin or --file").option("--file <path>", "path to a Playwright storage state JSON file (reads stdin if omitted)").action(
|
|
6792
|
+
run(async (scope, opts) => {
|
|
6793
|
+
let raw;
|
|
6794
|
+
if (opts.file) {
|
|
6795
|
+
const { readFileSync: rf } = await import("fs");
|
|
6796
|
+
raw = rf(opts.file, "utf8");
|
|
6797
|
+
} else {
|
|
6798
|
+
raw = await new Promise((resolve10) => {
|
|
6799
|
+
const chunks = [];
|
|
6800
|
+
process.stdin.on("data", (d) => chunks.push(d));
|
|
6801
|
+
process.stdin.on("end", () => resolve10(Buffer.concat(chunks).toString()));
|
|
6802
|
+
});
|
|
6803
|
+
}
|
|
6804
|
+
const state = JSON.parse(raw);
|
|
6805
|
+
const result = await importBrowserProfileState(scope, { state });
|
|
6806
|
+
console.log(`Imported ${result.sites.length} site${result.sites.length === 1 ? "" : "s"}: ${result.sites.join(", ")}`);
|
|
6807
|
+
})
|
|
6808
|
+
);
|
|
6809
|
+
browserProfile.command("remove-site <scope> <site>").description("remove a site from a profile (the agent will need to sign in again)").action(
|
|
6810
|
+
run(async (scope, site) => {
|
|
6811
|
+
await removeBrowserProfileSite(scope, site);
|
|
6812
|
+
console.log(`Removed ${site} from ${scope}`);
|
|
6813
|
+
})
|
|
6814
|
+
);
|
|
6815
|
+
browserProfile.command("move-site <scope> <site> <to>").description("move a site between profiles: <scope> is the current one, <to> is workspace or private").action(
|
|
6816
|
+
run(async (scope, site, to) => {
|
|
6817
|
+
if (to !== "workspace" && to !== "private") throw new ApiError(400, "invalid_request", "to must be workspace or private");
|
|
6818
|
+
const result = await moveBrowserProfileSite(scope, site, to);
|
|
6819
|
+
console.log(`Moved ${site} from ${scope} to ${result.to}`);
|
|
6820
|
+
})
|
|
6821
|
+
);
|
|
6822
|
+
var notifications = program.command("notifications").description("email and browser push notification preferences for tasks");
|
|
6823
|
+
notifications.command("get").description("show the current notification preferences").option("--json", "print as JSON").action(
|
|
6824
|
+
run(async (opts) => {
|
|
6825
|
+
const { notification_preferences: prefs } = await api("GET", "/notification-preferences");
|
|
6826
|
+
if (opts.json) {
|
|
6827
|
+
console.log(JSON.stringify(prefs, null, 2));
|
|
6828
|
+
return;
|
|
6829
|
+
}
|
|
6830
|
+
console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done}`);
|
|
6831
|
+
console.log(`push: needs_you=${prefs.push.needs_you} done=${prefs.push.done}`);
|
|
6832
|
+
})
|
|
6833
|
+
);
|
|
6834
|
+
notifications.command("set").description("update notification preferences (pass at least one flag)").option("--email-needs-you <bool>", "email when the task needs your input: true or false").option("--email-done <bool>", "email when the task finishes: true or false").option("--push-needs-you <bool>", "push notification when the task needs your input: true or false").option("--push-done <bool>", "push notification when the task finishes: true or false").action(
|
|
6835
|
+
run(async (opts) => {
|
|
6836
|
+
const parseBool = (v) => v === void 0 ? void 0 : v === "true" ? true : v === "false" ? false : (() => {
|
|
6837
|
+
throw new ApiError(400, "invalid_request", "value must be true or false");
|
|
6838
|
+
})();
|
|
6839
|
+
const email = {};
|
|
6840
|
+
const push = {};
|
|
6841
|
+
const ny_e = parseBool(opts.emailNeedsYou);
|
|
6842
|
+
if (ny_e !== void 0) email.needs_you = ny_e;
|
|
6843
|
+
const d_e = parseBool(opts.emailDone);
|
|
6844
|
+
if (d_e !== void 0) email.done = d_e;
|
|
6845
|
+
const ny_p = parseBool(opts.pushNeedsYou);
|
|
6846
|
+
if (ny_p !== void 0) push.needs_you = ny_p;
|
|
6847
|
+
const d_p = parseBool(opts.pushDone);
|
|
6848
|
+
if (d_p !== void 0) push.done = d_p;
|
|
6849
|
+
if (!Object.keys(email).length && !Object.keys(push).length) throw new ApiError(400, "invalid_request", "pass at least one --email-* or --push-* flag");
|
|
6850
|
+
const { notification_preferences: prefs } = await api("PATCH", "/notification-preferences", {
|
|
6851
|
+
...Object.keys(email).length ? { email } : {},
|
|
6852
|
+
...Object.keys(push).length ? { push } : {}
|
|
6853
|
+
});
|
|
6854
|
+
console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done}`);
|
|
6855
|
+
console.log(`push: needs_you=${prefs.push.needs_you} done=${prefs.push.done}`);
|
|
6856
|
+
})
|
|
6857
|
+
);
|
|
6858
|
+
var helpBase = () => agentUseBase().replace(/\/$/, "");
|
|
6859
|
+
async function helpFetch(path) {
|
|
6860
|
+
const res = await fetch(`${helpBase()}${path}`, { headers: { "user-agent": USER_AGENT } });
|
|
6861
|
+
if (!res.ok) {
|
|
6862
|
+
let msg = `${res.status} ${res.statusText}`;
|
|
6863
|
+
try {
|
|
6864
|
+
const b = await res.json();
|
|
6865
|
+
if (b.error) msg = b.error;
|
|
6866
|
+
} catch {
|
|
6867
|
+
}
|
|
6868
|
+
throw new ApiError(res.status, "http_error", msg);
|
|
6869
|
+
}
|
|
6870
|
+
return await res.json();
|
|
6871
|
+
}
|
|
6872
|
+
var help = program.command("help").description("search the AgentSky help center");
|
|
6873
|
+
help.command("search <query...>").description("search help articles by keyword").option("--top <n>", "how many results (1\u201320, default 5)", "5").option("--json", "print as JSON").action(
|
|
6874
|
+
run(async (words, opts) => {
|
|
6875
|
+
const q = words.join(" ");
|
|
6876
|
+
const topK = Math.min(Math.max(parseInt(opts.top, 10) || 5, 1), 20);
|
|
6877
|
+
const { results } = await helpFetch(`/api/help/search?q=${encodeURIComponent(q)}&top_k=${topK}`);
|
|
6878
|
+
if (opts.json) {
|
|
6879
|
+
console.log(JSON.stringify(results, null, 2));
|
|
6880
|
+
return;
|
|
6881
|
+
}
|
|
6882
|
+
if (!results.length) {
|
|
6883
|
+
console.log("no results");
|
|
6884
|
+
return;
|
|
6885
|
+
}
|
|
6886
|
+
for (const r of results) console.log(`${r.slug.padEnd(40)} [${r.category}] ${r.title}
|
|
6887
|
+
${r.snippet.slice(0, 100)}`);
|
|
6888
|
+
})
|
|
6889
|
+
);
|
|
6890
|
+
help.command("get <slug>").description("read a help article by its slug").option("--json", "print as JSON").action(
|
|
6891
|
+
run(async (slug, opts) => {
|
|
6892
|
+
const article = await helpFetch(`/api/help/${encodeURIComponent(slug)}`);
|
|
6893
|
+
if (opts.json) {
|
|
6894
|
+
console.log(JSON.stringify(article, null, 2));
|
|
6895
|
+
return;
|
|
6896
|
+
}
|
|
6897
|
+
console.log(`# ${article.title}
|
|
6898
|
+
`);
|
|
6899
|
+
console.log(article.body);
|
|
6900
|
+
})
|
|
6901
|
+
);
|
|
6164
6902
|
var vault = program.command("vault").description("vaults: credentials a session is granted when it starts (values write-only)");
|
|
6165
6903
|
vault.command("list").description("the workspace's vaults").action(
|
|
6166
6904
|
run(async () => {
|
|
@@ -6250,7 +6988,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
|
|
|
6250
6988
|
}));
|
|
6251
6989
|
var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
|
|
6252
6990
|
traceCmd.command("start").description("start a trace: a grant for this machine, a root span, and the spool the hooks write to").option("--name <name>", "a name for the task").option("--source <source>", "how this machine reports itself", "claude-code").option("--hooks [settings]", "also install the Claude Code hooks (default: ./.claude/settings.local.json)").option("--capture <scopes>", "also send content, redacted on this machine first: any of prompts,tool_output,files").option("--file <path>", "a file the files scope may send (repeatable)", collect).option("--dir <path>", "a folder whose files the files scope may send when a file tool touches them (repeatable)", collect).action(run(async (opts) => {
|
|
6253
|
-
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-
|
|
6991
|
+
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-DI2SQI7M.js");
|
|
6254
6992
|
if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
|
|
6255
6993
|
const scopes = parseContentScopes(opts.capture);
|
|
6256
6994
|
const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
|
|
@@ -6285,7 +7023,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
|
|
|
6285
7023
|
}));
|
|
6286
7024
|
traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
|
|
6287
7025
|
try {
|
|
6288
|
-
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-
|
|
7026
|
+
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-DI2SQI7M.js");
|
|
6289
7027
|
const chunks = [];
|
|
6290
7028
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
6291
7029
|
const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
@@ -6298,14 +7036,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
|
|
|
6298
7036
|
}
|
|
6299
7037
|
});
|
|
6300
7038
|
traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
|
|
6301
|
-
const { readState, flush, grantSender } = await import("./trace-
|
|
7039
|
+
const { readState, flush, grantSender } = await import("./trace-DI2SQI7M.js");
|
|
6302
7040
|
const state = readState();
|
|
6303
7041
|
if (!state) throw new Error("no trace is running here");
|
|
6304
7042
|
const outcome = await flush(grantSender(state), { attempts: 4 });
|
|
6305
7043
|
console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
|
|
6306
7044
|
}));
|
|
6307
7045
|
traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
|
|
6308
|
-
const { readState, spoolStatus } = await import("./trace-
|
|
7046
|
+
const { readState, spoolStatus } = await import("./trace-DI2SQI7M.js");
|
|
6309
7047
|
const state = readState();
|
|
6310
7048
|
if (!state) {
|
|
6311
7049
|
console.log("No trace is running here.");
|
|
@@ -6315,7 +7053,7 @@ traceCmd.command("status").description("the running trace and its spool").action
|
|
|
6315
7053
|
console.log(`${state.trace_id} as ${state.source}: ${spool.pending} pending, ${spool.dropped_full} dropped (spool full), ${spool.dropped_expired} given up, ${spool.refused} refused`);
|
|
6316
7054
|
}));
|
|
6317
7055
|
traceCmd.command("show [id]").description("the trace bundle: spans, the sessions they reach, cost counted once per session").option("--json", "print the bundle as JSON").action(run(async (id, opts) => {
|
|
6318
|
-
const { readState } = await import("./trace-
|
|
7056
|
+
const { readState } = await import("./trace-DI2SQI7M.js");
|
|
6319
7057
|
const ref = id ?? readState()?.trace_id;
|
|
6320
7058
|
if (!ref) throw new Error("name a trace, or start one here");
|
|
6321
7059
|
const bundle = await api("GET", `/traces/${ref}`);
|
|
@@ -6342,7 +7080,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
|
|
|
6342
7080
|
for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
|
|
6343
7081
|
}));
|
|
6344
7082
|
traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
|
|
6345
|
-
const { readState, record, flush, grantSender, clearState } = await import("./trace-
|
|
7083
|
+
const { readState, record, flush, grantSender, clearState } = await import("./trace-DI2SQI7M.js");
|
|
6346
7084
|
const state = readState();
|
|
6347
7085
|
if (!state) throw new Error("no trace is running here");
|
|
6348
7086
|
if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
|
|
@@ -6352,7 +7090,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
|
|
|
6352
7090
|
console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
|
|
6353
7091
|
}));
|
|
6354
7092
|
traceCmd.command("sync [payload]").description("upload this machine's Claude Code and Codex sessions to the matching workspace's trace library, redacted here first").option("--enable", "turn sync on for this machine").option("--disable", "turn sync off; nothing more is uploaded").option("--route <match=workspace>", "send sessions whose folder path contains <match> to <workspace> (repeatable; replaces the saved rules)", collect).option("--default <workspace>", "where a session no rule matches goes").option("--keep-local", "keep sessions no rule matches on this machine (the default)").option("--session <ref>", "only this session: its id or transcript path (repeatable)", collect).option("--full", "re-read every session, not only files that changed").option("--limit <n>", "at most this many changed sessions this run", (v) => Number.parseInt(v, 10)).option("--dry-run", "show what would be uploaded without uploading").option("--hook", "run from a Claude Code SessionEnd hook or Codex notify: upload that session in the background and return at once").option("--install-schedule", "also sweep every 15 minutes (macOS launchd)").option("--status", "what sync has done on this machine").option("--quiet", "print only failures").action(async (payload, opts) => {
|
|
6355
|
-
const lib = await import("./trace-library-
|
|
7093
|
+
const lib = await import("./trace-library-4WWKCSVU.js");
|
|
6356
7094
|
if (opts.hook) {
|
|
6357
7095
|
try {
|
|
6358
7096
|
let raw = payload ?? "";
|
|
@@ -6400,10 +7138,10 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
|
|
|
6400
7138
|
console.log(`A repository's .agentsky.json "workspace" wins over these rules.`);
|
|
6401
7139
|
}
|
|
6402
7140
|
if (opts.installSchedule) {
|
|
6403
|
-
const { join:
|
|
7141
|
+
const { join: join17 } = await import("path");
|
|
6404
7142
|
const { homedir: homedir8 } = await import("os");
|
|
6405
7143
|
const { execFileSync } = await import("child_process");
|
|
6406
|
-
const log =
|
|
7144
|
+
const log = join17(homedir8(), ".config", "sky", "trace-sync.log");
|
|
6407
7145
|
const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
|
|
6408
7146
|
try {
|
|
6409
7147
|
execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
|
|
@@ -6414,10 +7152,10 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
|
|
|
6414
7152
|
}
|
|
6415
7153
|
if (opts.status) {
|
|
6416
7154
|
const { readFileSync: readFileSync16 } = await import("fs");
|
|
6417
|
-
const { join:
|
|
7155
|
+
const { join: join17 } = await import("path");
|
|
6418
7156
|
let state = { sessions: {} };
|
|
6419
7157
|
try {
|
|
6420
|
-
state = JSON.parse(readFileSync16(
|
|
7158
|
+
state = JSON.parse(readFileSync16(join17(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
|
|
6421
7159
|
} catch {
|
|
6422
7160
|
}
|
|
6423
7161
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -6450,7 +7188,7 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
|
|
|
6450
7188
|
})();
|
|
6451
7189
|
});
|
|
6452
7190
|
traceCmd.command("search <query...>").description("search every session in a workspace's trace library (literal substring)").option("--workspace <slug>", "the workspace to search (default: the current one)").option("--limit <n>", "hits per page", (v) => Number.parseInt(v, 10), 20).option("--cursor <cursor>", "the next page of an earlier search").option("--json", "print the page as JSON").action(run(async (words, opts) => {
|
|
6453
|
-
const { libraryRequest } = await import("./trace-library-
|
|
7191
|
+
const { libraryRequest } = await import("./trace-library-4WWKCSVU.js");
|
|
6454
7192
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
6455
7193
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
6456
7194
|
const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
|
|
@@ -6467,7 +7205,7 @@ traceCmd.command("search <query...>").description("search every session in a wor
|
|
|
6467
7205
|
if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
|
|
6468
7206
|
}));
|
|
6469
7207
|
traceCmd.command("library").description("list the sessions in a workspace's trace library, most recently updated first").option("--workspace <slug>", "the workspace (default: the current one)").option("--limit <n>", "how many", (v) => Number.parseInt(v, 10), 20).option("--json", "print the page as JSON").action(run(async (opts) => {
|
|
6470
|
-
const { libraryRequest } = await import("./trace-library-
|
|
7208
|
+
const { libraryRequest } = await import("./trace-library-4WWKCSVU.js");
|
|
6471
7209
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
6472
7210
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
6473
7211
|
const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
|
|
@@ -6497,5 +7235,162 @@ workerCmd.command("start <pool>").description("start a worker connected to the g
|
|
|
6497
7235
|
process.on("SIGTERM", () => ac.abort());
|
|
6498
7236
|
await startWorker({ poolId, token, label: options.label, apiBase: base, signal: ac.signal });
|
|
6499
7237
|
});
|
|
7238
|
+
var compare = program.command("compare").description("side-by-side agent comparisons: create, list, read, pick a winner");
|
|
7239
|
+
compare.command("create").description("run one task on two or more agents simultaneously and start a comparison").requiredOption("--task <text>", "the task to send to every agent").requiredOption("--agent <type>", "the main agent's harness type (e.g. claude_code, hermes)").requiredOption("--llm <model>", "the main agent's model").option("--vs <type:model...>", "one or more competing agents in type:model form (e.g. codex:gpt-6)", (val, acc) => [...acc, val], []).option("--effort <level>", "reasoning effort for every agent (low|medium|high)").option("--json", "print the created comparison as JSON").action(
|
|
7240
|
+
run(async (opts) => {
|
|
7241
|
+
const competitors = (opts.vs ?? []).map((v) => {
|
|
7242
|
+
const [agentType, llm] = v.split(":");
|
|
7243
|
+
if (!agentType || !llm) throw new ApiError(400, "invalid_request", `--vs must be type:model, got "${v}"`);
|
|
7244
|
+
return { agentType, llm, ...opts.effort ? { reasoningEffort: opts.effort } : {} };
|
|
7245
|
+
});
|
|
7246
|
+
if (!competitors.length) throw new ApiError(400, "invalid_request", "Provide at least one --vs type:model competitor.");
|
|
7247
|
+
const requestId = `sky-compare-${Date.now()}`;
|
|
7248
|
+
const result = await createComparison({
|
|
7249
|
+
requestId,
|
|
7250
|
+
agentType: opts.agent,
|
|
7251
|
+
llm: opts.llm,
|
|
7252
|
+
task: opts.task,
|
|
7253
|
+
compare: competitors,
|
|
7254
|
+
...opts.effort ? { reasoningEffort: opts.effort } : {}
|
|
7255
|
+
});
|
|
7256
|
+
if (opts.json) {
|
|
7257
|
+
console.log(JSON.stringify(result, null, 2));
|
|
7258
|
+
return;
|
|
7259
|
+
}
|
|
7260
|
+
console.log(`created ${result.comparisonId}`);
|
|
7261
|
+
for (const col of result.columns) console.log(` column ${col.columnId} session ${col.sessionId}`);
|
|
7262
|
+
})
|
|
7263
|
+
);
|
|
7264
|
+
compare.command("list").description("your workspace comparisons, most recently updated first").option("--archived", "list archived comparisons instead").option("--json", "print as JSON").action(
|
|
7265
|
+
run(async (opts) => {
|
|
7266
|
+
const comparisons = await listComparisons({ archived: opts.archived });
|
|
7267
|
+
if (opts.json) {
|
|
7268
|
+
console.log(JSON.stringify(comparisons, null, 2));
|
|
7269
|
+
return;
|
|
7270
|
+
}
|
|
7271
|
+
if (!comparisons.length) console.log("no comparisons");
|
|
7272
|
+
for (const c of comparisons) console.log(renderComparisonSummary(c));
|
|
7273
|
+
})
|
|
7274
|
+
);
|
|
7275
|
+
compare.command("show <id>").description("one comparison: task, agents, status, and the vote verdict").option("--json", "print as JSON").action(
|
|
7276
|
+
run(async (id, opts) => {
|
|
7277
|
+
const detail = await getComparison(id);
|
|
7278
|
+
if (opts.json) {
|
|
7279
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
7280
|
+
return;
|
|
7281
|
+
}
|
|
7282
|
+
console.log(`comparison ${detail.id}`);
|
|
7283
|
+
if (detail.title) console.log(` title: ${detail.title}`);
|
|
7284
|
+
if (detail.openerText) console.log(` task: ${detail.openerText}`);
|
|
7285
|
+
console.log(` created ${detail.createdAt}`);
|
|
7286
|
+
for (const col of detail.columns) {
|
|
7287
|
+
const masked = !col.agentType;
|
|
7288
|
+
const identity = masked ? "(hidden \u2014 Arena match pending)" : `${col.agentType ?? "?"}/${col.llm ?? "?"}`;
|
|
7289
|
+
console.log(` column ${col.columnId} session ${col.sessionId ?? "-"} ${identity} ${col.podStatus ?? ""}`);
|
|
7290
|
+
}
|
|
7291
|
+
if (detail.arena) console.log(` arena kind=${detail.arena.kind} status=${detail.arena.status} hidden=${detail.arena.hidden}`);
|
|
7292
|
+
if (detail.vote) console.log(` vote outcome=${detail.vote.outcome} winner=${detail.vote.winnerSessionId ?? "none"} sealed=${detail.vote.sealed}`);
|
|
7293
|
+
})
|
|
7294
|
+
);
|
|
7295
|
+
compare.command("pick <id>").description('record the winning column \u2014 or a tie or "both_bad" \u2014 for this comparison').option("--winner <columnId>", "the columnId of the winning column").option("--tie", "record a tie instead of a winner").option("--both-bad", "record that neither agent did the job").option("--json", "print the verdict as JSON").action(
|
|
7296
|
+
run(async (id, opts) => {
|
|
7297
|
+
const choices = [opts.winner, opts.tie, opts.bothBad].filter(Boolean).length;
|
|
7298
|
+
if (choices !== 1) throw new ApiError(400, "invalid_request", "Provide exactly one of --winner, --tie or --both-bad.");
|
|
7299
|
+
const outcome = opts.tie ? "tie" : opts.bothBad ? "both_bad" : void 0;
|
|
7300
|
+
const result = await pickWinner(id, { winnerColumnId: opts.winner, outcome });
|
|
7301
|
+
if (opts.json) {
|
|
7302
|
+
console.log(JSON.stringify(result, null, 2));
|
|
7303
|
+
return;
|
|
7304
|
+
}
|
|
7305
|
+
if (result.winnerSessionId) console.log(`winner: session ${result.winnerSessionId}`);
|
|
7306
|
+
else console.log("recorded: " + (outcome ?? "pick"));
|
|
7307
|
+
if (result.revealed) console.log("identities revealed:");
|
|
7308
|
+
for (const s of result.seats) console.log(` ${s.sessionId} ${s.agentType}/${s.llm}`);
|
|
7309
|
+
})
|
|
7310
|
+
);
|
|
7311
|
+
var thread = program.command("thread").description("workspace threads: the conversation history across all agents");
|
|
7312
|
+
thread.command("list").description("your workspace threads, most recently updated first (includes comparisons)").option("--archived", "list archived threads only").option("--all", "include both active and archived").option("--json", "print as JSON").action(
|
|
7313
|
+
run(async (opts) => {
|
|
7314
|
+
const filter = opts.all ? "all" : opts.archived ? "archived" : "active";
|
|
7315
|
+
const threads = await listThreads2({ filter });
|
|
7316
|
+
if (opts.json) {
|
|
7317
|
+
console.log(JSON.stringify(threads, null, 2));
|
|
7318
|
+
return;
|
|
7319
|
+
}
|
|
7320
|
+
if (!threads.length) console.log("no threads");
|
|
7321
|
+
for (const t of threads) console.log(renderThreadSummary(t));
|
|
7322
|
+
})
|
|
7323
|
+
);
|
|
7324
|
+
session.command("feedback <id>").description("read the thumbs-up/down feedback on this session's messages").option("--json", "print as JSON").action(
|
|
7325
|
+
run(async (id, opts) => {
|
|
7326
|
+
const result = await api(
|
|
7327
|
+
"GET",
|
|
7328
|
+
`/sessions/${encodeURIComponent(id)}/feedback`
|
|
7329
|
+
);
|
|
7330
|
+
if (opts.json) {
|
|
7331
|
+
console.log(JSON.stringify(result, null, 2));
|
|
7332
|
+
return;
|
|
7333
|
+
}
|
|
7334
|
+
const entries = Object.entries(result.reports);
|
|
7335
|
+
if (!entries.length) {
|
|
7336
|
+
console.log("no feedback recorded");
|
|
7337
|
+
return;
|
|
7338
|
+
}
|
|
7339
|
+
for (const [msgId, r] of entries) console.log(`${msgId.slice(0, 12)} ${r.vote} ${(r.reasons ?? []).join(", ")}`);
|
|
7340
|
+
})
|
|
7341
|
+
);
|
|
7342
|
+
session.command("side-chat <id>").description("ask a question about this session's transcript (answered beside it, never sent to the agent)").requiredOption("--question <text>", "the question to ask").requiredOption("--transcript <text>", "the session transcript or relevant excerpt").option("--quote <text>", "a specific passage the question targets").option("--json", "print as JSON").action(
|
|
7343
|
+
run(async (id, opts) => {
|
|
7344
|
+
const result = await api("POST", `/sessions/${encodeURIComponent(id)}/side-chat`, {
|
|
7345
|
+
transcript: opts.transcript,
|
|
7346
|
+
history: [],
|
|
7347
|
+
question: opts.question,
|
|
7348
|
+
...opts.quote ? { quote: opts.quote } : {}
|
|
7349
|
+
});
|
|
7350
|
+
if (opts.json) {
|
|
7351
|
+
console.log(JSON.stringify(result, null, 2));
|
|
7352
|
+
return;
|
|
7353
|
+
}
|
|
7354
|
+
console.log(result.answer);
|
|
7355
|
+
if (result.looked?.length) console.log(`
|
|
7356
|
+
(sources: ${result.looked.join(", ")})`);
|
|
7357
|
+
})
|
|
7358
|
+
);
|
|
7359
|
+
session.command("code-review <id>").description("run the code-review assistant on this session's diff or file").requiredOption("--diff <text>", "the diff or file content to review").option("--filename <name>", "the filename for language-aware review").option("--json", "print as JSON").action(
|
|
7360
|
+
run(async (id, opts) => {
|
|
7361
|
+
const result = await api(
|
|
7362
|
+
"POST",
|
|
7363
|
+
`/sessions/${encodeURIComponent(id)}/code-review`,
|
|
7364
|
+
{ diff: opts.diff, ...opts.filename ? { filename: opts.filename } : {} }
|
|
7365
|
+
);
|
|
7366
|
+
if (opts.json) {
|
|
7367
|
+
console.log(JSON.stringify(result, null, 2));
|
|
7368
|
+
return;
|
|
7369
|
+
}
|
|
7370
|
+
console.log(result.review);
|
|
7371
|
+
if (result.issues?.length) {
|
|
7372
|
+
console.log("\nIssues:");
|
|
7373
|
+
for (const issue of result.issues) {
|
|
7374
|
+
const loc = issue.line ? ` line ${issue.line}` : "";
|
|
7375
|
+
console.log(` [${issue.severity}]${loc} ${issue.message}`);
|
|
7376
|
+
}
|
|
7377
|
+
}
|
|
7378
|
+
})
|
|
7379
|
+
);
|
|
7380
|
+
session.command("snapshot <id>").description("read or revoke the public share link for a session").requiredOption("--thread <threadId>", "the thread/comparison that owns this session").option("--revoke", "revoke the snapshot link instead of reading it").option("--json", "print as JSON").action(
|
|
7381
|
+
run(async (id, opts) => {
|
|
7382
|
+
const path = `/sessions/${encodeURIComponent(id)}/snapshot?thread=${encodeURIComponent(opts.thread)}`;
|
|
7383
|
+
const result = await api(opts.revoke ? "DELETE" : "GET", path);
|
|
7384
|
+
if (opts.json) {
|
|
7385
|
+
console.log(JSON.stringify(result, null, 2));
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
if (opts.revoke) {
|
|
7389
|
+
console.log(result.url === null ? "snapshot link revoked" : `still set: ${result.url}`);
|
|
7390
|
+
return;
|
|
7391
|
+
}
|
|
7392
|
+
console.log(result.url ?? "no snapshot link set");
|
|
7393
|
+
})
|
|
7394
|
+
);
|
|
6500
7395
|
var ARGUMENTS_FOLLOW_THE_SCRIPT_PATH_ON_EVERY_RUNTIME = { from: "node" };
|
|
6501
7396
|
program.parseAsync(process.argv, ARGUMENTS_FOLLOW_THE_SCRIPT_PATH_ON_EVERY_RUNTIME).catch(fail);
|