@agentsky/sky 2.20.2 → 2.22.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.
Files changed (49) hide show
  1. package/dist/{chunk-WTFBVTMI.js → chunk-EBH3KXSU.js} +2 -2
  2. package/dist/{chunk-HB7SPKQ5.js → chunk-XT3W75V3.js} +1 -1
  3. package/dist/{chunk-GHTAIKQ6.js → chunk-YMYQT6KR.js} +70 -34
  4. package/dist/index.js +917 -717
  5. package/dist/{src-QBXMNV2V.js → src-TGRDQ5XR.js} +1 -1
  6. package/dist/{trace-HTWXINXI.js → trace-PDIOL2TC.js} +2 -2
  7. package/dist/{trace-library-QR54Q4Z3.js → trace-library-TA3QUBHJ.js} +97 -11
  8. package/package.json +6 -4
  9. package/vendor/trace-collect/SOURCE.json +6 -0
  10. package/vendor/trace-collect/collect.mjs +75 -0
  11. package/vendor/trace-collect/identity.d.ts +8 -0
  12. package/vendor/trace-collect/identity.mjs +23 -0
  13. package/vendor/trace-collect/muse.mjs +87 -0
  14. package/vendor/trace-collect/normalize.d.ts +6 -0
  15. package/vendor/trace-collect/normalize.mjs +312 -0
  16. package/vendor/trace-collect/scrub.d.ts +2 -0
  17. package/vendor/trace-collect/scrub.mjs +186 -0
  18. package/vendor/trace-collect/sinks/agentsky.mjs +156 -0
  19. package/vendor/trace-collect/sources/_cline-family.mjs +182 -0
  20. package/vendor/trace-collect/sources/_project-root.mjs +40 -0
  21. package/vendor/trace-collect/sources/amp.mjs +18 -0
  22. package/vendor/trace-collect/sources/chatgpt.mjs +203 -0
  23. package/vendor/trace-collect/sources/claude-ai.mjs +94 -0
  24. package/vendor/trace-collect/sources/claude-code.mjs +4 -0
  25. package/vendor/trace-collect/sources/claude-desktop.mjs +87 -0
  26. package/vendor/trace-collect/sources/cline.mjs +14 -0
  27. package/vendor/trace-collect/sources/codex.mjs +4 -0
  28. package/vendor/trace-collect/sources/continue.mjs +117 -0
  29. package/vendor/trace-collect/sources/copilot-chat.mjs +145 -0
  30. package/vendor/trace-collect/sources/cursor-chat.mjs +274 -0
  31. package/vendor/trace-collect/sources/cursor.mjs +4 -0
  32. package/vendor/trace-collect/sources/deepseek.mjs +172 -0
  33. package/vendor/trace-collect/sources/dsh.mjs +145 -0
  34. package/vendor/trace-collect/sources/gemini-cli.mjs +113 -0
  35. package/vendor/trace-collect/sources/gemini.mjs +150 -0
  36. package/vendor/trace-collect/sources/goose.mjs +93 -0
  37. package/vendor/trace-collect/sources/grok-cli.mjs +110 -0
  38. package/vendor/trace-collect/sources/grok.mjs +83 -0
  39. package/vendor/trace-collect/sources/hermes.mjs +4 -0
  40. package/vendor/trace-collect/sources/index.mjs +119 -0
  41. package/vendor/trace-collect/sources/jan.mjs +134 -0
  42. package/vendor/trace-collect/sources/kilo-code.mjs +14 -0
  43. package/vendor/trace-collect/sources/kiro.mjs +77 -0
  44. package/vendor/trace-collect/sources/lm-studio.mjs +84 -0
  45. package/vendor/trace-collect/sources/muse.mjs +11 -0
  46. package/vendor/trace-collect/sources/opencode.mjs +109 -0
  47. package/vendor/trace-collect/sources/qwen-code.mjs +95 -0
  48. package/vendor/trace-collect/sources/roo-code.mjs +14 -0
  49. package/vendor/trace-collect/sources/zed.mjs +70 -0
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveSessionRef,
10
10
  streamEvents,
11
11
  uploadFile
12
- } from "./chunk-WTFBVTMI.js";
12
+ } from "./chunk-EBH3KXSU.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-HB7SPKQ5.js";
23
+ } from "./chunk-XT3W75V3.js";
24
24
  import {
25
25
  AGENT_TYPES,
26
26
  MODEL_SUBSCRIPTION_PLAN_PHRASE,
@@ -42,12 +42,12 @@ import {
42
42
  skillRefSchema,
43
43
  splitMcpServers,
44
44
  supersededBy
45
- } from "./chunk-GHTAIKQ6.js";
45
+ } from "./chunk-YMYQT6KR.js";
46
46
  import "./chunk-MLKGABMK.js";
47
47
 
48
48
  // src/index.ts
49
- import { existsSync as existsSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
50
- import { basename as basename8, resolve as resolve9 } from "path";
49
+ import { existsSync as existsSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
50
+ import { basename as basename9, resolve as resolve9 } from "path";
51
51
  import { createInterface as createInterface6 } from "readline";
52
52
  import { Command } from "commander";
53
53
 
@@ -1697,7 +1697,7 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1697
1697
  const scratch = mkdtempSync(join6(tmpdir(), "sky-environment-"));
1698
1698
  const path = join6(scratch, "environment.tar.gz");
1699
1699
  const leftOut = [];
1700
- const written = /* @__PURE__ */ new Set();
1700
+ const written2 = /* @__PURE__ */ new Set();
1701
1701
  const hash = createHash2("sha256");
1702
1702
  let bytes = 0;
1703
1703
  let redacted2 = 0;
@@ -1764,9 +1764,9 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1764
1764
  }
1765
1765
  await write(tarPadding(stat.size));
1766
1766
  }
1767
- written.add(rel);
1767
+ written2.add(rel);
1768
1768
  }
1769
- const layout = importLayout(resources, cwd, platform, { files: written.size, paths: written });
1769
+ const layout = importLayout(resources, cwd, platform, { files: written2.size, paths: written2 });
1770
1770
  for (const [entry2, data] of layout?.entries ?? []) {
1771
1771
  await write(tarHeader(entry2, data.length));
1772
1772
  await write(data);
@@ -1781,7 +1781,7 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1781
1781
  folder: layout?.folder ?? "imported-session",
1782
1782
  bytes,
1783
1783
  sha256: hash.digest("hex"),
1784
- files: written.size + (layout?.entries.size ?? 0),
1784
+ files: written2.size + (layout?.entries.size ?? 0),
1785
1785
  redacted: redacted2,
1786
1786
  leftOut,
1787
1787
  dispose: () => rmSync2(scratch, { recursive: true, force: true })
@@ -2294,507 +2294,92 @@ import { createHash as createHash4, randomBytes } from "crypto";
2294
2294
  import { createServer } from "http";
2295
2295
  import { hostname } from "os";
2296
2296
  import { spawn as spawn2 } from "child_process";
2297
- function b64url(buf) {
2298
- return buf.toString("base64url");
2299
- }
2300
- function openBrowser2(url) {
2301
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2302
- try {
2303
- spawn2(cmd, [url], { stdio: "ignore", detached: true }).unref();
2304
- } catch {
2305
- }
2297
+
2298
+ // src/mcp-register.ts
2299
+ import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
2300
+ import { spawnSync as spawnSync3 } from "child_process";
2301
+ import { homedir as homedir6 } from "os";
2302
+ import { join as join10 } from "path";
2303
+ import { parse as parseToml3 } from "smol-toml";
2304
+ import { isMap, parseDocument } from "yaml";
2305
+
2306
+ // src/local-agents.ts
2307
+ import { existsSync as existsSync6 } from "fs";
2308
+ import { homedir as homedir5 } from "os";
2309
+ import { join as join8 } from "path";
2310
+ function localAgents(env = process.env, home = homedir5()) {
2311
+ const claude = env.CLAUDE_CONFIG_DIR || join8(home, ".claude");
2312
+ const codex = env.CODEX_HOME || join8(home, ".codex");
2313
+ const cursor = join8(home, ".cursor");
2314
+ return [
2315
+ { id: "claude_code", name: "Claude Code", root: claude, skillsDir: join8(claude, "skills") },
2316
+ { id: "codex", name: "Codex", root: codex, skillsDir: join8(codex, "skills") },
2317
+ { id: "cursor", name: "Cursor", root: cursor, skillsDir: join8(cursor, "skills") }
2318
+ ];
2306
2319
  }
2307
- async function login(opts) {
2308
- const verifier = b64url(randomBytes(32));
2309
- const challenge = b64url(createHash4("sha256").update(verifier).digest());
2310
- const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
2311
- saveConfig({
2312
- ...loadConfig(),
2313
- apiBase: apiBase(),
2314
- token: result.token,
2315
- email: result.user.email ?? void 0,
2316
- currentUniverse: void 0
2317
- // personal by default
2318
- });
2319
- console.log(
2320
- `Logged in as ${result.user.email ?? "you"}${result.universe ? ` (universe: ${result.universe.slug})` : ""}`
2321
- );
2320
+ function detectLocalAgents(env = process.env, home = homedir5()) {
2321
+ return localAgents(env, home).filter((agent2) => existsSync6(agent2.root));
2322
2322
  }
2323
- async function browserLogin(verifier, challenge) {
2324
- const state = b64url(randomBytes(16));
2325
- const code = await new Promise((resolve10, reject) => {
2326
- const server = createServer((req, res) => {
2327
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
2328
- if (url.pathname !== "/callback") {
2329
- res.writeHead(404).end();
2330
- return;
2331
- }
2332
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2333
- res.end("<p>Logged in \u2014 you can close this tab and return to your terminal.</p>");
2334
- const got = url.searchParams.get("code");
2335
- if (url.searchParams.get("state") !== state || !got) {
2336
- reject(new Error("login callback state mismatch"));
2337
- } else {
2338
- resolve10(got);
2339
- }
2340
- server.close();
2341
- });
2342
- server.listen(0, "127.0.0.1", () => {
2343
- const address = server.address();
2344
- const port = typeof address === "object" && address ? address.port : 0;
2345
- const authorizeUrl = `${consoleBase()}/cli/authorize?state=${state}&port=${port}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
2346
- console.log("Opening your browser to authorize this machine\u2026");
2347
- console.log(`If it doesn't open, visit:
2348
- ${authorizeUrl}`);
2349
- openBrowser2(authorizeUrl);
2350
- });
2351
- setTimeout(() => {
2352
- server.close();
2353
- reject(new Error("login timed out after 10 minutes"));
2354
- }, 10 * 60 * 1e3).unref();
2323
+ function selectLocalAgents(ids, env = process.env, home = homedir5()) {
2324
+ if (!ids?.length) return detectLocalAgents(env, home);
2325
+ const known = localAgents(env, home);
2326
+ return ids.map((id) => {
2327
+ const agent2 = known.find((candidate) => candidate.id === id);
2328
+ if (!agent2) throw new Error(`unknown agent ${id} \u2014 expected ${known.map((candidate) => candidate.id).join(" | ")}`);
2329
+ return agent2;
2355
2330
  });
2356
- return api(
2357
- "POST",
2358
- "/cli/token-exchange",
2359
- { code, verifier, clientName: `cli ${hostname()}` },
2360
- { auth: false }
2361
- );
2362
- }
2363
- async function headlessLogin(verifier, challenge) {
2364
- const request = b64url(randomBytes(24));
2365
- const authorizeUrl = `${consoleBase()}/cli/authorize?request=${request}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
2366
- console.log(`Visit this URL on any device to authorize:
2367
- ${authorizeUrl}
2368
- `);
2369
- process.stdout.write("Waiting for approval\u2026");
2370
- const deadline = Date.now() + 10 * 60 * 1e3;
2371
- while (Date.now() < deadline) {
2372
- await new Promise((r) => setTimeout(r, 3e3));
2373
- const res = await fetch(
2374
- `${apiBase()}/cli/poll?request=${request}&verifier=${encodeURIComponent(verifier)}&clientName=${encodeURIComponent(`cli ${hostname()}`)}`,
2375
- { headers: { "user-agent": USER_AGENT } }
2376
- );
2377
- if (!res.ok) {
2378
- const body2 = await res.json().catch(() => null);
2379
- throw new Error(body2?.error?.message ?? `poll failed (${res.status})`);
2380
- }
2381
- const body = await res.json();
2382
- if (body.status === "approved") {
2383
- console.log(" approved.");
2384
- return body;
2385
- }
2386
- process.stdout.write(".");
2387
- }
2388
- throw new Error("login timed out after 10 minutes");
2389
2331
  }
2390
2332
 
2391
- // src/connectors.ts
2392
- async function listApps(query, limit = 50) {
2393
- const apps = [];
2394
- let cursor = null;
2395
- do {
2396
- const params = new URLSearchParams();
2397
- if (query) params.set("q", query);
2398
- if (cursor) params.set("cursor", cursor);
2399
- const qs = params.toString();
2400
- const page = await api("GET", `/connectors/apps${qs ? `?${qs}` : ""}`);
2401
- apps.push(...page.apps);
2402
- cursor = page.next_cursor;
2403
- } while (cursor && apps.length < limit);
2404
- return apps.slice(0, limit);
2405
- }
2406
- function connectionLine2(c) {
2407
- return `${c.id} ${c.app_name} (${c.app}) ${c.label} since ${c.created_at.slice(0, 10)}`;
2333
+ // src/skill.ts
2334
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2335
+ import { join as join9 } from "path";
2336
+
2337
+ // src/catalog-file.ts
2338
+ var CATALOG_FILE = "catalog.md";
2339
+ var CATEGORY_ORDER = ["websites", "content", "video", "code", "research", "general"];
2340
+ function categoryOf(agent2) {
2341
+ return CATEGORY_ORDER.find((category) => agent2.categories.includes(category)) ?? agent2.categories[0] ?? "general";
2408
2342
  }
2409
- function workspaceConnectionLine(c) {
2410
- const owner = c.workspace_owned ? "the workspace" : c.mine ? "you" : c.shared_by?.name ?? c.shared_by?.user_id ?? "someone";
2411
- return `${c.connection_id} ${c.app_name} (${c.app}) ${c.label} shared by ${owner}`;
2343
+ function describe(value) {
2344
+ const parts = Object.entries(value).filter(([, item]) => item !== null && item !== void 0 && item !== false && item !== "").map(([key, item]) => item === true ? key : `${key}: ${typeof item === "string" ? item : JSON.stringify(item)}`);
2345
+ return parts.length ? parts.join(", ") : null;
2412
2346
  }
2413
- function browserReturn(start, opts = {}) {
2414
- return new Promise((resolve10, reject) => {
2415
- const server = createServer2((req, res) => {
2416
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
2417
- if (url.pathname !== "/done" && url.pathname !== "/failed") {
2418
- res.writeHead(404).end();
2419
- return;
2420
- }
2421
- const ok = url.pathname === "/done";
2422
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2423
- res.end(ok ? "<p>Connected \u2014 you can close this tab and return to your terminal.</p>" : "<p>The app was not connected. Return to your terminal.</p>");
2424
- server.close();
2425
- if (ok) resolve10(url.searchParams.get("account_id") ?? void 0);
2426
- else reject(new ApiError(400, "connect_failed", "the app was not connected"));
2427
- });
2428
- server.listen(0, "127.0.0.1", () => {
2429
- const address = server.address();
2430
- const port = typeof address === "object" && address ? address.port : 0;
2431
- const base = `http://127.0.0.1:${port}`;
2432
- start(`${base}/done`, `${base}/failed`).then(
2433
- (authorizeUrl) => {
2434
- console.log(`Authorize access in your browser:
2435
- ${authorizeUrl}`);
2436
- if (opts.open !== false) openBrowser2(authorizeUrl);
2437
- },
2438
- (err) => {
2439
- server.close();
2440
- reject(err);
2441
- }
2442
- );
2443
- });
2444
- setTimeout(() => {
2445
- server.close();
2446
- reject(new ApiError(408, "connect_timeout", "nobody finished the authorization within 10 minutes"));
2447
- }, opts.timeoutMs ?? 10 * 6e4).unref();
2448
- });
2347
+ function entry(agent2) {
2348
+ const engine = agent2.engine_type ? ` \xB7 engine ${agent2.engine_type}` : "";
2349
+ const lines = [`### @${agent2.handle} \u2014 ${agent2.name}${engine}`, "", agent2.tagline];
2350
+ if (agent2.does.length) lines.push("", `Does: ${agent2.does.join("; ")}`);
2351
+ if (agent2.does_not.length) lines.push(`Does not: ${agent2.does_not.join("; ")}`);
2352
+ const { mcp_servers: mcpServers2, ...rest } = agent2.requirements;
2353
+ if (typeof mcpServers2 === "boolean") lines.push(mcpServers2 ? "Can use MCP servers." : "Cannot use MCP servers.");
2354
+ const details = describe(rest);
2355
+ if (details) lines.push(`Details: ${details}`);
2356
+ const pricing = describe(agent2.pricing);
2357
+ if (pricing) lines.push(`Pricing: ${pricing}`);
2358
+ return lines.join("\n");
2449
2359
  }
2450
- async function connectApp(app, opts = {}) {
2451
- const base = opts.workspace ? `/universes/${encodeURIComponent(opts.workspace)}/connections` : "/connectors/connections";
2452
- const accountId = await browserReturn(async (success_url, error_url) => {
2453
- const { url } = await api("POST", base, { app, success_url, error_url });
2454
- return url;
2455
- }, { open: opts.open });
2456
- const done = await api("POST", `${base}/complete`, {
2457
- app,
2458
- ...accountId ? { account_id: accountId } : {}
2360
+ function renderCatalog(agents, generatedAt) {
2361
+ const header2 = [
2362
+ "# AgentSky agent catalog",
2363
+ "",
2364
+ `Every agent you can hire, as of ${generatedAt.toISOString()}. Start one with \`start_session\` and its handle, written with the @.`,
2365
+ "Run `npx @agentsky/sky setup` again to refresh this file; `search_catalog` always has the current list."
2366
+ ];
2367
+ if (!agents.length) return [...header2, "", "The catalog was empty when this file was written. Use `search_catalog`.", ""].join("\n");
2368
+ const own = agents.filter((agent2) => agent2.visibility === "workspace");
2369
+ const shared = agents.filter((agent2) => agent2.visibility !== "workspace");
2370
+ const sections = [];
2371
+ if (own.length) sections.push(["## Your workspace's agents", ...own.map(entry)].join("\n\n"));
2372
+ const groups = /* @__PURE__ */ new Map();
2373
+ for (const agent2 of shared) groups.set(categoryOf(agent2), [...groups.get(categoryOf(agent2)) ?? [], agent2]);
2374
+ const ordered = [...groups.keys()].sort((a, b) => {
2375
+ const rank = (category) => CATEGORY_ORDER.includes(category) ? CATEGORY_ORDER.indexOf(category) : CATEGORY_ORDER.length;
2376
+ return rank(a) - rank(b) || a.localeCompare(b);
2459
2377
  });
2460
- return done.connection;
2461
- }
2462
-
2463
- // src/routine-triggers.ts
2464
- var triggerPath = (id, resource) => `/routines/${encodeURIComponent(id)}/${resource}`;
2465
- function inboundLine(u) {
2466
- if (!u) return "no inbound URL \u2014 make one with --create";
2467
- return u.url ?? `inbound URL ${u.prefix}\u2026 made ${u.created_at.slice(0, 10)}${u.last_used_at ? `, last used ${u.last_used_at.slice(0, 16).replace("T", " ")}` : ", never used"}`;
2468
- }
2469
- function precheckLine(p) {
2470
- if (!p) return "no precheck: every scheduled fire runs";
2471
- const last = p.last_result ? `last check ${p.last_result}${p.last_error ? ` (${p.last_error.type}: ${p.last_error.message})` : ""}` : "not checked yet";
2472
- return `fires only when ${p.url} changes \xB7 ${last}`;
2473
- }
2474
- function triggerLine(t, available = true) {
2475
- if (!t) return "no chat trigger";
2476
- const where = t.thread_id ? `thread ${t.thread_id} on ${t.connection_id}` : `any conversation on ${t.connection_id}`;
2477
- const what = t.text_prefix ? `messages starting "${t.text_prefix}"` : "every message";
2478
- return `${what} in ${where} start a run${available ? "" : " (chat messages are not offered to routines in this deployment)"}`;
2479
- }
2480
- function deliverLine(d) {
2481
- return d ? `answers post to thread ${d.thread_id} on ${d.connection_id}` : "answers are posted nowhere";
2482
- }
2483
- function triggerBody(opts) {
2484
- if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
2485
- return { connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {}, ...opts.prefix ? { text_prefix: opts.prefix } : {} };
2486
- }
2487
- function deliverBody(opts) {
2488
- if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
2489
- return { type: "channel_thread", connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {} };
2490
- }
2491
- async function triggerRequest(id, resource, change) {
2492
- const path = triggerPath(id, resource);
2493
- if (change.remove) return api("DELETE", path);
2494
- if (change.create) return api("POST", path);
2495
- if (change.body) return api("PUT", path, change.body);
2496
- return api("GET", path);
2497
- }
2498
-
2499
- // src/session-workspace.ts
2500
- import { spawnSync as spawnSync3 } from "child_process";
2501
- import { createHash as createHash5, randomUUID } from "crypto";
2502
- import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
2503
- import { basename as basename6, join as join8, relative as relative3, resolve as resolve5 } from "path";
2504
- var SINGLE_IMPORT_MAX = 25 * 1024 * 1024;
2505
- var PART_SIZE = 8 * 1024 * 1024;
2506
- var SKIPPED2 = /* @__PURE__ */ new Set([".git", "node_modules"]);
2507
- var workspacePath = (id) => `/sessions/${encodeURIComponent(id)}/workspace`;
2508
- async function listFiles2(id, path, hidden) {
2509
- const query = new URLSearchParams();
2510
- if (path) query.set("path", path);
2511
- if (hidden) query.set("hidden", "true");
2512
- const qs = query.toString();
2513
- return api("GET", `${workspacePath(id)}/files${qs ? `?${qs}` : ""}`);
2514
- }
2515
- function entryLine(e) {
2516
- const size = e.type === "directory" ? "-" : String(e.size ?? 0);
2517
- return `${e.type === "directory" ? "d" : "-"} ${size.padStart(10)} ${(e.mtime ?? "").slice(0, 16).replace("T", " ")} ${e.name}${e.type === "directory" ? "/" : ""}`;
2518
- }
2519
- async function readFile(id, path) {
2520
- return (await apiBytes("GET", `${workspacePath(id)}/files/content?path=${encodeURIComponent(path)}`)).bytes;
2521
- }
2522
- function renderChanges(changes) {
2523
- if (!changes.repos.length) return [`no git repository under ${changes.root}`];
2524
- return changes.repos.flatMap((r) => [
2525
- `${r.path === "." ? changes.root : r.path}: ${r.status ? "changed" : "clean"}`,
2526
- ...(r.stat ?? r.status ?? "").split("\n").filter(Boolean).map((line) => ` ${line}`)
2527
- ]);
2528
- }
2529
- async function workspacePatch(id, repo) {
2530
- const { patch, ...meta } = await api("POST", `${workspacePath(id)}/patch`, { repo });
2531
- return { meta, patch: Buffer.from(patch, "base64") };
2532
- }
2533
- function folderFiles(dir) {
2534
- const git2 = spawnSync3("git", ["-C", dir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8" });
2535
- const top = spawnSync3("git", ["-C", dir, "rev-parse", "--show-prefix"], { encoding: "utf8" });
2536
- if (git2.status === 0 && top.status === 0) {
2537
- return git2.stdout.split("\0").filter((path) => path && statSync6(join8(dir, path), { throwIfNoEntry: false })?.isFile());
2538
- }
2539
- const files2 = [];
2540
- const walk = (at) => {
2541
- for (const entry2 of readdirSync5(at, { withFileTypes: true })) {
2542
- if (SKIPPED2.has(entry2.name)) continue;
2543
- const full = join8(at, entry2.name);
2544
- if (entry2.isDirectory()) walk(full);
2545
- else if (entry2.isFile()) files2.push(relative3(dir, full));
2546
- }
2547
- };
2548
- walk(dir);
2549
- return files2;
2550
- }
2551
- function folderArchive(dir, files2) {
2552
- const tar = spawnSync3("tar", ["--format=ustar", "-czf", "-", "-C", dir, "-T", "-"], {
2553
- input: files2.join("\n"),
2554
- env: { ...process.env, COPYFILE_DISABLE: "1" },
2555
- maxBuffer: 2 * 1024 * 1024 * 1024
2556
- });
2557
- if (tar.status !== 0) throw new ApiError(400, "archive_failed", `tar failed: ${tar.stderr.toString().trim()}`);
2558
- return tar.stdout;
2559
- }
2560
- async function importFolder(id, dir, opts = {}) {
2561
- const root = resolve5(dir);
2562
- const name = opts.name ?? basename6(root);
2563
- const archive = opts.archive ?? folderArchive(root, folderFiles(root));
2564
- if (archive.length <= SINGLE_IMPORT_MAX) {
2565
- const res = await apiBytes("POST", `${workspacePath(id)}/import?name=${encodeURIComponent(name)}`, { bytes: archive, mediaType: "application/gzip" });
2566
- return res.json();
2567
- }
2568
- const sha256 = createHash5("sha256").update(archive).digest("hex");
2569
- let upload = await api("POST", `${workspacePath(id)}/imports`, { name, size: archive.length, sha256, part_size: PART_SIZE });
2570
- const uploadPath = `${workspacePath(id)}/imports/${encodeURIComponent(upload.id)}`;
2571
- for (let part = 0; part < upload.parts; part += 1) {
2572
- if (upload.received.includes(part)) continue;
2573
- const bytes = archive.subarray(part * upload.part_size, (part + 1) * upload.part_size);
2574
- await apiBytes("PUT", `${uploadPath}/parts/${part}`, { bytes, mediaType: "application/octet-stream" });
2575
- }
2576
- upload = await api("POST", `${uploadPath}/complete`);
2577
- while (upload.state !== "imported" && upload.state !== "failed") {
2578
- await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
2579
- upload = await api("GET", uploadPath);
2580
- }
2581
- if (upload.state === "failed") throw new ApiError(422, upload.error?.code ?? "import_failed", upload.error?.message ?? "the import failed");
2582
- return { path: upload.path ?? name, files: upload.files ?? 0 };
2583
- }
2584
- function renderAccess(a) {
2585
- const lines = [
2586
- `${a.session_id}${a.title ? ` \u2014 ${a.title}` : ""}`,
2587
- ` ${a.visibility === "private" ? "private to its owner" : `visible to ${a.workspace.name} (${a.workspace.slug})`} \xB7 you are its ${a.access}`,
2588
- ` owner: ${a.owner.name ?? a.owner.email ?? "unknown"}`
2589
- ];
2590
- if (a.viewers) lines.push(a.viewers.length ? ` viewers: ${a.viewers.map((v) => v.email).join(", ")}` : " viewers: none");
2591
- return lines;
2592
- }
2593
- function accessPatch(opts) {
2594
- if (opts.private && opts.members) throw new ApiError(400, "invalid_request", "pass --private or --members, not both");
2595
- return {
2596
- ...opts.private ? { visibility: "private" } : opts.members ? { visibility: "workspace" } : {},
2597
- ...opts.addViewer?.length ? { add_viewers: opts.addViewer } : {},
2598
- ...opts.removeViewer?.length ? { remove_viewers: opts.removeViewer } : {}
2599
- };
2600
- }
2601
- function engineChange(model, thinking) {
2602
- if (!model && !thinking) return null;
2603
- return {
2604
- ...model ? { llm: model } : {},
2605
- ...thinking ? { reasoningEffort: thinking === "default" ? null : thinking } : {}
2606
- };
2607
- }
2608
- var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
2609
- async function handoff(id, targetAgent, opts = {}) {
2610
- let { handoff: h } = await api("POST", `/sessions/${encodeURIComponent(id)}/handoff`, {
2611
- target_agent: targetAgent,
2612
- ...opts.fromMessage ? { last_message_id: opts.fromMessage } : {}
2613
- }, { idempotencyKey: opts.idempotencyKey ?? randomUUID() });
2614
- opts.onStatus?.(h);
2615
- while (opts.wait && !TERMINAL.has(h.status)) {
2616
- await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
2617
- const next = (await api("GET", `/session-handoffs/${encodeURIComponent(h.id)}`)).handoff;
2618
- if (next.status !== h.status) opts.onStatus?.(next);
2619
- h = next;
2620
- }
2621
- return h;
2622
- }
2623
-
2624
- // src/local-agents.ts
2625
- import { existsSync as existsSync6 } from "fs";
2626
- import { homedir as homedir5 } from "os";
2627
- import { join as join9 } from "path";
2628
- function localAgents(env = process.env, home = homedir5()) {
2629
- const claude = env.CLAUDE_CONFIG_DIR || join9(home, ".claude");
2630
- const codex = env.CODEX_HOME || join9(home, ".codex");
2631
- const cursor = join9(home, ".cursor");
2632
- return [
2633
- { id: "claude_code", name: "Claude Code", root: claude, skillsDir: join9(claude, "skills") },
2634
- { id: "codex", name: "Codex", root: codex, skillsDir: join9(codex, "skills") },
2635
- { id: "cursor", name: "Cursor", root: cursor, skillsDir: join9(cursor, "skills") }
2636
- ];
2637
- }
2638
- function detectLocalAgents(env = process.env, home = homedir5()) {
2639
- return localAgents(env, home).filter((agent2) => existsSync6(agent2.root));
2640
- }
2641
- function selectLocalAgents(ids, env = process.env, home = homedir5()) {
2642
- if (!ids?.length) return detectLocalAgents(env, home);
2643
- const known = localAgents(env, home);
2644
- return ids.map((id) => {
2645
- const agent2 = known.find((candidate) => candidate.id === id);
2646
- if (!agent2) throw new Error(`unknown agent ${id} \u2014 expected ${known.map((candidate) => candidate.id).join(" | ")}`);
2647
- return agent2;
2648
- });
2649
- }
2650
-
2651
- // src/render.ts
2652
- var dim = (s) => `\x1B[2m${s}\x1B[0m`;
2653
- var red = (s) => `\x1B[31m${s}\x1B[0m`;
2654
- function clip(value, max = 120) {
2655
- const text = typeof value === "string" ? value : JSON.stringify(value);
2656
- return text.length > max ? `${text.slice(0, max)}\u2026` : text;
2657
- }
2658
- function renderEvent(event, data) {
2659
- const part = data.part;
2660
- switch (event) {
2661
- case "agent.message":
2662
- console.log(String(data.text ?? ""));
2663
- break;
2664
- case "agent.reasoning":
2665
- console.log(dim(`\xB7 ${clip(part?.text ?? "")}`));
2666
- break;
2667
- case "agent.tool_use":
2668
- console.log(dim(`\u2192 ${String(part?.tool_name ?? "tool")}(${clip(part?.args ?? {}, 80)})`));
2669
- break;
2670
- case "agent.tool_result":
2671
- console.log(dim(`\u2190 ${String(part?.tool_name ?? "tool")} ${String(part?.status ?? "")}`));
2672
- break;
2673
- case "agent.status":
2674
- console.log(dim(`\u2026 ${String(part?.level ?? "")} ${String(part?.text ?? "")}`.trim()));
2675
- break;
2676
- case "user.interrupt":
2677
- console.log(dim("\u23F9 interrupting"));
2678
- break;
2679
- case "session.error": {
2680
- const error = data.error;
2681
- const message2 = String(error?.message ?? error?.type ?? "unknown");
2682
- if (error?.retry_status === "retrying") console.error(dim(`retrying: ${message2}`));
2683
- else console.error(red(`error: ${message2}`));
2684
- break;
2685
- }
2686
- case "session.status_terminated":
2687
- console.log(dim("session terminated"));
2688
- break;
2689
- default:
2690
- break;
2691
- }
2692
- }
2693
-
2694
- // src/session-read.ts
2695
- var TURN_TYPES = ["user.message", "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
2696
- function latestTurn(events) {
2697
- let start = 0;
2698
- for (let index = events.length - 1; index >= 0; index -= 1) {
2699
- if (events[index].type === "user.message") {
2700
- start = index + 1;
2701
- break;
2702
- }
2703
- }
2704
- const reading = { finished: false, messages: [], errors: [] };
2705
- for (const event of events.slice(start)) {
2706
- if (event.type === "agent.message" && event.text?.trim()) reading.messages.push(event.text);
2707
- else if (event.type === "session.error" && event.error?.retry_status !== "retrying") {
2708
- reading.errors.push(event.error?.message ?? event.error?.type ?? "unknown error");
2709
- } else if (event.type === "session.status_terminated") reading.finished = true;
2710
- else if (event.type === "session.status_idle" && event.stop_reason?.type !== "requires_action") reading.finished = true;
2711
- }
2712
- return reading;
2713
- }
2714
- async function history2(sessionId) {
2715
- const events = [];
2716
- let cursor = null;
2717
- for (; ; ) {
2718
- const query = new URLSearchParams({ limit: "500" });
2719
- for (const type of TURN_TYPES) query.append("types[]", type);
2720
- if (cursor) query.set("cursor", cursor);
2721
- const page = await api(
2722
- "GET",
2723
- `/sessions/${encodeURIComponent(sessionId)}/events?${query}`
2724
- );
2725
- events.push(...page.events);
2726
- if (!page.hasMore || !page.cursor) return events;
2727
- cursor = page.cursor;
2728
- }
2729
- }
2730
- async function readTurn(sessionId, opts = {}) {
2731
- const deadline = Date.now() + (opts.timeoutSeconds ?? 600) * 1e3;
2732
- for (; ; ) {
2733
- const reading = latestTurn(await history2(sessionId));
2734
- if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
2735
- await new Promise((resolve10) => setTimeout(resolve10, opts.intervalMs ?? 3e3));
2736
- }
2737
- }
2738
-
2739
- // src/setup.ts
2740
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
2741
- import { dirname as dirname6, join as join13 } from "path";
2742
- import { createInterface as createInterface3 } from "readline";
2743
-
2744
- // src/mcp-register.ts
2745
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
2746
- import { spawnSync as spawnSync4 } from "child_process";
2747
-
2748
- // src/skill.ts
2749
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2750
- import { join as join10 } from "path";
2751
-
2752
- // src/catalog-file.ts
2753
- var CATALOG_FILE = "catalog.md";
2754
- var CATEGORY_ORDER = ["websites", "content", "video", "code", "research", "general"];
2755
- function categoryOf(agent2) {
2756
- return CATEGORY_ORDER.find((category) => agent2.categories.includes(category)) ?? agent2.categories[0] ?? "general";
2757
- }
2758
- function describe(value) {
2759
- const parts = Object.entries(value).filter(([, item]) => item !== null && item !== void 0 && item !== false && item !== "").map(([key, item]) => item === true ? key : `${key}: ${typeof item === "string" ? item : JSON.stringify(item)}`);
2760
- return parts.length ? parts.join(", ") : null;
2761
- }
2762
- function entry(agent2) {
2763
- const engine = agent2.engine_type ? ` \xB7 engine ${agent2.engine_type}` : "";
2764
- const lines = [`### @${agent2.handle} \u2014 ${agent2.name}${engine}`, "", agent2.tagline];
2765
- if (agent2.does.length) lines.push("", `Does: ${agent2.does.join("; ")}`);
2766
- if (agent2.does_not.length) lines.push(`Does not: ${agent2.does_not.join("; ")}`);
2767
- const { mcp_servers: mcpServers2, ...rest } = agent2.requirements;
2768
- if (typeof mcpServers2 === "boolean") lines.push(mcpServers2 ? "Can use MCP servers." : "Cannot use MCP servers.");
2769
- const details = describe(rest);
2770
- if (details) lines.push(`Details: ${details}`);
2771
- const pricing = describe(agent2.pricing);
2772
- if (pricing) lines.push(`Pricing: ${pricing}`);
2773
- return lines.join("\n");
2774
- }
2775
- function renderCatalog(agents, generatedAt) {
2776
- const header2 = [
2777
- "# AgentSky agent catalog",
2778
- "",
2779
- `Every agent you can hire, as of ${generatedAt.toISOString()}. Start one with \`start_session\` and its handle, written with the @.`,
2780
- "Run `npx @agentsky/sky setup` again to refresh this file; `search_catalog` always has the current list."
2781
- ];
2782
- if (!agents.length) return [...header2, "", "The catalog was empty when this file was written. Use `search_catalog`.", ""].join("\n");
2783
- const own = agents.filter((agent2) => agent2.visibility === "workspace");
2784
- const shared = agents.filter((agent2) => agent2.visibility !== "workspace");
2785
- const sections = [];
2786
- if (own.length) sections.push(["## Your workspace's agents", ...own.map(entry)].join("\n\n"));
2787
- const groups = /* @__PURE__ */ new Map();
2788
- for (const agent2 of shared) groups.set(categoryOf(agent2), [...groups.get(categoryOf(agent2)) ?? [], agent2]);
2789
- const ordered = [...groups.keys()].sort((a, b) => {
2790
- const rank = (category) => CATEGORY_ORDER.includes(category) ? CATEGORY_ORDER.indexOf(category) : CATEGORY_ORDER.length;
2791
- return rank(a) - rank(b) || a.localeCompare(b);
2792
- });
2793
- for (const category of ordered) {
2794
- const title = category.charAt(0).toUpperCase() + category.slice(1);
2795
- sections.push([`## ${title}`, ...groups.get(category).map(entry)].join("\n\n"));
2796
- }
2797
- return [...header2, "", sections.join("\n\n"), ""].join("\n");
2378
+ for (const category of ordered) {
2379
+ const title = category.charAt(0).toUpperCase() + category.slice(1);
2380
+ sections.push([`## ${title}`, ...groups.get(category).map(entry)].join("\n\n"));
2381
+ }
2382
+ return [...header2, "", sections.join("\n\n"), ""].join("\n");
2798
2383
  }
2799
2384
 
2800
2385
  // src/skill.ts
@@ -3172,158 +2757,738 @@ Check one real session:
3172
2757
  - A file id from another customer's workspace is refused at session create.
3173
2758
  `;
3174
2759
  }
3175
- function workspacesReferenceDoc() {
3176
- return `# Workspaces in AgentSky
3177
-
3178
- A workspace (also called a universe) groups agents and sessions. Every API call
3179
- and MCP tool accepts an optional workspace slug.
3180
-
3181
- ## How to choose
3182
-
3183
- 1. The person named one \u2192 use it.
3184
- 2. Check the git root for \`.agentsky.json\` with a \`"workspace"\` key \u2192 use that.
3185
- 3. Neither \u2192 use the account default (personal workspace). Never ask.
3186
-
3187
- ## Applying it
2760
+ function workspacesReferenceDoc() {
2761
+ return `# Workspaces in AgentSky
2762
+
2763
+ A workspace (also called a universe) groups agents and sessions. Every API call
2764
+ and MCP tool accepts an optional workspace slug.
2765
+
2766
+ ## How to choose
2767
+
2768
+ 1. The person named one \u2192 use it.
2769
+ 2. Check the git root for \`.agentsky.json\` with a \`"workspace"\` key \u2192 use that.
2770
+ 3. Neither \u2192 use the account default (personal workspace). Never ask.
2771
+
2772
+ ## Applying it
2773
+
2774
+ - MCP tools: pass \`workspace: "<slug>"\` on tools that accept it.
2775
+ - sky CLI: \`--workspace <slug>\` on any command, or set \`SKY_WORKSPACE=<slug>\`.
2776
+ - Tools that take a \`task_id\` or \`session_id\` inherit that task's workspace
2777
+ automatically; one task never spans workspaces.
2778
+
2779
+ ## Listing workspaces
2780
+
2781
+ \`sky workspace list\` (or \`sky universe list\`) prints your workspaces, with
2782
+ \`*\` marking the current default.
2783
+
2784
+ ## .agentsky.json
2785
+
2786
+ To set a per-repo default, create \`.agentsky.json\` at the git root:
2787
+
2788
+ \`\`\`json
2789
+ {"workspace": "my-team-slug"}
2790
+ \`\`\`
2791
+ `;
2792
+ }
2793
+ function installedVersion(dir) {
2794
+ try {
2795
+ const marker = JSON.parse(readFileSync9(join9(dir, SKILL_MARKER), "utf8"));
2796
+ return typeof marker.version === "string" ? marker.version : null;
2797
+ } catch {
2798
+ return null;
2799
+ }
2800
+ }
2801
+ function isInstalledSkill(dir) {
2802
+ return installedVersion(dir) !== null;
2803
+ }
2804
+ function installSkill(agent2, opts = {}) {
2805
+ const dir = join9(agent2.skillsDir, SKILL_NAME);
2806
+ const previous = installedVersion(dir);
2807
+ const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
2808
+ if (opts.dryRun) return { agent: agent2.id, path: dir, outcome };
2809
+ if (outcome !== "current") {
2810
+ mkdirSync2(dir, { recursive: true });
2811
+ writeFileSync4(join9(dir, "SKILL.md"), skillDocument());
2812
+ writeFileSync4(join9(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
2813
+ `);
2814
+ }
2815
+ if (opts.catalog) {
2816
+ mkdirSync2(dir, { recursive: true });
2817
+ writeFileSync4(join9(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
2818
+ }
2819
+ const refsDir = join9(dir, REFERENCES_DIR);
2820
+ mkdirSync2(refsDir, { recursive: true });
2821
+ writeFileSync4(join9(refsDir, "import.md"), importReferenceDoc());
2822
+ writeFileSync4(join9(refsDir, "workspaces.md"), workspacesReferenceDoc());
2823
+ writeFileSync4(join9(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
2824
+ return { agent: agent2.id, path: dir, outcome };
2825
+ }
2826
+
2827
+ // src/mcp-register.ts
2828
+ var MCP_SERVER_NAME = SKILL_NAME;
2829
+ function detectHermes(env = process.env, home = homedir6()) {
2830
+ const root = env.HERMES_HOME || join10(home, ".hermes");
2831
+ return existsSync7(root) ? { id: "hermes", name: "Hermes", root } : null;
2832
+ }
2833
+ function detectMcpClients(env = process.env, home = homedir6()) {
2834
+ const hermes = detectHermes(env, home);
2835
+ return [...detectLocalAgents(env, home), ...hermes ? [hermes] : []];
2836
+ }
2837
+ function systemCommandRunner() {
2838
+ return {
2839
+ has: (command) => spawnSync3(command, ["--version"], { stdio: "ignore" }).status === 0,
2840
+ run: (command, args) => {
2841
+ const result = spawnSync3(command, args, { encoding: "utf8" });
2842
+ return { status: result.status, stderr: result.stderr ?? "" };
2843
+ }
2844
+ };
2845
+ }
2846
+ function mcpUrlFor(apiBase2) {
2847
+ return `${apiBase2.replace(/\/$/, "")}/mcp`;
2848
+ }
2849
+ function mcpCommands(agent2, opts) {
2850
+ if (agent2 === "claude_code") {
2851
+ return {
2852
+ command: "claude",
2853
+ remove: ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME],
2854
+ add: ["mcp", "add", "--scope", "user", "--transport", "http", MCP_SERVER_NAME, opts.mcpUrl, "--header", `Authorization: Bearer ${opts.token}`]
2855
+ };
2856
+ }
2857
+ return null;
2858
+ }
2859
+ function redact(text, token) {
2860
+ return text.split(token).join(`${token.slice(0, 10)}\u2026`);
2861
+ }
2862
+ function registerMcpServer(client, opts, runner = systemCommandRunner()) {
2863
+ if (client.id === "codex") return registerCodex(client, opts);
2864
+ if (client.id === "cursor") return registerCursor(client, opts);
2865
+ if (client.id === "hermes") return registerHermes(client, opts);
2866
+ const commands = mcpCommands(client.id, opts);
2867
+ if (!commands) return { agent: client.id, outcome: "unsupported" };
2868
+ if (!runner.has(commands.command)) return { agent: client.id, outcome: "no_command", detail: `${commands.command} is not on PATH` };
2869
+ if (opts.dryRun) return { agent: client.id, outcome: "planned", detail: redact(`run ${[commands.command, ...commands.add].join(" ")}`, opts.token) };
2870
+ runner.run(commands.command, commands.remove);
2871
+ const added = runner.run(commands.command, commands.add);
2872
+ if (added.status !== 0) return { agent: client.id, outcome: "failed", detail: redact(added.stderr.trim() || `exit ${added.status}`, opts.token) };
2873
+ return { agent: client.id, outcome: "registered" };
2874
+ }
2875
+ function mcpConfigPath(client, env = process.env, home = homedir6()) {
2876
+ switch (client.id) {
2877
+ case "claude_code":
2878
+ return join10(env.CLAUDE_CONFIG_DIR || home, ".claude.json");
2879
+ case "codex":
2880
+ return join10(client.root, "config.toml");
2881
+ case "cursor":
2882
+ return join10(client.root, "mcp.json");
2883
+ case "hermes":
2884
+ return join10(client.root, "config.yaml");
2885
+ }
2886
+ }
2887
+ function readText(path) {
2888
+ try {
2889
+ return readFileSync10(path, "utf8");
2890
+ } catch {
2891
+ return null;
2892
+ }
2893
+ }
2894
+ function hasMcpServer(client, env = process.env, home = homedir6()) {
2895
+ const raw = readText(mcpConfigPath(client, env, home));
2896
+ if (raw === null) return false;
2897
+ try {
2898
+ if (client.id === "codex") return Boolean(parseToml3(raw).mcp_servers?.[MCP_SERVER_NAME]);
2899
+ if (client.id === "hermes") return parseDocument(raw).hasIn(["mcp_servers", MCP_SERVER_NAME]);
2900
+ return Boolean(JSON.parse(raw).mcpServers?.[MCP_SERVER_NAME]);
2901
+ } catch {
2902
+ return false;
2903
+ }
2904
+ }
2905
+ function planned(client, path, entry2, opts) {
2906
+ return { agent: client.id, outcome: "planned", detail: `write to ${path}: ${redact(entry2, opts.token)}` };
2907
+ }
2908
+ function written(client, path, text) {
2909
+ try {
2910
+ writeFileSync5(path, text, { encoding: "utf8", mode: 384 });
2911
+ } catch (err) {
2912
+ return { agent: client.id, outcome: "failed", detail: String(err) };
2913
+ }
2914
+ return { agent: client.id, outcome: "registered" };
2915
+ }
2916
+ function tomlTableKey(name) {
2917
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2918
+ return `(?:${escaped}|"${escaped}"|'${escaped}')`;
2919
+ }
2920
+ function withoutTomlServer(text, name) {
2921
+ const ours = new RegExp(`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*${tomlTableKey(name)}\\s*(?:\\.[^\\]]*)?\\]\\s*(?:#.*)?$`);
2922
+ let skipping = false;
2923
+ const kept = [];
2924
+ for (const line of text.split(/\r?\n/)) {
2925
+ if (/^\s*\[/.test(line)) skipping = ours.test(line);
2926
+ if (!skipping) kept.push(line);
2927
+ }
2928
+ return kept.join("\n").replace(/\n+$/, "");
2929
+ }
2930
+ function registerCodex(client, opts) {
2931
+ const path = mcpConfigPath(client);
2932
+ const block = [
2933
+ `[mcp_servers.${MCP_SERVER_NAME}]`,
2934
+ `url = ${JSON.stringify(opts.mcpUrl)}`,
2935
+ `http_headers = { Authorization = ${JSON.stringify(`Bearer ${opts.token}`)} }`
2936
+ ].join("\n");
2937
+ if (opts.dryRun) return planned(client, path, block, opts);
2938
+ const raw = readText(path) ?? "";
2939
+ let before;
2940
+ try {
2941
+ before = parseToml3(raw);
2942
+ } catch (err) {
2943
+ return { agent: client.id, outcome: "failed", detail: `${path} is not valid TOML, left as it was (${String(err).split("\n")[0]})` };
2944
+ }
2945
+ const rest = withoutTomlServer(raw, MCP_SERVER_NAME);
2946
+ const text = `${rest ? `${rest}
2947
+
2948
+ ` : ""}${block}
2949
+ `;
2950
+ try {
2951
+ const after = parseToml3(text);
2952
+ const others = Object.keys(before.mcp_servers ?? {}).filter((name) => name !== MCP_SERVER_NAME);
2953
+ if (after.mcp_servers?.[MCP_SERVER_NAME]?.url !== opts.mcpUrl || others.some((name) => !after.mcp_servers?.[name])) throw new Error("the edited file lost an entry");
2954
+ } catch (err) {
2955
+ return { agent: client.id, outcome: "failed", detail: `could not edit ${path} safely, left as it was (${String(err).split("\n")[0]})` };
2956
+ }
2957
+ return written(client, path, text);
2958
+ }
2959
+ function registerCursor(client, opts) {
2960
+ const path = mcpConfigPath(client);
2961
+ const entry2 = { url: opts.mcpUrl, headers: { Authorization: `Bearer ${opts.token}` } };
2962
+ if (opts.dryRun) return planned(client, path, JSON.stringify({ [MCP_SERVER_NAME]: entry2 }, null, 2), opts);
2963
+ let existing = {};
2964
+ try {
2965
+ const parsed = JSON.parse(readText(path) ?? "");
2966
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
2967
+ } catch {
2968
+ existing = {};
2969
+ }
2970
+ const updated = { ...existing, mcpServers: { ...existing.mcpServers ?? {}, [MCP_SERVER_NAME]: entry2 } };
2971
+ return written(client, path, `${JSON.stringify(updated, null, 2)}
2972
+ `);
2973
+ }
2974
+ function registerHermes(client, opts) {
2975
+ const path = mcpConfigPath(client);
2976
+ const entry2 = { url: opts.mcpUrl, headers: { Authorization: `Bearer ${opts.token}` } };
2977
+ if (opts.dryRun) return planned(client, path, `mcp_servers.${MCP_SERVER_NAME}: ${JSON.stringify(entry2)}`, opts);
2978
+ const doc = parseDocument(readText(path) ?? "");
2979
+ if (doc.errors.length) return { agent: client.id, outcome: "failed", detail: `${path} is not valid YAML, left as it was (${doc.errors[0].message.split("\n")[0]})` };
2980
+ if (doc.contents !== null && !isMap(doc.contents)) {
2981
+ return { agent: client.id, outcome: "failed", detail: `${path} is not a YAML mapping, left as it was` };
2982
+ }
2983
+ doc.setIn(["mcp_servers", MCP_SERVER_NAME], doc.createNode(entry2));
2984
+ return written(client, path, doc.toString());
2985
+ }
2986
+ function refreshMcpRegistrations(opts, clients = detectMcpClients(), runner = systemCommandRunner()) {
2987
+ return clients.filter((client) => hasMcpServer(client)).map((client) => registerMcpServer(client, opts, runner));
2988
+ }
3188
2989
 
3189
- - MCP tools: pass \`workspace: "<slug>"\` on tools that accept it.
3190
- - sky CLI: \`--workspace <slug>\` on any command, or set \`SKY_WORKSPACE=<slug>\`.
3191
- - Tools that take a \`task_id\` or \`session_id\` inherit that task's workspace
3192
- automatically; one task never spans workspaces.
2990
+ // src/shell-env.ts
2991
+ import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
2992
+ import { homedir as homedir7 } from "os";
2993
+ import { basename as basename6, dirname as dirname5, join as join11 } from "path";
2994
+ var TOKEN_ENV = "AGENTSKY_API_KEY";
2995
+ var HOOK_MARK = "# AgentSky API token, kept current by sky";
2996
+ function shellEnvFiles() {
2997
+ const dir = dirname5(configPath());
2998
+ return { sh: join11(dir, "env.sh"), fish: join11(dir, "env.fish") };
2999
+ }
3000
+ function quoted(value) {
3001
+ return `'${value.replace(/'/g, `'\\''`)}'`;
3002
+ }
3003
+ function shellStartupFiles(env = process.env, home = homedir7(), platform = process.platform) {
3004
+ const shell = basename6(env.SHELL ?? "");
3005
+ const files2 = [];
3006
+ const zshenv = join11(env.ZDOTDIR || home, ".zshenv");
3007
+ if (shell === "zsh" || platform === "darwin" || existsSync8(zshenv)) files2.push({ path: zshenv, kind: "sh" });
3008
+ const bashrc = join11(home, ".bashrc");
3009
+ if (shell === "bash" || existsSync8(bashrc)) files2.push({ path: bashrc, kind: "sh" });
3010
+ const bashProfile = join11(home, ".bash_profile");
3011
+ if (existsSync8(bashProfile)) files2.push({ path: bashProfile, kind: "sh" });
3012
+ const fish = join11(env.XDG_CONFIG_HOME || join11(home, ".config"), "fish");
3013
+ if (shell === "fish" || existsSync8(fish)) files2.push({ path: join11(fish, "conf.d", "agentsky.fish"), kind: "fish" });
3014
+ return files2;
3015
+ }
3016
+ function writeShellEnv(token, opts = {}) {
3017
+ const { sh, fish } = shellEnvFiles();
3018
+ const startup = shellStartupFiles(opts.env, opts.home, opts.platform);
3019
+ if (opts.dryRun) return { outcome: "planned", files: [sh, fish, ...startup.map((file) => file.path)] };
3020
+ const header2 = "# Written by sky. `sky auth login` and `sky auth logout` rewrite this file.\n";
3021
+ const hooked = [];
3022
+ try {
3023
+ mkdirSync3(dirname5(sh), { recursive: true });
3024
+ writeFileSync6(sh, `${header2}${token ? `export ${TOKEN_ENV}=${quoted(token)}` : `unset ${TOKEN_ENV}`}
3025
+ `, { mode: 384 });
3026
+ writeFileSync6(fish, `${header2}${token ? `set -gx ${TOKEN_ENV} ${quoted(token)}` : `set -e ${TOKEN_ENV}`}
3027
+ `, { mode: 384 });
3028
+ for (const file of startup) {
3029
+ const existing = existsSync8(file.path) ? readFileSync11(file.path, "utf8") : "";
3030
+ if (existing.includes(HOOK_MARK)) continue;
3031
+ const line = file.kind === "fish" ? `${HOOK_MARK}
3032
+ test -f ${quoted(fish)}; and source ${quoted(fish)}
3033
+ ` : `${HOOK_MARK}
3034
+ [ -f ${quoted(sh)} ] && . ${quoted(sh)}
3035
+ `;
3036
+ mkdirSync3(dirname5(file.path), { recursive: true });
3037
+ appendFileSync(file.path, `${existing && !existing.endsWith("\n") ? "\n" : ""}${line}`);
3038
+ hooked.push(file.path);
3039
+ }
3040
+ } catch (err) {
3041
+ return { outcome: "failed", files: [sh, fish, ...hooked], detail: String(err) };
3042
+ }
3043
+ return { outcome: token ? "written" : "cleared", files: [sh, fish, ...hooked] };
3044
+ }
3193
3045
 
3194
- ## Listing workspaces
3046
+ // src/local-access.ts
3047
+ function syncLocalAccess(token, clients = detectMcpClients()) {
3048
+ const lines = [];
3049
+ const shell = writeShellEnv(token);
3050
+ if (shell.outcome === "failed") lines.push(` ${TOKEN_ENV} not updated for new shells: ${shell.detail}`);
3051
+ else lines.push(token ? ` new shells get ${TOKEN_ENV} for this login` : ` new shells no longer get ${TOKEN_ENV}`);
3052
+ if (token) {
3053
+ for (const registered of refreshMcpRegistrations({ token, mcpUrl: mcpUrlFor(apiBase()) }, clients)) {
3054
+ const name = clients.find((client) => client.id === registered.agent)?.name ?? registered.agent;
3055
+ lines.push(registered.outcome === "registered" ? ` agentsky MCP server for ${name} now uses this login` : ` agentsky MCP server for ${name} not updated: ${registered.detail ?? registered.outcome} \u2014 run \`sky setup skill\``);
3056
+ }
3057
+ return lines;
3058
+ }
3059
+ const holding = clients.filter((client) => hasMcpServer(client)).map((client) => client.name);
3060
+ if (holding.length) lines.push(` the agentsky MCP server in ${holding.join(", ")} still uses the old token: \`sky auth login\` replaces it, or revoke the key in Developer \u2192 API keys`);
3061
+ return lines;
3062
+ }
3195
3063
 
3196
- \`sky workspace list\` (or \`sky universe list\`) prints your workspaces, with
3197
- \`*\` marking the current default.
3064
+ // src/login.ts
3065
+ function b64url(buf) {
3066
+ return buf.toString("base64url");
3067
+ }
3068
+ function openBrowser2(url) {
3069
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3070
+ try {
3071
+ spawn2(cmd, [url], { stdio: "ignore", detached: true }).unref();
3072
+ } catch {
3073
+ }
3074
+ }
3075
+ async function login(opts) {
3076
+ const verifier = b64url(randomBytes(32));
3077
+ const challenge = b64url(createHash4("sha256").update(verifier).digest());
3078
+ const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
3079
+ saveConfig({
3080
+ ...loadConfig(),
3081
+ apiBase: apiBase(),
3082
+ token: result.token,
3083
+ email: result.user.email ?? void 0,
3084
+ currentUniverse: void 0
3085
+ // personal by default
3086
+ });
3087
+ console.log(
3088
+ `Logged in as ${result.user.email ?? "you"}${result.universe ? ` (universe: ${result.universe.slug})` : ""}`
3089
+ );
3090
+ for (const line of syncLocalAccess(result.token)) console.log(line);
3091
+ }
3092
+ async function browserLogin(verifier, challenge) {
3093
+ const state = b64url(randomBytes(16));
3094
+ const code = await new Promise((resolve10, reject) => {
3095
+ const server = createServer((req, res) => {
3096
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
3097
+ if (url.pathname !== "/callback") {
3098
+ res.writeHead(404).end();
3099
+ return;
3100
+ }
3101
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
3102
+ res.end("<p>Logged in \u2014 you can close this tab and return to your terminal.</p>");
3103
+ const got = url.searchParams.get("code");
3104
+ if (url.searchParams.get("state") !== state || !got) {
3105
+ reject(new Error("login callback state mismatch"));
3106
+ } else {
3107
+ resolve10(got);
3108
+ }
3109
+ server.close();
3110
+ });
3111
+ server.listen(0, "127.0.0.1", () => {
3112
+ const address = server.address();
3113
+ const port = typeof address === "object" && address ? address.port : 0;
3114
+ const authorizeUrl = `${consoleBase()}/cli/authorize?state=${state}&port=${port}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
3115
+ console.log("Opening your browser to authorize this machine\u2026");
3116
+ console.log(`If it doesn't open, visit:
3117
+ ${authorizeUrl}`);
3118
+ openBrowser2(authorizeUrl);
3119
+ });
3120
+ setTimeout(() => {
3121
+ server.close();
3122
+ reject(new Error("login timed out after 10 minutes"));
3123
+ }, 10 * 60 * 1e3).unref();
3124
+ });
3125
+ return api(
3126
+ "POST",
3127
+ "/cli/token-exchange",
3128
+ { code, verifier, clientName: `cli ${hostname()}` },
3129
+ { auth: false }
3130
+ );
3131
+ }
3132
+ async function headlessLogin(verifier, challenge) {
3133
+ const request = b64url(randomBytes(24));
3134
+ const authorizeUrl = `${consoleBase()}/cli/authorize?request=${request}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
3135
+ console.log(`Visit this URL on any device to authorize:
3136
+ ${authorizeUrl}
3137
+ `);
3138
+ process.stdout.write("Waiting for approval\u2026");
3139
+ const deadline = Date.now() + 10 * 60 * 1e3;
3140
+ while (Date.now() < deadline) {
3141
+ await new Promise((r) => setTimeout(r, 3e3));
3142
+ const res = await fetch(
3143
+ `${apiBase()}/cli/poll?request=${request}&verifier=${encodeURIComponent(verifier)}&clientName=${encodeURIComponent(`cli ${hostname()}`)}`,
3144
+ { headers: { "user-agent": USER_AGENT } }
3145
+ );
3146
+ if (!res.ok) {
3147
+ const body2 = await res.json().catch(() => null);
3148
+ throw new Error(body2?.error?.message ?? `poll failed (${res.status})`);
3149
+ }
3150
+ const body = await res.json();
3151
+ if (body.status === "approved") {
3152
+ console.log(" approved.");
3153
+ return body;
3154
+ }
3155
+ process.stdout.write(".");
3156
+ }
3157
+ throw new Error("login timed out after 10 minutes");
3158
+ }
3198
3159
 
3199
- ## .agentsky.json
3160
+ // src/connectors.ts
3161
+ async function listApps(query, limit = 50) {
3162
+ const apps = [];
3163
+ let cursor = null;
3164
+ do {
3165
+ const params = new URLSearchParams();
3166
+ if (query) params.set("q", query);
3167
+ if (cursor) params.set("cursor", cursor);
3168
+ const qs = params.toString();
3169
+ const page = await api("GET", `/connectors/apps${qs ? `?${qs}` : ""}`);
3170
+ apps.push(...page.apps);
3171
+ cursor = page.next_cursor;
3172
+ } while (cursor && apps.length < limit);
3173
+ return apps.slice(0, limit);
3174
+ }
3175
+ function connectionLine2(c) {
3176
+ return `${c.id} ${c.app_name} (${c.app}) ${c.label} since ${c.created_at.slice(0, 10)}`;
3177
+ }
3178
+ function workspaceConnectionLine(c) {
3179
+ const owner = c.workspace_owned ? "the workspace" : c.mine ? "you" : c.shared_by?.name ?? c.shared_by?.user_id ?? "someone";
3180
+ return `${c.connection_id} ${c.app_name} (${c.app}) ${c.label} shared by ${owner}`;
3181
+ }
3182
+ function browserReturn(start, opts = {}) {
3183
+ return new Promise((resolve10, reject) => {
3184
+ const server = createServer2((req, res) => {
3185
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
3186
+ if (url.pathname !== "/done" && url.pathname !== "/failed") {
3187
+ res.writeHead(404).end();
3188
+ return;
3189
+ }
3190
+ const ok = url.pathname === "/done";
3191
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
3192
+ res.end(ok ? "<p>Connected \u2014 you can close this tab and return to your terminal.</p>" : "<p>The app was not connected. Return to your terminal.</p>");
3193
+ server.close();
3194
+ if (ok) resolve10(url.searchParams.get("account_id") ?? void 0);
3195
+ else reject(new ApiError(400, "connect_failed", "the app was not connected"));
3196
+ });
3197
+ server.listen(0, "127.0.0.1", () => {
3198
+ const address = server.address();
3199
+ const port = typeof address === "object" && address ? address.port : 0;
3200
+ const base = `http://127.0.0.1:${port}`;
3201
+ start(`${base}/done`, `${base}/failed`).then(
3202
+ (authorizeUrl) => {
3203
+ console.log(`Authorize access in your browser:
3204
+ ${authorizeUrl}`);
3205
+ if (opts.open !== false) openBrowser2(authorizeUrl);
3206
+ },
3207
+ (err) => {
3208
+ server.close();
3209
+ reject(err);
3210
+ }
3211
+ );
3212
+ });
3213
+ setTimeout(() => {
3214
+ server.close();
3215
+ reject(new ApiError(408, "connect_timeout", "nobody finished the authorization within 10 minutes"));
3216
+ }, opts.timeoutMs ?? 10 * 6e4).unref();
3217
+ });
3218
+ }
3219
+ async function connectApp(app, opts = {}) {
3220
+ const base = opts.workspace ? `/universes/${encodeURIComponent(opts.workspace)}/connections` : "/connectors/connections";
3221
+ const accountId = await browserReturn(async (success_url, error_url) => {
3222
+ const { url } = await api("POST", base, { app, success_url, error_url });
3223
+ return url;
3224
+ }, { open: opts.open });
3225
+ const done = await api("POST", `${base}/complete`, {
3226
+ app,
3227
+ ...accountId ? { account_id: accountId } : {}
3228
+ });
3229
+ return done.connection;
3230
+ }
3200
3231
 
3201
- To set a per-repo default, create \`.agentsky.json\` at the git root:
3232
+ // src/routine-triggers.ts
3233
+ var triggerPath = (id, resource) => `/routines/${encodeURIComponent(id)}/${resource}`;
3234
+ function inboundLine(u) {
3235
+ if (!u) return "no inbound URL \u2014 make one with --create";
3236
+ return u.url ?? `inbound URL ${u.prefix}\u2026 made ${u.created_at.slice(0, 10)}${u.last_used_at ? `, last used ${u.last_used_at.slice(0, 16).replace("T", " ")}` : ", never used"}`;
3237
+ }
3238
+ function precheckLine(p) {
3239
+ if (!p) return "no precheck: every scheduled fire runs";
3240
+ const last = p.last_result ? `last check ${p.last_result}${p.last_error ? ` (${p.last_error.type}: ${p.last_error.message})` : ""}` : "not checked yet";
3241
+ return `fires only when ${p.url} changes \xB7 ${last}`;
3242
+ }
3243
+ function triggerLine(t, available = true) {
3244
+ if (!t) return "no chat trigger";
3245
+ const where = t.thread_id ? `thread ${t.thread_id} on ${t.connection_id}` : `any conversation on ${t.connection_id}`;
3246
+ const what = t.text_prefix ? `messages starting "${t.text_prefix}"` : "every message";
3247
+ return `${what} in ${where} start a run${available ? "" : " (chat messages are not offered to routines in this deployment)"}`;
3248
+ }
3249
+ function deliverLine(d) {
3250
+ return d ? `answers post to thread ${d.thread_id} on ${d.connection_id}` : "answers are posted nowhere";
3251
+ }
3252
+ function triggerBody(opts) {
3253
+ if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
3254
+ return { connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {}, ...opts.prefix ? { text_prefix: opts.prefix } : {} };
3255
+ }
3256
+ function deliverBody(opts) {
3257
+ if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
3258
+ return { type: "channel_thread", connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {} };
3259
+ }
3260
+ async function triggerRequest(id, resource, change) {
3261
+ const path = triggerPath(id, resource);
3262
+ if (change.remove) return api("DELETE", path);
3263
+ if (change.create) return api("POST", path);
3264
+ if (change.body) return api("PUT", path, change.body);
3265
+ return api("GET", path);
3266
+ }
3202
3267
 
3203
- \`\`\`json
3204
- {"workspace": "my-team-slug"}
3205
- \`\`\`
3206
- `;
3268
+ // src/session-workspace.ts
3269
+ import { spawnSync as spawnSync4 } from "child_process";
3270
+ import { createHash as createHash5, randomUUID } from "crypto";
3271
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
3272
+ import { basename as basename7, join as join12, relative as relative3, resolve as resolve5 } from "path";
3273
+ var SINGLE_IMPORT_MAX = 25 * 1024 * 1024;
3274
+ var PART_SIZE = 8 * 1024 * 1024;
3275
+ var SKIPPED2 = /* @__PURE__ */ new Set([".git", "node_modules"]);
3276
+ var workspacePath = (id) => `/sessions/${encodeURIComponent(id)}/workspace`;
3277
+ async function listFiles2(id, path, hidden) {
3278
+ const query = new URLSearchParams();
3279
+ if (path) query.set("path", path);
3280
+ if (hidden) query.set("hidden", "true");
3281
+ const qs = query.toString();
3282
+ return api("GET", `${workspacePath(id)}/files${qs ? `?${qs}` : ""}`);
3207
3283
  }
3208
- function installedVersion(dir) {
3209
- try {
3210
- const marker = JSON.parse(readFileSync9(join10(dir, SKILL_MARKER), "utf8"));
3211
- return typeof marker.version === "string" ? marker.version : null;
3212
- } catch {
3213
- return null;
3284
+ function entryLine(e) {
3285
+ const size = e.type === "directory" ? "-" : String(e.size ?? 0);
3286
+ return `${e.type === "directory" ? "d" : "-"} ${size.padStart(10)} ${(e.mtime ?? "").slice(0, 16).replace("T", " ")} ${e.name}${e.type === "directory" ? "/" : ""}`;
3287
+ }
3288
+ async function readFile(id, path) {
3289
+ return (await apiBytes("GET", `${workspacePath(id)}/files/content?path=${encodeURIComponent(path)}`)).bytes;
3290
+ }
3291
+ function renderChanges(changes) {
3292
+ if (!changes.repos.length) return [`no git repository under ${changes.root}`];
3293
+ return changes.repos.flatMap((r) => [
3294
+ `${r.path === "." ? changes.root : r.path}: ${r.status ? "changed" : "clean"}`,
3295
+ ...(r.stat ?? r.status ?? "").split("\n").filter(Boolean).map((line) => ` ${line}`)
3296
+ ]);
3297
+ }
3298
+ async function workspacePatch(id, repo) {
3299
+ const { patch, ...meta } = await api("POST", `${workspacePath(id)}/patch`, { repo });
3300
+ return { meta, patch: Buffer.from(patch, "base64") };
3301
+ }
3302
+ function folderFiles(dir) {
3303
+ const git2 = spawnSync4("git", ["-C", dir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8" });
3304
+ const top = spawnSync4("git", ["-C", dir, "rev-parse", "--show-prefix"], { encoding: "utf8" });
3305
+ if (git2.status === 0 && top.status === 0) {
3306
+ return git2.stdout.split("\0").filter((path) => path && statSync6(join12(dir, path), { throwIfNoEntry: false })?.isFile());
3214
3307
  }
3308
+ const files2 = [];
3309
+ const walk = (at) => {
3310
+ for (const entry2 of readdirSync5(at, { withFileTypes: true })) {
3311
+ if (SKIPPED2.has(entry2.name)) continue;
3312
+ const full = join12(at, entry2.name);
3313
+ if (entry2.isDirectory()) walk(full);
3314
+ else if (entry2.isFile()) files2.push(relative3(dir, full));
3315
+ }
3316
+ };
3317
+ walk(dir);
3318
+ return files2;
3215
3319
  }
3216
- function isInstalledSkill(dir) {
3217
- return installedVersion(dir) !== null;
3320
+ function folderArchive(dir, files2) {
3321
+ const tar = spawnSync4("tar", ["--format=ustar", "-czf", "-", "-C", dir, "-T", "-"], {
3322
+ input: files2.join("\n"),
3323
+ env: { ...process.env, COPYFILE_DISABLE: "1" },
3324
+ maxBuffer: 2 * 1024 * 1024 * 1024
3325
+ });
3326
+ if (tar.status !== 0) throw new ApiError(400, "archive_failed", `tar failed: ${tar.stderr.toString().trim()}`);
3327
+ return tar.stdout;
3218
3328
  }
3219
- function installSkill(agent2, opts = {}) {
3220
- const dir = join10(agent2.skillsDir, SKILL_NAME);
3221
- const previous = installedVersion(dir);
3222
- const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
3223
- if (opts.dryRun) return { agent: agent2.id, path: dir, outcome };
3224
- if (outcome !== "current") {
3225
- mkdirSync2(dir, { recursive: true });
3226
- writeFileSync4(join10(dir, "SKILL.md"), skillDocument());
3227
- writeFileSync4(join10(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
3228
- `);
3329
+ async function importFolder(id, dir, opts = {}) {
3330
+ const root = resolve5(dir);
3331
+ const name = opts.name ?? basename7(root);
3332
+ const archive = opts.archive ?? folderArchive(root, folderFiles(root));
3333
+ if (archive.length <= SINGLE_IMPORT_MAX) {
3334
+ const res = await apiBytes("POST", `${workspacePath(id)}/import?name=${encodeURIComponent(name)}`, { bytes: archive, mediaType: "application/gzip" });
3335
+ return res.json();
3229
3336
  }
3230
- if (opts.catalog) {
3231
- mkdirSync2(dir, { recursive: true });
3232
- writeFileSync4(join10(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
3337
+ const sha256 = createHash5("sha256").update(archive).digest("hex");
3338
+ let upload = await api("POST", `${workspacePath(id)}/imports`, { name, size: archive.length, sha256, part_size: PART_SIZE });
3339
+ const uploadPath = `${workspacePath(id)}/imports/${encodeURIComponent(upload.id)}`;
3340
+ for (let part = 0; part < upload.parts; part += 1) {
3341
+ if (upload.received.includes(part)) continue;
3342
+ const bytes = archive.subarray(part * upload.part_size, (part + 1) * upload.part_size);
3343
+ await apiBytes("PUT", `${uploadPath}/parts/${part}`, { bytes, mediaType: "application/octet-stream" });
3233
3344
  }
3234
- const refsDir = join10(dir, REFERENCES_DIR);
3235
- mkdirSync2(refsDir, { recursive: true });
3236
- writeFileSync4(join10(refsDir, "import.md"), importReferenceDoc());
3237
- writeFileSync4(join10(refsDir, "workspaces.md"), workspacesReferenceDoc());
3238
- writeFileSync4(join10(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
3239
- return { agent: agent2.id, path: dir, outcome };
3345
+ upload = await api("POST", `${uploadPath}/complete`);
3346
+ while (upload.state !== "imported" && upload.state !== "failed") {
3347
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
3348
+ upload = await api("GET", uploadPath);
3349
+ }
3350
+ if (upload.state === "failed") throw new ApiError(422, upload.error?.code ?? "import_failed", upload.error?.message ?? "the import failed");
3351
+ return { path: upload.path ?? name, files: upload.files ?? 0 };
3240
3352
  }
3241
-
3242
- // src/mcp-register.ts
3243
- var MCP_SERVER_NAME = SKILL_NAME;
3244
- var CODEX_TOKEN_ENV = "AGENTSKY_API_KEY";
3245
- function systemCommandRunner() {
3353
+ function renderAccess(a) {
3354
+ const lines = [
3355
+ `${a.session_id}${a.title ? ` \u2014 ${a.title}` : ""}`,
3356
+ ` ${a.visibility === "private" ? "private to its owner" : `visible to ${a.workspace.name} (${a.workspace.slug})`} \xB7 you are its ${a.access}`,
3357
+ ` owner: ${a.owner.name ?? a.owner.email ?? "unknown"}`
3358
+ ];
3359
+ if (a.viewers) lines.push(a.viewers.length ? ` viewers: ${a.viewers.map((v) => v.email).join(", ")}` : " viewers: none");
3360
+ return lines;
3361
+ }
3362
+ function accessPatch(opts) {
3363
+ if (opts.private && opts.members) throw new ApiError(400, "invalid_request", "pass --private or --members, not both");
3246
3364
  return {
3247
- has: (command) => spawnSync4(command, ["--version"], { stdio: "ignore" }).status === 0,
3248
- run: (command, args) => {
3249
- const result = spawnSync4(command, args, { encoding: "utf8" });
3250
- return { status: result.status, stderr: result.stderr ?? "" };
3251
- }
3365
+ ...opts.private ? { visibility: "private" } : opts.members ? { visibility: "workspace" } : {},
3366
+ ...opts.addViewer?.length ? { add_viewers: opts.addViewer } : {},
3367
+ ...opts.removeViewer?.length ? { remove_viewers: opts.removeViewer } : {}
3252
3368
  };
3253
3369
  }
3254
- function mcpUrlFor(apiBase2) {
3255
- return `${apiBase2.replace(/\/$/, "")}/mcp`;
3370
+ function engineChange(model, thinking) {
3371
+ if (!model && !thinking) return null;
3372
+ return {
3373
+ ...model ? { llm: model } : {},
3374
+ ...thinking ? { reasoningEffort: thinking === "default" ? null : thinking } : {}
3375
+ };
3256
3376
  }
3257
- function mcpCommands(agent2, opts) {
3258
- if (agent2 === "claude_code") {
3259
- return {
3260
- command: "claude",
3261
- remove: ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME],
3262
- add: ["mcp", "add", "--scope", "user", "--transport", "http", MCP_SERVER_NAME, opts.mcpUrl, "--header", `Authorization: Bearer ${opts.token}`]
3263
- };
3264
- }
3265
- if (agent2 === "codex") {
3266
- return {
3267
- command: "codex",
3268
- remove: ["mcp", "remove", MCP_SERVER_NAME],
3269
- add: ["mcp", "add", MCP_SERVER_NAME, "--url", opts.mcpUrl, "--bearer-token-env-var", CODEX_TOKEN_ENV]
3270
- };
3377
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
3378
+ async function handoff(id, targetAgent, opts = {}) {
3379
+ let { handoff: h } = await api("POST", `/sessions/${encodeURIComponent(id)}/handoff`, {
3380
+ target_agent: targetAgent,
3381
+ ...opts.fromMessage ? { last_message_id: opts.fromMessage } : {}
3382
+ }, { idempotencyKey: opts.idempotencyKey ?? randomUUID() });
3383
+ opts.onStatus?.(h);
3384
+ while (opts.wait && !TERMINAL.has(h.status)) {
3385
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
3386
+ const next = (await api("GET", `/session-handoffs/${encodeURIComponent(h.id)}`)).handoff;
3387
+ if (next.status !== h.status) opts.onStatus?.(next);
3388
+ h = next;
3271
3389
  }
3272
- return null;
3390
+ return h;
3273
3391
  }
3274
- function redact(text, token) {
3275
- return text.split(token).join(`${token.slice(0, 10)}\u2026`);
3392
+
3393
+ // src/render.ts
3394
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
3395
+ var red = (s) => `\x1B[31m${s}\x1B[0m`;
3396
+ function clip(value, max = 120) {
3397
+ const text = typeof value === "string" ? value : JSON.stringify(value);
3398
+ return text.length > max ? `${text.slice(0, max)}\u2026` : text;
3276
3399
  }
3277
- function registerMcpServer(agent2, opts, runner = systemCommandRunner()) {
3278
- if (agent2.id === "cursor") {
3279
- return registerCursorMcpServer(agent2, opts);
3280
- }
3281
- const commands = mcpCommands(agent2.id, opts);
3282
- if (!commands) return { agent: agent2.id, outcome: "unsupported" };
3283
- if (!runner.has(commands.command)) return { agent: agent2.id, outcome: "no_command", detail: `${commands.command} is not on PATH` };
3284
- if (opts.dryRun) return { agent: agent2.id, outcome: "planned", detail: redact([commands.command, ...commands.add].join(" "), opts.token) };
3285
- runner.run(commands.command, commands.remove);
3286
- const added = runner.run(commands.command, commands.add);
3287
- if (added.status !== 0) return { agent: agent2.id, outcome: "failed", detail: redact(added.stderr.trim() || `exit ${added.status}`, opts.token) };
3288
- return agent2.id === "codex" ? { agent: agent2.id, outcome: "registered", detail: `set ${CODEX_TOKEN_ENV} to an AgentSky API token in the shell that runs codex` } : { agent: agent2.id, outcome: "registered" };
3289
- }
3290
- function registerCursorMcpServer(agent2, opts) {
3291
- const configPath2 = `${agent2.root}/mcp.json`;
3292
- const entry2 = {
3293
- url: opts.mcpUrl,
3294
- headers: { Authorization: `Bearer ${opts.token}` }
3295
- };
3296
- if (opts.dryRun) {
3297
- const preview = JSON.stringify({ agentsky: { url: opts.mcpUrl, headers: { Authorization: `Bearer ${opts.token.slice(0, 10)}\u2026` } } }, null, 2);
3298
- return { agent: agent2.id, outcome: "planned", detail: `would write to ${configPath2}: ${preview}` };
3400
+ function renderEvent(event, data) {
3401
+ const part = data.part;
3402
+ switch (event) {
3403
+ case "agent.message":
3404
+ console.log(String(data.text ?? ""));
3405
+ break;
3406
+ case "agent.reasoning":
3407
+ console.log(dim(`\xB7 ${clip(part?.text ?? "")}`));
3408
+ break;
3409
+ case "agent.tool_use":
3410
+ console.log(dim(`\u2192 ${String(part?.tool_name ?? "tool")}(${clip(part?.args ?? {}, 80)})`));
3411
+ break;
3412
+ case "agent.tool_result":
3413
+ console.log(dim(`\u2190 ${String(part?.tool_name ?? "tool")} ${String(part?.status ?? "")}`));
3414
+ break;
3415
+ case "agent.status":
3416
+ console.log(dim(`\u2026 ${String(part?.level ?? "")} ${String(part?.text ?? "")}`.trim()));
3417
+ break;
3418
+ case "user.interrupt":
3419
+ console.log(dim("\u23F9 interrupting"));
3420
+ break;
3421
+ case "session.error": {
3422
+ const error = data.error;
3423
+ const message2 = String(error?.message ?? error?.type ?? "unknown");
3424
+ if (error?.retry_status === "retrying") console.error(dim(`retrying: ${message2}`));
3425
+ else console.error(red(`error: ${message2}`));
3426
+ break;
3427
+ }
3428
+ case "session.status_terminated":
3429
+ console.log(dim("session terminated"));
3430
+ break;
3431
+ default:
3432
+ break;
3299
3433
  }
3300
- let existing = { mcpServers: {} };
3301
- try {
3302
- const raw = readFileSync10(configPath2, "utf8");
3303
- const parsed = JSON.parse(raw);
3304
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3305
- existing = parsed;
3434
+ }
3435
+
3436
+ // src/session-read.ts
3437
+ var TURN_TYPES = ["user.message", "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
3438
+ function latestTurn(events) {
3439
+ let start = 0;
3440
+ for (let index = events.length - 1; index >= 0; index -= 1) {
3441
+ if (events[index].type === "user.message") {
3442
+ start = index + 1;
3443
+ break;
3306
3444
  }
3307
- } catch {
3308
3445
  }
3309
- const updated = {
3310
- ...existing,
3311
- mcpServers: { ...existing.mcpServers ?? {}, [MCP_SERVER_NAME]: entry2 }
3312
- };
3313
- try {
3314
- writeFileSync5(configPath2, JSON.stringify(updated, null, 2) + "\n", "utf8");
3315
- } catch (err) {
3316
- return { agent: agent2.id, outcome: "failed", detail: String(err) };
3446
+ const reading = { finished: false, messages: [], errors: [] };
3447
+ for (const event of events.slice(start)) {
3448
+ if (event.type === "agent.message" && event.text?.trim()) reading.messages.push(event.text);
3449
+ else if (event.type === "session.error" && event.error?.retry_status !== "retrying") {
3450
+ reading.errors.push(event.error?.message ?? event.error?.type ?? "unknown error");
3451
+ } else if (event.type === "session.status_terminated") reading.finished = true;
3452
+ else if (event.type === "session.status_idle" && event.stop_reason?.type !== "requires_action") reading.finished = true;
3453
+ }
3454
+ return reading;
3455
+ }
3456
+ async function history2(sessionId) {
3457
+ const events = [];
3458
+ let cursor = null;
3459
+ for (; ; ) {
3460
+ const query = new URLSearchParams({ limit: "500" });
3461
+ for (const type of TURN_TYPES) query.append("types[]", type);
3462
+ if (cursor) query.set("cursor", cursor);
3463
+ const page = await api(
3464
+ "GET",
3465
+ `/sessions/${encodeURIComponent(sessionId)}/events?${query}`
3466
+ );
3467
+ events.push(...page.events);
3468
+ if (!page.hasMore || !page.cursor) return events;
3469
+ cursor = page.cursor;
3470
+ }
3471
+ }
3472
+ async function readTurn(sessionId, opts = {}) {
3473
+ const deadline = Date.now() + (opts.timeoutSeconds ?? 600) * 1e3;
3474
+ for (; ; ) {
3475
+ const reading = latestTurn(await history2(sessionId));
3476
+ if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
3477
+ await new Promise((resolve10) => setTimeout(resolve10, opts.intervalMs ?? 3e3));
3317
3478
  }
3318
- return { agent: agent2.id, outcome: "registered" };
3319
3479
  }
3320
3480
 
3481
+ // src/setup.ts
3482
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
3483
+ import { dirname as dirname7, join as join15 } from "path";
3484
+ import { createInterface as createInterface3 } from "readline";
3485
+
3321
3486
  // src/setup-local.ts
3322
3487
  import { isUtf8 as isUtf83 } from "buffer";
3323
- import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync7 } from "fs";
3324
- import { homedir as homedir6 } from "os";
3325
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join11, resolve as resolve6 } from "path";
3326
- import { parse as parseToml3 } from "smol-toml";
3488
+ import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
3489
+ import { homedir as homedir8 } from "os";
3490
+ import { dirname as dirname6, isAbsolute as isAbsolute2, join as join13, resolve as resolve6 } from "path";
3491
+ import { parse as parseToml4 } from "smol-toml";
3327
3492
  var MAX_SKILL_FILE_BYTES = 1024 * 1024;
3328
3493
  var MAX_IMPORT_DEPTH = 5;
3329
3494
  var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
@@ -3347,11 +3512,11 @@ function redacted(data) {
3347
3512
  function skillFiles2(root, prefix) {
3348
3513
  const files2 = [];
3349
3514
  for (const entry2 of readdirSync6(root, { withFileTypes: true })) {
3350
- const path = join11(root, entry2.name);
3515
+ const path = join13(root, entry2.name);
3351
3516
  if (entry2.isDirectory()) {
3352
3517
  if (!SKIPPED_DIRS.has(entry2.name)) files2.push(...skillFiles2(path, `${prefix}/${entry2.name}`));
3353
3518
  } else if (entry2.isFile() && !sensitive(entry2.name) && statSync7(path).size <= MAX_SKILL_FILE_BYTES) {
3354
- const bytes = redacted(readFileSync11(path));
3519
+ const bytes = redacted(readFileSync12(path));
3355
3520
  if (bytes) files2.push({ path: `${prefix}/${entry2.name}`, bytes });
3356
3521
  }
3357
3522
  }
@@ -3361,9 +3526,9 @@ function readLocalSkills(agents) {
3361
3526
  const skills2 = [];
3362
3527
  const ignored = [];
3363
3528
  for (const agent2 of agents) {
3364
- if (!existsSync7(agent2.skillsDir)) continue;
3529
+ if (!existsSync9(agent2.skillsDir)) continue;
3365
3530
  for (const entry2 of readdirSync6(agent2.skillsDir, { withFileTypes: true })) {
3366
- const dir = join11(agent2.skillsDir, entry2.name);
3531
+ const dir = join13(agent2.skillsDir, entry2.name);
3367
3532
  let isDir2 = entry2.isDirectory();
3368
3533
  if (entry2.isSymbolicLink()) {
3369
3534
  try {
@@ -3372,8 +3537,8 @@ function readLocalSkills(agents) {
3372
3537
  isDir2 = false;
3373
3538
  }
3374
3539
  }
3375
- if (!isDir2 || !existsSync7(join11(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
3376
- const name = frontmatterName(readFileSync11(join11(dir, "SKILL.md"), "utf8"));
3540
+ if (!isDir2 || !existsSync9(join13(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
3541
+ const name = frontmatterName(readFileSync12(join13(dir, "SKILL.md"), "utf8"));
3377
3542
  if (!name) {
3378
3543
  ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
3379
3544
  continue;
@@ -3401,11 +3566,11 @@ function inlineImports(content, from, home, depth, seen) {
3401
3566
  }
3402
3567
  const target = fence ? void 0 : line.match(/^@(\S+\.(?:md|markdown|txt))$/i)?.[1];
3403
3568
  if (!target || depth >= MAX_IMPORT_DEPTH) return line;
3404
- const path = target.startsWith("~/") ? join11(home, target.slice(2)) : isAbsolute2(target) ? target : resolve6(dirname5(from), target);
3569
+ const path = target.startsWith("~/") ? join13(home, target.slice(2)) : isAbsolute2(target) ? target : resolve6(dirname6(from), target);
3405
3570
  if (seen.includes(path)) return "";
3406
3571
  let imported;
3407
3572
  try {
3408
- imported = readFileSync11(path, "utf8");
3573
+ imported = readFileSync12(path, "utf8");
3409
3574
  } catch {
3410
3575
  return line;
3411
3576
  }
@@ -3413,15 +3578,15 @@ function inlineImports(content, from, home, depth, seen) {
3413
3578
  return inlineImports(imported, path, home, depth + 1, seen).trim();
3414
3579
  }).join("\n");
3415
3580
  }
3416
- function readGlobalInstructions(agents, home = homedir6()) {
3581
+ function readGlobalInstructions(agents, home = homedir8()) {
3417
3582
  const found = [];
3418
3583
  for (const agent2 of agents) {
3419
3584
  const file = INSTRUCTION_FILES2[agent2.id];
3420
3585
  if (!file) continue;
3421
- const path = join11(agent2.root, file);
3586
+ const path = join13(agent2.root, file);
3422
3587
  let raw;
3423
3588
  try {
3424
- raw = readFileSync11(path, "utf8");
3589
+ raw = readFileSync12(path, "utf8");
3425
3590
  } catch {
3426
3591
  continue;
3427
3592
  }
@@ -3440,7 +3605,7 @@ ${item.content.trim()}`).join("\n\n");
3440
3605
  }
3441
3606
  function readJson3(path) {
3442
3607
  try {
3443
- return JSON.parse(readFileSync11(path, "utf8"));
3608
+ return JSON.parse(readFileSync12(path, "utf8"));
3444
3609
  } catch {
3445
3610
  return void 0;
3446
3611
  }
@@ -3454,18 +3619,19 @@ function bearerOf(def) {
3454
3619
  return match?.[1] ?? null;
3455
3620
  }
3456
3621
  function serversOf(agent2, home) {
3457
- if (agent2.id === "claude_code") return readJson3(join11(home, ".claude.json"))?.mcpServers ?? {};
3458
- if (agent2.id === "cursor") return readJson3(join11(agent2.root, "mcp.json"))?.mcpServers ?? {};
3622
+ if (agent2.id === "claude_code") return readJson3(join13(home, ".claude.json"))?.mcpServers ?? {};
3623
+ if (agent2.id === "cursor") return readJson3(join13(agent2.root, "mcp.json"))?.mcpServers ?? {};
3459
3624
  try {
3460
- return parseToml3(readFileSync11(join11(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
3625
+ return parseToml4(readFileSync12(join13(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
3461
3626
  } catch {
3462
3627
  return {};
3463
3628
  }
3464
3629
  }
3465
- function findLocalSecrets(agents, home = homedir6()) {
3630
+ function findLocalSecrets(agents, home = homedir8()) {
3466
3631
  const secrets2 = [];
3467
3632
  for (const agent2 of agents) {
3468
3633
  for (const [name, def] of Object.entries(serversOf(agent2, home))) {
3634
+ if (name === SKILL_NAME) continue;
3469
3635
  if (!def || typeof def !== "object" || typeof def.url !== "string" || !/^https:\/\//i.test(def.url)) continue;
3470
3636
  const token = bearerOf(def);
3471
3637
  if (!token || /^\$\{?[A-Za-z_]/.test(token)) continue;
@@ -3478,9 +3644,9 @@ function findLocalSecrets(agents, home = homedir6()) {
3478
3644
 
3479
3645
  // src/subscriptions.ts
3480
3646
  import { spawn as spawn3 } from "child_process";
3481
- import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
3482
- import { homedir as homedir7 } from "os";
3483
- import { join as join12 } from "path";
3647
+ import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
3648
+ import { homedir as homedir9 } from "os";
3649
+ import { join as join14 } from "path";
3484
3650
  import { createInterface as createInterface2 } from "readline";
3485
3651
  var ALIASES = {
3486
3652
  claude: "anthropic",
@@ -3575,12 +3741,12 @@ async function connectClaude() {
3575
3741
  const expiry = sub.expiresAt ? ` (expires ~${sub.expiresAt.slice(0, 10)})` : "";
3576
3742
  console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
3577
3743
  }
3578
- function codexSignIn(env = process.env, home = homedir7()) {
3579
- const path = join12(env.CODEX_HOME || join12(home, ".codex"), "auth.json");
3580
- if (!existsSync8(path)) {
3744
+ function codexSignIn(env = process.env, home = homedir9()) {
3745
+ const path = join14(env.CODEX_HOME || join14(home, ".codex"), "auth.json");
3746
+ if (!existsSync10(path)) {
3581
3747
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
3582
3748
  }
3583
- const raw = readFileSync12(path, "utf8");
3749
+ const raw = readFileSync13(path, "utf8");
3584
3750
  try {
3585
3751
  const parsed = JSON.parse(raw);
3586
3752
  return { path, raw, account: parsed.tokens?.account_id };
@@ -3588,12 +3754,12 @@ function codexSignIn(env = process.env, home = homedir7()) {
3588
3754
  throw new ApiError(400, "invalid_codex_auth", `${path} isn't valid JSON \u2014 re-run \`codex login\``);
3589
3755
  }
3590
3756
  }
3591
- function cursorSignIn(env = process.env, home = homedir7()) {
3592
- const path = join12(env.XDG_CONFIG_HOME || join12(home, ".config"), "cursor", "auth.json");
3593
- if (!existsSync8(path)) {
3757
+ function cursorSignIn(env = process.env, home = homedir9()) {
3758
+ const path = join14(env.XDG_CONFIG_HOME || join14(home, ".config"), "cursor", "auth.json");
3759
+ if (!existsSync10(path)) {
3594
3760
  throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
3595
3761
  }
3596
- const raw = readFileSync12(path, "utf8");
3762
+ const raw = readFileSync13(path, "utf8");
3597
3763
  try {
3598
3764
  const parsed = JSON.parse(raw);
3599
3765
  if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") throw new Error("shape");
@@ -3752,11 +3918,11 @@ function planProviders(names2) {
3752
3918
  }));
3753
3919
  }
3754
3920
  function reportPath() {
3755
- return join13(dirname6(configPath()), "setup.json");
3921
+ return join15(dirname7(configPath()), "setup.json");
3756
3922
  }
3757
3923
  function loadReport() {
3758
3924
  try {
3759
- const saved = JSON.parse(readFileSync13(reportPath(), "utf8"));
3925
+ const saved = JSON.parse(readFileSync14(reportPath(), "utf8"));
3760
3926
  return { agents: saved.agents ?? [], plans: saved.plans ?? [], ...saved.secrets ? { secrets: saved.secrets } : {} };
3761
3927
  } catch {
3762
3928
  return { agents: [], plans: [] };
@@ -3769,8 +3935,8 @@ function mergeReport(base, next) {
3769
3935
  return { agents, plans, ...secrets2 ? { secrets: secrets2 } : {} };
3770
3936
  }
3771
3937
  function saveReport(report) {
3772
- mkdirSync3(dirname6(reportPath()), { recursive: true });
3773
- writeFileSync6(reportPath(), `${JSON.stringify(report, null, 2)}
3938
+ mkdirSync4(dirname7(reportPath()), { recursive: true });
3939
+ writeFileSync7(reportPath(), `${JSON.stringify(report, null, 2)}
3774
3940
  `, { mode: 384 });
3775
3941
  }
3776
3942
  function confirm3(question) {
@@ -3788,7 +3954,7 @@ function kb2(bytes) {
3788
3954
  function describe2(event) {
3789
3955
  switch (event.type) {
3790
3956
  case "agents":
3791
- return event.agents.length ? `Found on this computer: ${event.agents.map((agent2) => agent2.name).join(", ")}` : "No agent found on this computer (looked for Claude Code, Codex and Cursor).";
3957
+ return event.agents.length ? `Found on this computer: ${event.agents.map((agent2) => agent2.name).join(", ")}` : "No agent found on this computer (looked for Claude Code, Codex, Cursor and Hermes).";
3792
3958
  case "catalog":
3793
3959
  return event.outcome === "read" ? ` agent catalog: ${event.agents} agent${event.agents === 1 ? "" : "s"}` : ` agent catalog: not read${event.detail ? ` (${event.detail})` : ""}; the skill looks agents up with search_catalog`;
3794
3960
  case "skill":
@@ -3798,12 +3964,23 @@ function describe2(event) {
3798
3964
  case "registered":
3799
3965
  return ` agentsky MCP server registered for ${event.agent}${event.detail ? ` (${event.detail})` : ""}`;
3800
3966
  case "planned":
3801
- return ` would run: ${event.detail}`;
3967
+ return ` ${event.agent}: would ${event.detail}`;
3802
3968
  case "unsupported":
3803
3969
  return null;
3804
3970
  default:
3805
3971
  return ` agentsky MCP server not registered for ${event.agent}: ${event.detail ?? event.outcome}`;
3806
3972
  }
3973
+ case "env":
3974
+ switch (event.outcome) {
3975
+ case "written":
3976
+ return ` new shells get ${event.variable} for this login (open a new terminal, or run: . ${event.files[0]})`;
3977
+ case "planned":
3978
+ return ` would write ${event.variable} to ${event.files.join(", ")}`;
3979
+ case "cleared":
3980
+ return null;
3981
+ default:
3982
+ return ` ${event.variable} not set for new shells: ${event.detail ?? event.outcome}`;
3983
+ }
3807
3984
  case "plan":
3808
3985
  return ` ${event.provider} plan: ${event.outcome === "already" ? "already connected" : event.outcome}${event.detail ? ` (${event.detail})` : ""}`;
3809
3986
  case "found": {
@@ -3893,8 +4070,11 @@ async function runSetup(opts) {
3893
4070
  };
3894
4071
  const agreed = async (question) => !interactive || confirm3(question);
3895
4072
  const has = (step) => steps.includes(step);
3896
- const agents = selectLocalAgents(opts.agents);
3897
- emit({ type: "agents", agents: agents.map((agent2) => ({ id: agent2.id, name: agent2.name })) });
4073
+ const named = opts.agents?.filter((id) => id !== "hermes");
4074
+ const agents = opts.agents?.length && !named?.length ? [] : selectLocalAgents(named);
4075
+ const hermes = !opts.agents?.length || opts.agents.includes("hermes") ? detectHermes() : null;
4076
+ const mcpOnly = hermes ? [hermes] : [];
4077
+ emit({ type: "agents", agents: [...agents, ...mcpOnly].map((agent2) => ({ id: agent2.id, name: agent2.name })) });
3898
4078
  const needsToken = !opts.dryRun && (has("skill") || has("logins") || has("secrets") || has("import") && !opts.key);
3899
4079
  if (needsToken && !apiToken()) {
3900
4080
  if (opts.json) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
@@ -3916,10 +4096,14 @@ async function runSetup(opts) {
3916
4096
  held = new Map((claimed.body.skills ?? []).map((skill) => [skill.name, skill.fingerprint]));
3917
4097
  };
3918
4098
  await tell();
3919
- if (has("skill") && agents.length && await agreed("Add the AgentSky skill and MCP server to these agents, so they can hand work to others?")) {
4099
+ if (has("skill") && (agents.length || mcpOnly.length) && await agreed("Add the AgentSky skill and MCP server to these agents, so they can hand work to others?")) {
3920
4100
  const { catalog: catalog2, detail } = await readCatalog();
3921
4101
  emit(catalog2 ? { type: "catalog", outcome: "read", agents: catalog2.length } : { type: "catalog", outcome: "unavailable", ...detail ? { detail } : {} });
3922
4102
  const token = apiToken();
4103
+ const registerMcp = (client) => {
4104
+ const registered = token ? registerMcpServer(client, { token, mcpUrl: mcpUrlFor(apiBase()), dryRun: opts.dryRun }) : { agent: client.id, outcome: "skipped", detail: "not logged in" };
4105
+ emit({ type: "mcp", agent: client.id, outcome: registered.outcome, ...registered.detail ? { detail: registered.detail } : {} });
4106
+ };
3923
4107
  for (const agent2 of agents) {
3924
4108
  try {
3925
4109
  const installed = installSkill(agent2, { dryRun: opts.dryRun, catalog: catalog2 });
@@ -3931,8 +4115,12 @@ async function runSetup(opts) {
3931
4115
  run2.agents.push({ id: agent2.id, skill: "failed" });
3932
4116
  continue;
3933
4117
  }
3934
- const registered = token ? registerMcpServer(agent2, { token, mcpUrl: mcpUrlFor(apiBase()), dryRun: opts.dryRun }) : { agent: agent2.id, outcome: "skipped", detail: "not logged in" };
3935
- emit({ type: "mcp", agent: agent2.id, outcome: registered.outcome, ...registered.detail ? { detail: registered.detail } : {} });
4118
+ registerMcp(agent2);
4119
+ }
4120
+ for (const client of mcpOnly) registerMcp(client);
4121
+ if (token) {
4122
+ const shell = writeShellEnv(token, { dryRun: opts.dryRun });
4123
+ emit({ type: "env", variable: TOKEN_ENV, outcome: shell.outcome, files: shell.files, ...shell.detail ? { detail: shell.detail } : {} });
3936
4124
  }
3937
4125
  await tell();
3938
4126
  }
@@ -4119,17 +4307,17 @@ function renderArtifact(artifact2) {
4119
4307
  }
4120
4308
 
4121
4309
  // src/skills-store.ts
4122
- import { readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as statSync8 } from "fs";
4123
- import { basename as basename7, join as join14, relative as relative4, resolve as resolve7 } from "path";
4310
+ import { readdirSync as readdirSync7, readFileSync as readFileSync15, statSync as statSync8 } from "fs";
4311
+ import { basename as basename8, join as join16, relative as relative4, resolve as resolve7 } from "path";
4124
4312
  function skillBundle(dir) {
4125
4313
  const root = resolve7(dir);
4126
- const top = basename7(root);
4314
+ const top = basename8(root);
4127
4315
  const files2 = [];
4128
4316
  const walk = (path) => {
4129
4317
  for (const name of readdirSync7(path).sort()) {
4130
- const full = join14(path, name);
4318
+ const full = join16(path, name);
4131
4319
  if (statSync8(full).isDirectory()) walk(full);
4132
- else files2.push({ path: `${top}/${relative4(root, full)}`, bytes: readFileSync14(full) });
4320
+ else files2.push({ path: `${top}/${relative4(root, full)}`, bytes: readFileSync15(full) });
4133
4321
  }
4134
4322
  };
4135
4323
  walk(root);
@@ -4139,7 +4327,7 @@ function skillBundle(dir) {
4139
4327
  async function uploadSkill(dir, displayName) {
4140
4328
  const form = new FormData();
4141
4329
  for (const file of skillBundle(dir)) form.append("files", new Blob([new Uint8Array(file.bytes)]), file.path);
4142
- form.append("displayName", displayName ?? basename7(resolve7(dir)));
4330
+ form.append("displayName", displayName ?? basename8(resolve7(dir)));
4143
4331
  return apiForm("POST", "/skills", form);
4144
4332
  }
4145
4333
  async function listSkills() {
@@ -4168,8 +4356,8 @@ async function getSkillUsage(since) {
4168
4356
 
4169
4357
  // src/task-export.ts
4170
4358
  import { createHash as createHash6 } from "crypto";
4171
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "fs";
4172
- import { dirname as dirname7, join as join15 } from "path";
4359
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
4360
+ import { dirname as dirname8, join as join17 } from "path";
4173
4361
  import { stringify as stringifyToml2 } from "smol-toml";
4174
4362
  var TEXT_MAX = 2e4;
4175
4363
  function scrub(text, names2) {
@@ -4457,14 +4645,14 @@ async function exportTaskHarbor(taskRef, outDir, opts) {
4457
4645
  fileBytes.set(file.path, bytes);
4458
4646
  }
4459
4647
  const files2 = buildHarborTask(record, { ...opts, fileBytes });
4460
- const written = [];
4648
+ const written2 = [];
4461
4649
  for (const [path, content] of files2) {
4462
- const target = join15(outDir, path);
4463
- mkdirSync4(dirname7(target), { recursive: true });
4464
- writeFileSync7(target, content, typeof content === "string" ? { mode: path.endsWith(".sh") || path.endsWith(".py") ? 493 : 420 } : void 0);
4465
- written.push(path);
4650
+ const target = join17(outDir, path);
4651
+ mkdirSync5(dirname8(target), { recursive: true });
4652
+ writeFileSync8(target, content, typeof content === "string" ? { mode: path.endsWith(".sh") || path.endsWith(".py") ? 493 : 420 } : void 0);
4653
+ written2.push(path);
4466
4654
  }
4467
- return { written, record };
4655
+ return { written: written2, record };
4468
4656
  }
4469
4657
  function parseRedactNames(values) {
4470
4658
  return (values ?? []).map((value) => {
@@ -4806,19 +4994,19 @@ function renderThreadSummary(t) {
4806
4994
  }
4807
4995
 
4808
4996
  // src/harness.ts
4809
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
4810
- import { dirname as dirname8, join as join16, resolve as resolve8 } from "path";
4997
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
4998
+ import { dirname as dirname9, join as join18, resolve as resolve8 } from "path";
4811
4999
  var HARNESS_FILE = "harness.json";
4812
5000
  function harnessFilePath(dir) {
4813
- return join16(resolve8(dir), HARNESS_FILE);
5001
+ return join18(resolve8(dir), HARNESS_FILE);
4814
5002
  }
4815
5003
  function readHarnessFile(dir) {
4816
5004
  const path = harnessFilePath(dir);
4817
- if (!existsSync9(path)) {
5005
+ if (!existsSync11(path)) {
4818
5006
  throw new Error(`no ${HARNESS_FILE} found in ${resolve8(dir)} \u2014 run \`sky harness init\` first`);
4819
5007
  }
4820
5008
  try {
4821
- return JSON.parse(readFileSync15(path, "utf8"));
5009
+ return JSON.parse(readFileSync16(path, "utf8"));
4822
5010
  } catch (err) {
4823
5011
  throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
4824
5012
  }
@@ -4845,11 +5033,11 @@ var INIT_COMMENT = `# harness.json \u2014 workspace harness profile skeleton
4845
5033
  `;
4846
5034
  function harnessInit(dir) {
4847
5035
  const path = harnessFilePath(dir);
4848
- if (existsSync9(path)) {
5036
+ if (existsSync11(path)) {
4849
5037
  throw new Error(`${path} already exists \u2014 delete it to re-init`);
4850
5038
  }
4851
- mkdirSync5(dirname8(path), { recursive: true });
4852
- writeFileSync8(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
5039
+ mkdirSync6(dirname9(path), { recursive: true });
5040
+ writeFileSync9(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
4853
5041
  `);
4854
5042
  console.log(`Created ${path}`);
4855
5043
  console.log(INIT_COMMENT);
@@ -5010,6 +5198,7 @@ auth.command("logout").description("forget the stored token (revoke it in Develo
5010
5198
  run(async () => {
5011
5199
  saveConfig({ ...loadConfig(), token: void 0, email: void 0 });
5012
5200
  console.log(`Logged out. (Token revocation: Developer \u2192 API keys.)`);
5201
+ for (const line of syncLocalAccess(void 0)) console.log(line);
5013
5202
  })
5014
5203
  );
5015
5204
  auth.command("whoami").description("who the current token acts as").action(
@@ -5558,7 +5747,7 @@ sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1
5558
5747
  run(async (ref, path, opts) => {
5559
5748
  const bytes = await readFile(await resolveSessionRef(ref), path);
5560
5749
  if (opts.out) {
5561
- writeFileSync9(opts.out, bytes);
5750
+ writeFileSync10(opts.out, bytes);
5562
5751
  console.error(`saved ${bytes.length} bytes to ${opts.out}`);
5563
5752
  } else process.stdout.write(bytes);
5564
5753
  })
@@ -5575,7 +5764,7 @@ sessionWorkspace.command("patch <ref> [repo]").description("the work in a reposi
5575
5764
  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)})`);
5576
5765
  if (!patch.length) console.error("nothing changed");
5577
5766
  else if (opts.out) {
5578
- writeFileSync9(opts.out, patch);
5767
+ writeFileSync10(opts.out, patch);
5579
5768
  console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
5580
5769
  } else process.stdout.write(patch);
5581
5770
  })
@@ -5720,7 +5909,7 @@ catalog.command("search [words...]").description("find an agent for a piece of w
5720
5909
  Start one: sky session create @<handle> --message "<the job>"`);
5721
5910
  })
5722
5911
  );
5723
- program.command("skill").description("the AgentSky skill for the agents on this machine").command("install").description("add the AgentSky skill to each installed agent (Claude Code, Codex, Cursor)").option("--agent <id...>", "only these agents: claude_code | codex | cursor").option("--dry-run", "say what would be written; write nothing").option("--json", "print the outcome per agent as JSON").action(
5912
+ program.command("skill").description("the AgentSky skill for the agents on this machine").command("install").description("add the AgentSky skill to each installed agent (Claude Code, Codex, Cursor)").option("--agent <id...>", "only these agents: claude_code | codex | cursor | hermes").option("--dry-run", "say what would be written; write nothing").option("--json", "print the outcome per agent as JSON").action(
5724
5913
  run(async (opts) => {
5725
5914
  const installs = selectLocalAgents(opts.agent).map((a) => installSkill(a, { dryRun: opts.dryRun }));
5726
5915
  if (opts.json) {
@@ -5736,7 +5925,7 @@ program.command("skill").description("the AgentSky skill for the agents on this
5736
5925
  })
5737
5926
  );
5738
5927
  program.command("setup [steps...]").description("bring this machine's agents to AgentSky: the skill and its MCP server, your plans, your skills and instructions, your tool keys").addHelpText("after", `
5739
- Steps (all when none is named): ${SETUP_STEPS.join(" | ")}`).option("--key <key>", "a setup key from agentsky.dev/setup").option("--agent <id...>", "only these agents: claude_code | codex | cursor").option("--secret <name...>", "save these tool keys to your vault (asked one by one when omitted)").option("--no-claude", "leave the Claude plan alone").option("--plan <name...>", "connect only these plans: claude | chatgpt | cursor").option("--dry-run", "say what would be done; write and send nothing").option("-y, --yes", "do not ask before each step").option("--json", "print progress as one JSON object per line; never asks").action(
5928
+ Steps (all when none is named): ${SETUP_STEPS.join(" | ")}`).option("--key <key>", "a setup key from agentsky.dev/setup").option("--agent <id...>", "only these agents: claude_code | codex | cursor | hermes").option("--secret <name...>", "save these tool keys to your vault (asked one by one when omitted)").option("--no-claude", "leave the Claude plan alone").option("--plan <name...>", "connect only these plans: claude | chatgpt | cursor").option("--dry-run", "say what would be done; write and send nothing").option("-y, --yes", "do not ask before each step").option("--json", "print progress as one JSON object per line; never asks").action(
5740
5929
  run(async (steps, opts) => {
5741
5930
  const unknown = steps.find((step) => !SETUP_STEPS.includes(step));
5742
5931
  if (unknown) throw new Error(`unknown step ${unknown} \u2014 expected ${SETUP_STEPS.join(" | ")}`);
@@ -5784,8 +5973,8 @@ secrets.command("unset <id> <key>").action(
5784
5973
  program.command("launch").description("scaffold ./agent.toml if absent, create the agent, print its address").option("-f, --file <path>", "agent file", "agent.toml").option("--type <type>", "override the file's harness: hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode").option("--llm <llm>", "override the file's model (must suit the harness)").action(
5785
5974
  run(async (opts) => {
5786
5975
  const path = resolve9(opts.file);
5787
- if (!existsSync10(path)) {
5788
- const name = basename8(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
5976
+ if (!existsSync12(path)) {
5977
+ const name = basename9(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
5789
5978
  scaffoldAgentFile(path, name);
5790
5979
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
5791
5980
  }
@@ -6067,8 +6256,8 @@ files.command("rm <id>").description("delete a file (a session that already rece
6067
6256
  );
6068
6257
  files.command("download <id>").description("download a session file's bytes").requiredOption("-o, --out <path>", "where to write the bytes").action(
6069
6258
  run(async (id, opts) => {
6070
- const written = await downloadFileTo(id, opts.out);
6071
- console.log(`Wrote ${written} bytes to ${opts.out}`);
6259
+ const written2 = await downloadFileTo(id, opts.out);
6260
+ console.log(`Wrote ${written2} bytes to ${opts.out}`);
6072
6261
  })
6073
6262
  );
6074
6263
  var task = program.command("task").description("a task's shared files and open requests (the task is its first session's id)");
@@ -6083,14 +6272,14 @@ task.command("get <task> <path>").description("read one task file; prints it, or
6083
6272
  run(async (taskId, path, opts) => {
6084
6273
  const bytes = await readTaskFile(taskId, path);
6085
6274
  if (opts.out) {
6086
- writeFileSync9(opts.out, bytes);
6275
+ writeFileSync10(opts.out, bytes);
6087
6276
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6088
6277
  } else process.stdout.write(bytes);
6089
6278
  })
6090
6279
  );
6091
6280
  task.command("put <task> <path> <local>").description("write a local file to <path> in the task's files (every session of the task sees it)").action(
6092
6281
  run(async (taskId, path, local) => {
6093
- const { file, conflict } = await writeTaskFile(taskId, path, readFileSync16(local), mediaTypeFor(local));
6282
+ const { file, conflict } = await writeTaskFile(taskId, path, readFileSync17(local), mediaTypeFor(local));
6094
6283
  console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
6095
6284
  })
6096
6285
  );
@@ -6104,9 +6293,9 @@ task.command("export <task>").description("write the task's session record as a
6104
6293
  run(async (taskId, opts) => {
6105
6294
  const tier = opts.tier === "gold" || opts.tier === "exploratory" ? opts.tier : "silver";
6106
6295
  const outDir = opts.out ?? `./harbor-${taskId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
6107
- const { written, record } = await exportTaskHarbor(taskId, outDir, { tier, redactNames: parseRedactNames(opts.redact) });
6108
- 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"})`);
6109
- if (!written.includes("environment/Dockerfile")) console.log("environment/: not resettable from the record \u2014 see environment/README.md before running with Harbor");
6296
+ const { written: written2, record } = await exportTaskHarbor(taskId, outDir, { tier, redactNames: parseRedactNames(opts.redact) });
6297
+ console.log(`Wrote ${written2.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"})`);
6298
+ if (!written2.includes("environment/Dockerfile")) console.log("environment/: not resettable from the record \u2014 see environment/README.md before running with Harbor");
6110
6299
  })
6111
6300
  );
6112
6301
  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(
@@ -6244,7 +6433,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
6244
6433
  run(async (id, path, opts) => {
6245
6434
  const bytes = await readArtifactFile(id, path);
6246
6435
  if (opts.out) {
6247
- writeFileSync9(opts.out, bytes);
6436
+ writeFileSync10(opts.out, bytes);
6248
6437
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6249
6438
  } else process.stdout.write(bytes);
6250
6439
  })
@@ -6542,7 +6731,7 @@ usageOptions(
6542
6731
  run(async (opts) => {
6543
6732
  const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
6544
6733
  if (opts.out) {
6545
- writeFileSync9(opts.out, bytes);
6734
+ writeFileSync10(opts.out, bytes);
6546
6735
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6547
6736
  } else {
6548
6737
  process.stdout.write(bytes);
@@ -7093,7 +7282,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
7093
7282
  }));
7094
7283
  var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
7095
7284
  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) => {
7096
- const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-HTWXINXI.js");
7285
+ const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-PDIOL2TC.js");
7097
7286
  if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
7098
7287
  const scopes = parseContentScopes(opts.capture);
7099
7288
  const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
@@ -7128,7 +7317,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
7128
7317
  }));
7129
7318
  traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
7130
7319
  try {
7131
- const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-HTWXINXI.js");
7320
+ const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-PDIOL2TC.js");
7132
7321
  const chunks = [];
7133
7322
  for await (const chunk of process.stdin) chunks.push(chunk);
7134
7323
  const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
@@ -7141,14 +7330,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
7141
7330
  }
7142
7331
  });
7143
7332
  traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
7144
- const { readState, flush, grantSender } = await import("./trace-HTWXINXI.js");
7333
+ const { readState, flush, grantSender } = await import("./trace-PDIOL2TC.js");
7145
7334
  const state = readState();
7146
7335
  if (!state) throw new Error("no trace is running here");
7147
7336
  const outcome = await flush(grantSender(state), { attempts: 4 });
7148
7337
  console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
7149
7338
  }));
7150
7339
  traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
7151
- const { readState, spoolStatus } = await import("./trace-HTWXINXI.js");
7340
+ const { readState, spoolStatus } = await import("./trace-PDIOL2TC.js");
7152
7341
  const state = readState();
7153
7342
  if (!state) {
7154
7343
  console.log("No trace is running here.");
@@ -7158,7 +7347,7 @@ traceCmd.command("status").description("the running trace and its spool").action
7158
7347
  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`);
7159
7348
  }));
7160
7349
  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) => {
7161
- const { readState } = await import("./trace-HTWXINXI.js");
7350
+ const { readState } = await import("./trace-PDIOL2TC.js");
7162
7351
  const ref = id ?? readState()?.trace_id;
7163
7352
  if (!ref) throw new Error("name a trace, or start one here");
7164
7353
  const bundle = await api("GET", `/traces/${ref}`);
@@ -7185,7 +7374,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
7185
7374
  for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
7186
7375
  }));
7187
7376
  traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
7188
- const { readState, record, flush, grantSender, clearState } = await import("./trace-HTWXINXI.js");
7377
+ const { readState, record, flush, grantSender, clearState } = await import("./trace-PDIOL2TC.js");
7189
7378
  const state = readState();
7190
7379
  if (!state) throw new Error("no trace is running here");
7191
7380
  if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
@@ -7194,8 +7383,8 @@ traceCmd.command("stop").description("end the root span, send what is left, and
7194
7383
  clearState();
7195
7384
  console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
7196
7385
  }));
7197
- 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) => {
7198
- const lib = await import("./trace-library-QR54Q4Z3.js");
7386
+ traceCmd.command("sync [payload]").description("upload this machine's agent sessions (Claude Code, Codex, Cursor, Gemini CLI and every other agent `--agents` lists) 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("--import <export>", "add a downloaded cloud export (claude.ai, ChatGPT, Gemini, Grok, DeepSeek \u2026: its .zip, folder or file) so sync uploads it (repeatable)", collect).option("--agents", "list the agents sync reads and which are on this machine").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) => {
7387
+ const lib = await import("./trace-library-TA3QUBHJ.js");
7199
7388
  if (opts.hook) {
7200
7389
  try {
7201
7390
  let raw = payload ?? "";
@@ -7236,17 +7425,28 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
7236
7425
  };
7237
7426
  lib.saveSyncConfig(next);
7238
7427
  console.log(`Trace sync is ${next.enabled ? "on" : "off"} on this machine. It reads only:`);
7239
- for (const folder of lib.sourceFolders()) console.log(` ${folder}`);
7428
+ for (const folder of await lib.sourceFolders()) console.log(` ${folder}`);
7240
7429
  console.log("and sends each session, redacted here first, to:");
7241
7430
  for (const rule of next.rules) console.log(` folder path contains "${rule.match}" \u2192 ${rule.workspace}`);
7242
7431
  console.log(` anything else \u2192 ${next.fallback ?? "stays on this machine"}`);
7243
7432
  console.log(`A repository's .agentsky.json "workspace" wins over these rules.`);
7244
7433
  }
7434
+ if (opts.agents) {
7435
+ for (const agent2 of await lib.supportedAgents()) {
7436
+ const how = agent2.kind === "export" ? "cloud export: sky trace sync --import <file>" : agent2.found ? "found on this machine" : "not on this machine";
7437
+ console.log(` ${agent2.name.padEnd(46)} ${how}`);
7438
+ }
7439
+ return;
7440
+ }
7441
+ for (const input of opts.import ?? []) {
7442
+ const imported = await lib.importExport(input);
7443
+ console.log(`Added ${imported.sessions} ${imported.agent} session${imported.sessions === 1 ? "" : "s"} from ${input}.`);
7444
+ }
7245
7445
  if (opts.installSchedule) {
7246
- const { join: join17 } = await import("path");
7247
- const { homedir: homedir8 } = await import("os");
7446
+ const { join: join19 } = await import("path");
7447
+ const { homedir: homedir10 } = await import("os");
7248
7448
  const { execFileSync } = await import("child_process");
7249
- const log = join17(homedir8(), ".config", "sky", "trace-sync.log");
7449
+ const log = join19(homedir10(), ".config", "sky", "trace-sync.log");
7250
7450
  const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
7251
7451
  try {
7252
7452
  execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
@@ -7256,11 +7456,11 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
7256
7456
  console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
7257
7457
  }
7258
7458
  if (opts.status) {
7259
- const { readFileSync: readFileSync17 } = await import("fs");
7260
- const { join: join17 } = await import("path");
7459
+ const { readFileSync: readFileSync18 } = await import("fs");
7460
+ const { join: join19 } = await import("path");
7261
7461
  let state = { sessions: {} };
7262
7462
  try {
7263
- state = JSON.parse(readFileSync17(join17(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
7463
+ state = JSON.parse(readFileSync18(join19(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
7264
7464
  } catch {
7265
7465
  }
7266
7466
  const counts = /* @__PURE__ */ new Map();
@@ -7293,7 +7493,7 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
7293
7493
  })();
7294
7494
  });
7295
7495
  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) => {
7296
- const { libraryRequest } = await import("./trace-library-QR54Q4Z3.js");
7496
+ const { libraryRequest } = await import("./trace-library-TA3QUBHJ.js");
7297
7497
  const workspace2 = opts.workspace ?? currentUniverse();
7298
7498
  if (!workspace2) throw new Error("name a workspace with --workspace");
7299
7499
  const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
@@ -7310,7 +7510,7 @@ traceCmd.command("search <query...>").description("search every session in a wor
7310
7510
  if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
7311
7511
  }));
7312
7512
  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) => {
7313
- const { libraryRequest } = await import("./trace-library-QR54Q4Z3.js");
7513
+ const { libraryRequest } = await import("./trace-library-TA3QUBHJ.js");
7314
7514
  const workspace2 = opts.workspace ?? currentUniverse();
7315
7515
  if (!workspace2) throw new Error("name a workspace with --workspace");
7316
7516
  const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);