@agentsky/sky 2.18.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveSessionRef,
10
10
  streamEvents,
11
11
  uploadFile
12
- } from "./chunk-PL5KNPCX.js";
12
+ } from "./chunk-4MVWXHZY.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-MUHFB3LC.js";
23
+ } from "./chunk-MOYCAI4S.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-ZJVFWFOY.js";
45
+ } from "./chunk-UJ3D24IG.js";
45
46
  import "./chunk-MLKGABMK.js";
46
47
 
47
48
  // src/index.ts
48
- import { existsSync as existsSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
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((thread) => thread.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 thread of record.threads) {
4146
+ for (const event of record.events[thread.id] ?? []) {
4147
+ if (event.type === "human.decision" || event.type === "outcome.observed") out.push({ ...event, thread_id: thread.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((thread) => thread.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((thread) => thread.relation === "primary") ?? record.threads[0];
4281
+ const harnesses = [...new Set(record.threads.map((thread) => thread.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 thread of threads) events[thread.id] = await pagedEvents(thread.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((thread) => thread.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(thread) {
4425
+ const spend = (Number(thread.usage.spent_usd_micros) / 1e6).toFixed(4);
4426
+ const env = thread.environment?.repo ? ` ${thread.environment.repo}${thread.environment.commit ? `@${thread.environment.commit.slice(0, 7)}` : ""}` : "";
4427
+ return `${thread.id} ${thread.relation.padEnd(7)} ${thread.source.padEnd(12)} ${thread.agent.harness}/${thread.agent.model} ${thread.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,45 @@ 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
+
4324
4707
  // src/harness.ts
4325
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
4326
- import { dirname as dirname7, join as join15, resolve as resolve8 } from "path";
4708
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
4709
+ import { dirname as dirname8, join as join16, resolve as resolve8 } from "path";
4327
4710
  var HARNESS_FILE = "harness.json";
4328
4711
  function harnessFilePath(dir) {
4329
- return join15(resolve8(dir), HARNESS_FILE);
4712
+ return join16(resolve8(dir), HARNESS_FILE);
4330
4713
  }
4331
4714
  function readHarnessFile(dir) {
4332
4715
  const path = harnessFilePath(dir);
@@ -4364,8 +4747,8 @@ function harnessInit(dir) {
4364
4747
  if (existsSync9(path)) {
4365
4748
  throw new Error(`${path} already exists \u2014 delete it to re-init`);
4366
4749
  }
4367
- mkdirSync4(dirname7(path), { recursive: true });
4368
- writeFileSync6(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
4750
+ mkdirSync5(dirname8(path), { recursive: true });
4751
+ writeFileSync7(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
4369
4752
  `);
4370
4753
  console.log(`Created ${path}`);
4371
4754
  console.log(INIT_COMMENT);
@@ -5070,7 +5453,7 @@ sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1
5070
5453
  run(async (ref, path, opts) => {
5071
5454
  const bytes = await readFile(await resolveSessionRef(ref), path);
5072
5455
  if (opts.out) {
5073
- writeFileSync7(opts.out, bytes);
5456
+ writeFileSync8(opts.out, bytes);
5074
5457
  console.error(`saved ${bytes.length} bytes to ${opts.out}`);
5075
5458
  } else process.stdout.write(bytes);
5076
5459
  })
@@ -5087,7 +5470,7 @@ sessionWorkspace.command("patch <ref> [repo]").description("the work in a reposi
5087
5470
  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
5471
  if (!patch.length) console.error("nothing changed");
5089
5472
  else if (opts.out) {
5090
- writeFileSync7(opts.out, patch);
5473
+ writeFileSync8(opts.out, patch);
5091
5474
  console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
5092
5475
  } else process.stdout.write(patch);
5093
5476
  })
@@ -5098,6 +5481,43 @@ sessionWorkspace.command("import <ref> <dir>").description("copy a local folder
5098
5481
  console.log(`${imported.path} (${imported.files} files)`);
5099
5482
  })
5100
5483
  );
5484
+ var sessionBrowser = session.command("browser").description("the sandbox browser for a session (start, show status, stop)");
5485
+ sessionBrowser.command("show <ref>").description("show the sandbox browser status for a session").option("--json", "print as JSON").action(
5486
+ run(async (ref, opts) => {
5487
+ const id = await resolveSessionRef(ref);
5488
+ const { browser } = await api("GET", `/sessions/${encodeURIComponent(id)}/browser`);
5489
+ if (opts.json) {
5490
+ console.log(JSON.stringify(browser, null, 2));
5491
+ return;
5492
+ }
5493
+ if (!browser) {
5494
+ console.log("no browser");
5495
+ return;
5496
+ }
5497
+ const b = browser;
5498
+ console.log(`${b.status ?? "unknown"}${b.url ? ` ${b.url}` : ""}${b.entry_url ? ` entry: ${b.entry_url}` : ""}`);
5499
+ })
5500
+ );
5501
+ sessionBrowser.command("start <ref>").description("start a browser for a session (provisions it if not running)").option("--json", "print as JSON").action(
5502
+ run(async (ref, opts) => {
5503
+ const id = await resolveSessionRef(ref);
5504
+ const { browser } = await api("POST", `/sessions/${encodeURIComponent(id)}/browser`, {});
5505
+ if (opts.json) {
5506
+ console.log(JSON.stringify(browser, null, 2));
5507
+ return;
5508
+ }
5509
+ const b = browser;
5510
+ console.log(`${b.status ?? "starting"}${b.entry_url ? ` entry: ${b.entry_url}` : ""}`);
5511
+ })
5512
+ );
5513
+ sessionBrowser.command("stop <ref>").description("stop the browser for a session").action(
5514
+ run(async (ref) => {
5515
+ const id = await resolveSessionRef(ref);
5516
+ const { browser } = await api("DELETE", `/sessions/${encodeURIComponent(id)}/browser`);
5517
+ const b = browser;
5518
+ console.log(b ? b.status ?? "stopped" : "stopped");
5519
+ })
5520
+ );
5101
5521
  function interactionMode(opts) {
5102
5522
  if (opts.plan && opts.build) throw new ApiError(400, "invalid_request", "pass --plan or --build, not both");
5103
5523
  return opts.plan ? "plan" : opts.build ? "build" : void 0;
@@ -5558,7 +5978,7 @@ task.command("get <task> <path>").description("read one task file; prints it, or
5558
5978
  run(async (taskId, path, opts) => {
5559
5979
  const bytes = await readTaskFile(taskId, path);
5560
5980
  if (opts.out) {
5561
- writeFileSync7(opts.out, bytes);
5981
+ writeFileSync8(opts.out, bytes);
5562
5982
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
5563
5983
  } else process.stdout.write(bytes);
5564
5984
  })
@@ -5575,6 +5995,15 @@ task.command("rm <task> <path>").description("remove a path from the task's file
5575
5995
  console.log(`Removed ${path}`);
5576
5996
  })
5577
5997
  );
5998
+ 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(
5999
+ run(async (taskId, opts) => {
6000
+ const tier = opts.tier === "gold" || opts.tier === "exploratory" ? opts.tier : "silver";
6001
+ const outDir = opts.out ?? `./harbor-${taskId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
6002
+ const { written, record } = await exportTaskHarbor(taskId, outDir, { tier, redactNames: parseRedactNames(opts.redact) });
6003
+ 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"})`);
6004
+ if (!written.includes("environment/Dockerfile")) console.log("environment/: not resettable from the record \u2014 see environment/README.md before running with Harbor");
6005
+ })
6006
+ );
5578
6007
  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
6008
  run(async (taskId, opts) => {
5580
6009
  const found = await getTask(taskId);
@@ -5593,6 +6022,42 @@ task.command("answer <task> <request>").description("answer an open request; the
5593
6022
  console.log(`Answered ${requestId}`);
5594
6023
  })
5595
6024
  );
6025
+ 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(
6026
+ run(async (taskId, opts) => {
6027
+ const threads = await listThreads(taskId);
6028
+ if (opts.json) {
6029
+ console.log(JSON.stringify({ threads }));
6030
+ return;
6031
+ }
6032
+ for (const thread of threads) console.log(renderThread(thread));
6033
+ })
6034
+ );
6035
+ 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(
6036
+ run(async (sessionId, kind, opts) => {
6037
+ const value = opts.yes ? true : opts.no ? false : void 0;
6038
+ const { event } = await recordDecision(sessionId, kind, { target: opts.target, value, reason: opts.reason, idempotencyKey: opts.key });
6039
+ console.log(`Recorded ${kind} on ${event.sessionId} (${event.id})`);
6040
+ })
6041
+ );
6042
+ 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(
6043
+ run(async (sessionId, kind, opts) => {
6044
+ const { event } = await recordOutcome(sessionId, kind, { ref: opts.ref, idempotencyKey: opts.key });
6045
+ console.log(`Recorded ${kind} on ${event.sessionId} (${event.id})`);
6046
+ })
6047
+ );
6048
+ 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(
6049
+ run(async (taskId, opts) => {
6050
+ const { task: updated } = await api("POST", `/tasks/${encodeURIComponent(taskId)}/agent-fee/dispute`, { reason: opts.reason, ...opts.session ? { session_id: opts.session } : {} });
6051
+ console.log(`Disputed fee on task ${updated.id}`);
6052
+ })
6053
+ );
6054
+ task.command("verdict <task> <did-the-job>").description("record whether the task did the job: `yes` or `no` (replaces an earlier answer)").action(
6055
+ run(async (taskId, value) => {
6056
+ if (value !== "yes" && value !== "no") throw new Error("value must be yes or no");
6057
+ const { task: updated } = await api("PUT", `/tasks/${encodeURIComponent(taskId)}/completion_answer`, { did_the_job: value === "yes" });
6058
+ console.log(`Recorded verdict on task ${updated.id}: did_the_job=${value === "yes"}`);
6059
+ })
6060
+ );
5596
6061
  var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent by id");
5597
6062
  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
6063
  run(async (dir, opts) => {
@@ -5614,6 +6079,27 @@ skills.command("list").description("the workspace's stored skills").action(
5614
6079
  for (const skill of list) console.log(`${skill.id} ${skill.displayName ?? ""}`);
5615
6080
  })
5616
6081
  );
6082
+ 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(
6083
+ run(async (opts) => {
6084
+ const summary = await getSkillUsage(opts.since);
6085
+ if (opts.json) {
6086
+ console.log(JSON.stringify(summary, null, 2));
6087
+ return;
6088
+ }
6089
+ if (!summary.skills.length) {
6090
+ console.log("No skill usage recorded in this period.");
6091
+ } else {
6092
+ console.log(`${"NAME".padEnd(32)} ${"VIEWS".padStart(6)} ${"MISSES".padStart(7)} LAST SEEN`);
6093
+ for (const s of summary.skills) {
6094
+ console.log(`${s.name.padEnd(32)} ${String(s.views).padStart(6)} ${String(s.misses).padStart(7)} ${s.lastSeen.slice(0, 10)}`);
6095
+ }
6096
+ }
6097
+ if (summary.topMissed.length) {
6098
+ console.log("\nTop missed:");
6099
+ for (const m of summary.topMissed) console.log(` ${m.name} (${m.misses})`);
6100
+ }
6101
+ })
6102
+ );
5617
6103
  async function decideReflection(id, verb) {
5618
6104
  const { reflection_proposal: p } = await api("POST", `/reflection_proposals/${encodeURIComponent(id)}/${verb}`);
5619
6105
  console.log(`${p.id} ${p.status}`);
@@ -5653,7 +6139,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
5653
6139
  run(async (id, path, opts) => {
5654
6140
  const bytes = await readArtifactFile(id, path);
5655
6141
  if (opts.out) {
5656
- writeFileSync7(opts.out, bytes);
6142
+ writeFileSync8(opts.out, bytes);
5657
6143
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
5658
6144
  } else process.stdout.write(bytes);
5659
6145
  })
@@ -5951,7 +6437,7 @@ usageOptions(
5951
6437
  run(async (opts) => {
5952
6438
  const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
5953
6439
  if (opts.out) {
5954
- writeFileSync7(opts.out, bytes);
6440
+ writeFileSync8(opts.out, bytes);
5955
6441
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
5956
6442
  } else {
5957
6443
  process.stdout.write(bytes);
@@ -6161,6 +6647,226 @@ connector.command("shared").description("everything you share with workspaces, a
6161
6647
  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
6648
  })
6163
6649
  );
6650
+ var hookRule = program.command("hook-rule").description("workspace hook rules: run a webhook or block at platform-controlled points");
6651
+ 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(
6652
+ run(async (opts) => {
6653
+ const slug = opts.workspace ?? await workspaceSlug();
6654
+ let data;
6655
+ if (opts.workspace) {
6656
+ data = await api("GET", `/universes/${encodeURIComponent(slug)}/hooks`);
6657
+ } else {
6658
+ data = await api("GET", "/hook_rules");
6659
+ }
6660
+ console.log(opts.json ? JSON.stringify(data, null, 2) : JSON.stringify(data, null, 2));
6661
+ })
6662
+ );
6663
+ 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(
6664
+ run(async (opts) => {
6665
+ const raw = opts.rules ?? await new Promise((resolve10) => {
6666
+ const chunks = [];
6667
+ process.stdin.on("data", (d) => chunks.push(d));
6668
+ process.stdin.on("end", () => resolve10(Buffer.concat(chunks).toString()));
6669
+ });
6670
+ const rules = JSON.parse(raw);
6671
+ const slug = opts.workspace ?? await workspaceSlug();
6672
+ let result;
6673
+ if (opts.workspace) {
6674
+ result = await api("PUT", `/universes/${encodeURIComponent(slug)}/hooks`, { hooks: rules });
6675
+ } else {
6676
+ result = await api("PUT", "/hook_rules", { rules });
6677
+ }
6678
+ console.log(JSON.stringify(result, null, 2));
6679
+ })
6680
+ );
6681
+ 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"}`;
6682
+ var vendor = program.command("vendor").description("vendor accounts: third-party API keys and agent logins your agents use");
6683
+ vendor.command("list").description("your workspace's vendor accounts").option("--json", "print as JSON").action(
6684
+ run(async (opts) => {
6685
+ const { external_accounts: list } = await api("GET", "/external_accounts");
6686
+ if (opts.json) {
6687
+ console.log(JSON.stringify(list, null, 2));
6688
+ return;
6689
+ }
6690
+ if (!list.length) console.log("no vendor accounts");
6691
+ for (const a of list) console.log(vendorLine(a));
6692
+ })
6693
+ );
6694
+ 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(
6695
+ run(async (vendorId, opts) => {
6696
+ let credential = opts.credential ?? "";
6697
+ if (!credential) {
6698
+ credential = await new Promise((resolve10) => {
6699
+ const rl = createInterface6({ input: process.stdin, output: process.stderr });
6700
+ rl.question(`${vendorId} credential: `, (a) => {
6701
+ rl.close();
6702
+ resolve10(a.trim());
6703
+ });
6704
+ });
6705
+ }
6706
+ let parsed = credential;
6707
+ try {
6708
+ parsed = JSON.parse(credential);
6709
+ } catch {
6710
+ }
6711
+ const { external_account: created } = await api("POST", "/external_accounts", { vendor: vendorId, label: opts.label, credential: parsed });
6712
+ console.log(vendorLine(created));
6713
+ })
6714
+ );
6715
+ vendor.command("rm <id>").description("disconnect a vendor account").action(
6716
+ run(async (id) => {
6717
+ await api("DELETE", `/external_accounts/${encodeURIComponent(id)}`);
6718
+ console.log(`Removed ${id}`);
6719
+ })
6720
+ );
6721
+ vendor.command("usage <id>").description("the agents and tasks that used this vendor account").option("--json", "print as JSON").action(
6722
+ run(async (id, opts) => {
6723
+ const { usage } = await api("GET", `/external_accounts/${encodeURIComponent(id)}/usage`);
6724
+ if (opts.json) {
6725
+ console.log(JSON.stringify(usage, null, 2));
6726
+ return;
6727
+ }
6728
+ console.log(`${usage.agents.length} agent${usage.agents.length === 1 ? "" : "s"}, ${usage.tasks.length} task${usage.tasks.length === 1 ? "" : "s"}`);
6729
+ const agents = usage.agents;
6730
+ for (const a of agents) console.log(` agent ${a.id} ${a.name}`);
6731
+ const tasks = usage.tasks;
6732
+ for (const t of tasks) console.log(` task ${t.session_id} ${t.title ?? "(untitled)"} ${t.last_used_at.slice(0, 10)}`);
6733
+ })
6734
+ );
6735
+ var browserProfile = program.command("browser-profile").description("saved-login profiles: the sites your agents stay signed in to");
6736
+ browserProfile.command("list").description("your workspace's browser profiles (requires BROWSER_PROFILES to be enabled)").option("--json", "print as JSON").action(
6737
+ run(async (opts) => {
6738
+ const profiles = await listBrowserProfiles();
6739
+ if (opts.json) {
6740
+ console.log(JSON.stringify(profiles, null, 2));
6741
+ return;
6742
+ }
6743
+ if (!profiles.length) console.log("no browser profiles");
6744
+ for (const p of profiles) console.log(renderBrowserProfile(p));
6745
+ })
6746
+ );
6747
+ browserProfile.command("sites <scope>").description("list the sites in a profile: workspace or private").option("--json", "print as JSON").action(
6748
+ run(async (scope, opts) => {
6749
+ const sites = await listBrowserProfileSites(scope);
6750
+ if (opts.json) {
6751
+ console.log(JSON.stringify(sites, null, 2));
6752
+ return;
6753
+ }
6754
+ if (!sites.length) console.log("no sites");
6755
+ console.log(`${"SITE".padEnd(40)} ${"STATUS".padEnd(14)} SIGNED IN BY LAST USED`);
6756
+ for (const s of sites) console.log(renderBrowserProfileSite(s));
6757
+ })
6758
+ );
6759
+ 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(
6760
+ run(async (scope, opts) => {
6761
+ let raw;
6762
+ if (opts.file) {
6763
+ const { readFileSync: rf } = await import("fs");
6764
+ raw = rf(opts.file, "utf8");
6765
+ } else {
6766
+ raw = await new Promise((resolve10) => {
6767
+ const chunks = [];
6768
+ process.stdin.on("data", (d) => chunks.push(d));
6769
+ process.stdin.on("end", () => resolve10(Buffer.concat(chunks).toString()));
6770
+ });
6771
+ }
6772
+ const state = JSON.parse(raw);
6773
+ const result = await importBrowserProfileState(scope, { state });
6774
+ console.log(`Imported ${result.sites.length} site${result.sites.length === 1 ? "" : "s"}: ${result.sites.join(", ")}`);
6775
+ })
6776
+ );
6777
+ browserProfile.command("remove-site <scope> <site>").description("remove a site from a profile (the agent will need to sign in again)").action(
6778
+ run(async (scope, site) => {
6779
+ await removeBrowserProfileSite(scope, site);
6780
+ console.log(`Removed ${site} from ${scope}`);
6781
+ })
6782
+ );
6783
+ browserProfile.command("move-site <scope> <site> <to>").description("move a site between profiles: <scope> is the current one, <to> is workspace or private").action(
6784
+ run(async (scope, site, to) => {
6785
+ if (to !== "workspace" && to !== "private") throw new ApiError(400, "invalid_request", "to must be workspace or private");
6786
+ const result = await moveBrowserProfileSite(scope, site, to);
6787
+ console.log(`Moved ${site} from ${scope} to ${result.to}`);
6788
+ })
6789
+ );
6790
+ var notifications = program.command("notifications").description("email and browser push notification preferences for tasks");
6791
+ notifications.command("get").description("show the current notification preferences").option("--json", "print as JSON").action(
6792
+ run(async (opts) => {
6793
+ const { notification_preferences: prefs } = await api("GET", "/notification-preferences");
6794
+ if (opts.json) {
6795
+ console.log(JSON.stringify(prefs, null, 2));
6796
+ return;
6797
+ }
6798
+ console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done}`);
6799
+ console.log(`push: needs_you=${prefs.push.needs_you} done=${prefs.push.done}`);
6800
+ })
6801
+ );
6802
+ 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(
6803
+ run(async (opts) => {
6804
+ const parseBool = (v) => v === void 0 ? void 0 : v === "true" ? true : v === "false" ? false : (() => {
6805
+ throw new ApiError(400, "invalid_request", "value must be true or false");
6806
+ })();
6807
+ const email = {};
6808
+ const push = {};
6809
+ const ny_e = parseBool(opts.emailNeedsYou);
6810
+ if (ny_e !== void 0) email.needs_you = ny_e;
6811
+ const d_e = parseBool(opts.emailDone);
6812
+ if (d_e !== void 0) email.done = d_e;
6813
+ const ny_p = parseBool(opts.pushNeedsYou);
6814
+ if (ny_p !== void 0) push.needs_you = ny_p;
6815
+ const d_p = parseBool(opts.pushDone);
6816
+ if (d_p !== void 0) push.done = d_p;
6817
+ if (!Object.keys(email).length && !Object.keys(push).length) throw new ApiError(400, "invalid_request", "pass at least one --email-* or --push-* flag");
6818
+ const { notification_preferences: prefs } = await api("PATCH", "/notification-preferences", {
6819
+ ...Object.keys(email).length ? { email } : {},
6820
+ ...Object.keys(push).length ? { push } : {}
6821
+ });
6822
+ console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done}`);
6823
+ console.log(`push: needs_you=${prefs.push.needs_you} done=${prefs.push.done}`);
6824
+ })
6825
+ );
6826
+ var helpBase = () => agentUseBase().replace(/\/$/, "");
6827
+ async function helpFetch(path) {
6828
+ const res = await fetch(`${helpBase()}${path}`, { headers: { "user-agent": USER_AGENT } });
6829
+ if (!res.ok) {
6830
+ let msg = `${res.status} ${res.statusText}`;
6831
+ try {
6832
+ const b = await res.json();
6833
+ if (b.error) msg = b.error;
6834
+ } catch {
6835
+ }
6836
+ throw new ApiError(res.status, "http_error", msg);
6837
+ }
6838
+ return await res.json();
6839
+ }
6840
+ var help = program.command("help").description("search the AgentSky help center");
6841
+ 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(
6842
+ run(async (words, opts) => {
6843
+ const q = words.join(" ");
6844
+ const topK = Math.min(Math.max(parseInt(opts.top, 10) || 5, 1), 20);
6845
+ const { results } = await helpFetch(`/api/help/search?q=${encodeURIComponent(q)}&top_k=${topK}`);
6846
+ if (opts.json) {
6847
+ console.log(JSON.stringify(results, null, 2));
6848
+ return;
6849
+ }
6850
+ if (!results.length) {
6851
+ console.log("no results");
6852
+ return;
6853
+ }
6854
+ for (const r of results) console.log(`${r.slug.padEnd(40)} [${r.category}] ${r.title}
6855
+ ${r.snippet.slice(0, 100)}`);
6856
+ })
6857
+ );
6858
+ help.command("get <slug>").description("read a help article by its slug").option("--json", "print as JSON").action(
6859
+ run(async (slug, opts) => {
6860
+ const article = await helpFetch(`/api/help/${encodeURIComponent(slug)}`);
6861
+ if (opts.json) {
6862
+ console.log(JSON.stringify(article, null, 2));
6863
+ return;
6864
+ }
6865
+ console.log(`# ${article.title}
6866
+ `);
6867
+ console.log(article.body);
6868
+ })
6869
+ );
6164
6870
  var vault = program.command("vault").description("vaults: credentials a session is granted when it starts (values write-only)");
6165
6871
  vault.command("list").description("the workspace's vaults").action(
6166
6872
  run(async () => {
@@ -6250,7 +6956,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
6250
6956
  }));
6251
6957
  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
6958
  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-QLJH7VK6.js");
6959
+ const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-VZX6UIOF.js");
6254
6960
  if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
6255
6961
  const scopes = parseContentScopes(opts.capture);
6256
6962
  const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
@@ -6285,7 +6991,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
6285
6991
  }));
6286
6992
  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
6993
  try {
6288
- const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-QLJH7VK6.js");
6994
+ const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-VZX6UIOF.js");
6289
6995
  const chunks = [];
6290
6996
  for await (const chunk of process.stdin) chunks.push(chunk);
6291
6997
  const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
@@ -6298,14 +7004,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
6298
7004
  }
6299
7005
  });
6300
7006
  traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
6301
- const { readState, flush, grantSender } = await import("./trace-QLJH7VK6.js");
7007
+ const { readState, flush, grantSender } = await import("./trace-VZX6UIOF.js");
6302
7008
  const state = readState();
6303
7009
  if (!state) throw new Error("no trace is running here");
6304
7010
  const outcome = await flush(grantSender(state), { attempts: 4 });
6305
7011
  console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
6306
7012
  }));
6307
7013
  traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
6308
- const { readState, spoolStatus } = await import("./trace-QLJH7VK6.js");
7014
+ const { readState, spoolStatus } = await import("./trace-VZX6UIOF.js");
6309
7015
  const state = readState();
6310
7016
  if (!state) {
6311
7017
  console.log("No trace is running here.");
@@ -6315,7 +7021,7 @@ traceCmd.command("status").description("the running trace and its spool").action
6315
7021
  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
7022
  }));
6317
7023
  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-QLJH7VK6.js");
7024
+ const { readState } = await import("./trace-VZX6UIOF.js");
6319
7025
  const ref = id ?? readState()?.trace_id;
6320
7026
  if (!ref) throw new Error("name a trace, or start one here");
6321
7027
  const bundle = await api("GET", `/traces/${ref}`);
@@ -6342,7 +7048,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
6342
7048
  for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
6343
7049
  }));
6344
7050
  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-QLJH7VK6.js");
7051
+ const { readState, record, flush, grantSender, clearState } = await import("./trace-VZX6UIOF.js");
6346
7052
  const state = readState();
6347
7053
  if (!state) throw new Error("no trace is running here");
6348
7054
  if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
@@ -6352,7 +7058,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
6352
7058
  console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
6353
7059
  }));
6354
7060
  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-FS5PCBJC.js");
7061
+ const lib = await import("./trace-library-EBH2GYDA.js");
6356
7062
  if (opts.hook) {
6357
7063
  try {
6358
7064
  let raw = payload ?? "";
@@ -6400,10 +7106,10 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
6400
7106
  console.log(`A repository's .agentsky.json "workspace" wins over these rules.`);
6401
7107
  }
6402
7108
  if (opts.installSchedule) {
6403
- const { join: join16 } = await import("path");
7109
+ const { join: join17 } = await import("path");
6404
7110
  const { homedir: homedir8 } = await import("os");
6405
7111
  const { execFileSync } = await import("child_process");
6406
- const log = join16(homedir8(), ".config", "sky", "trace-sync.log");
7112
+ const log = join17(homedir8(), ".config", "sky", "trace-sync.log");
6407
7113
  const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
6408
7114
  try {
6409
7115
  execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
@@ -6414,10 +7120,10 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
6414
7120
  }
6415
7121
  if (opts.status) {
6416
7122
  const { readFileSync: readFileSync16 } = await import("fs");
6417
- const { join: join16 } = await import("path");
7123
+ const { join: join17 } = await import("path");
6418
7124
  let state = { sessions: {} };
6419
7125
  try {
6420
- state = JSON.parse(readFileSync16(join16(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
7126
+ state = JSON.parse(readFileSync16(join17(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
6421
7127
  } catch {
6422
7128
  }
6423
7129
  const counts = /* @__PURE__ */ new Map();
@@ -6450,7 +7156,7 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
6450
7156
  })();
6451
7157
  });
6452
7158
  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-FS5PCBJC.js");
7159
+ const { libraryRequest } = await import("./trace-library-EBH2GYDA.js");
6454
7160
  const workspace2 = opts.workspace ?? currentUniverse();
6455
7161
  if (!workspace2) throw new Error("name a workspace with --workspace");
6456
7162
  const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
@@ -6467,7 +7173,7 @@ traceCmd.command("search <query...>").description("search every session in a wor
6467
7173
  if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
6468
7174
  }));
6469
7175
  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-FS5PCBJC.js");
7176
+ const { libraryRequest } = await import("./trace-library-EBH2GYDA.js");
6471
7177
  const workspace2 = opts.workspace ?? currentUniverse();
6472
7178
  if (!workspace2) throw new Error("name a workspace with --workspace");
6473
7179
  const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);