@agentsky/sky 2.21.0 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveSessionRef,
10
10
  streamEvents,
11
11
  uploadFile
12
- } from "./chunk-ZZ6UF7QA.js";
12
+ } from "./chunk-HQPOCQRQ.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-6JMFB3CV.js";
23
+ } from "./chunk-V5XLV37X.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-HDPDY77W.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
 
@@ -410,6 +410,52 @@ function renderApiKey(key) {
410
410
  return `${key.id} ${key.token_prefix}\u2026 ${key.status.padEnd(7)} ${key.scopes.join(",").padEnd(16)} ${where} ${used} ${key.name}${key.current ? " (this key)" : ""}`;
411
411
  }
412
412
 
413
+ // src/account-access.ts
414
+ function linkToken(ref) {
415
+ const fromUrl = /\/s\/([A-Za-z0-9_-]+)\/?(?:[?#].*)?$/.exec(ref.trim());
416
+ const token = fromUrl ? fromUrl[1] : ref.trim();
417
+ if (!/^[A-Za-z0-9_-]{16,64}$/.test(token)) throw new ApiError(400, "invalid_request", "a share link is a /s/<token> URL or its token");
418
+ return token;
419
+ }
420
+ async function sendViaLink(ref, text, idempotencyKey) {
421
+ await api("POST", `/links/${encodeURIComponent(linkToken(ref))}/messages`, { text }, { idempotencyKey });
422
+ }
423
+ async function stopViaLink(ref) {
424
+ await api("POST", `/links/${encodeURIComponent(linkToken(ref))}/interrupt`);
425
+ }
426
+ function inviteCode(ref) {
427
+ const fromUrl = /\/i\/([^/?#]+)\/?(?:[?#].*)?$/.exec(ref.trim());
428
+ const code = fromUrl ? decodeURIComponent(fromUrl[1]) : ref.trim();
429
+ if (!code) throw new ApiError(400, "invalid_request", "a credit invite is an /i/<code> URL or its code");
430
+ return code;
431
+ }
432
+ async function claimCreditInvite(ref) {
433
+ return api("POST", "/billing/credit-invites/claim", { code: inviteCode(ref) });
434
+ }
435
+ function renderCreditInviteClaim(claim) {
436
+ const amount = `$${claim.amount_usd.toFixed(2)}`;
437
+ return claim.status === "granted" ? `Added ${amount} of credit to your balance` : `Already claimed: this link added ${amount} to your balance`;
438
+ }
439
+ async function listAppGrants() {
440
+ return (await api("GET", "/oauth_grants")).oauth_grants;
441
+ }
442
+ async function revokeAppGrant(clientId) {
443
+ await api("DELETE", `/oauth_grants/${encodeURIComponent(clientId)}`);
444
+ }
445
+ function renderAppGrant(grant) {
446
+ const since = grant.allowed_at ? ` since ${grant.allowed_at.slice(0, 10)}` : "";
447
+ const review = grant.review_status === "approved" ? "" : " (unreviewed)";
448
+ return `${grant.client_id} ${grant.name || "(unnamed)"} ${grant.scopes.join(" ")}${since}${review}`;
449
+ }
450
+ async function unsubscribeMarketing() {
451
+ const { notification_preferences } = await api(
452
+ "PATCH",
453
+ "/notification-preferences",
454
+ { email: { marketing: false } }
455
+ );
456
+ return { marketing: notification_preferences.email.marketing };
457
+ }
458
+
413
459
  // src/context.ts
414
460
  function memoryBody(operation, version) {
415
461
  return { ...operation, expectedVersion: version };
@@ -682,9 +728,9 @@ function readClaudeCode() {
682
728
  sources.push("global CLAUDE.md");
683
729
  }
684
730
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
685
- const project = readIfExists(join3(process.cwd(), name));
686
- if (project?.trim()) {
687
- parts.push({ label: `Project ${name}`, content: project });
731
+ const project2 = readIfExists(join3(process.cwd(), name));
732
+ if (project2?.trim()) {
733
+ parts.push({ label: `Project ${name}`, content: project2 });
688
734
  sources.push(`project ${name}`);
689
735
  break;
690
736
  }
@@ -739,11 +785,11 @@ function linkPath(configRoot) {
739
785
  return join3(configRoot, "agentsky-link.json");
740
786
  }
741
787
  function readLink(configRoot) {
742
- const link = readJson(linkPath(configRoot));
743
- return link?.agentId ? link : void 0;
788
+ const link2 = readJson(linkPath(configRoot));
789
+ return link2?.agentId ? link2 : void 0;
744
790
  }
745
- function writeLink(configRoot, link) {
746
- writeFileSync3(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
791
+ function writeLink(configRoot, link2) {
792
+ writeFileSync3(linkPath(configRoot), `${JSON.stringify(link2, null, 2)}
747
793
  `);
748
794
  }
749
795
  var DEFAULT_VAULT_NAME = "default";
@@ -909,14 +955,14 @@ async function createClone(local, opts) {
909
955
  await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
910
956
  if (opts.open) openBrowser(url);
911
957
  }
912
- async function syncClone(local, link, opts) {
913
- const since = link.lastSyncedAt ?? link.clonedAt;
958
+ async function syncClone(local, link2, opts) {
959
+ const since = link2.lastSyncedAt ?? link2.clonedAt;
914
960
  console.log(
915
- `Syncing ${link.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
961
+ `Syncing ${link2.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
916
962
  );
917
963
  if (opts.dryRun) {
918
964
  console.log(
919
- ` would push prompt (${kb(local.prompt)}) to ${link.agentId}. Dry run \u2014 nothing sent.`
965
+ ` would push prompt (${kb(local.prompt)}) to ${link2.agentId}. Dry run \u2014 nothing sent.`
920
966
  );
921
967
  return;
922
968
  }
@@ -924,24 +970,24 @@ async function syncClone(local, link, opts) {
924
970
  try {
925
971
  ({ agent: agent2 } = await api(
926
972
  "GET",
927
- `/agents/${encodeURIComponent(link.agentId)}`
973
+ `/agents/${encodeURIComponent(link2.agentId)}`
928
974
  ));
929
975
  } catch (error) {
930
976
  if (error instanceof ApiError && error.status === 404) {
931
977
  throw new Error(
932
- `linked agent ${link.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
978
+ `linked agent ${link2.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
933
979
  );
934
980
  }
935
981
  throw error;
936
982
  }
937
983
  if (agent2.agentType !== local.harness) {
938
984
  throw new Error(
939
- `linked agent ${link.agentId} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
985
+ `linked agent ${link2.agentId} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
940
986
  );
941
987
  }
942
988
  const result = await api(
943
989
  "PUT",
944
- `/agents/${encodeURIComponent(link.agentId)}/prompt`,
990
+ `/agents/${encodeURIComponent(link2.agentId)}/prompt`,
945
991
  { prompt: local.prompt }
946
992
  );
947
993
  console.log(
@@ -949,11 +995,11 @@ async function syncClone(local, link, opts) {
949
995
  );
950
996
  const drift = [];
951
997
  const localModel = local.model ?? null;
952
- const linkedModel = link.llm ?? agent2.llm;
998
+ const linkedModel = link2.llm ?? agent2.llm;
953
999
  if (localModel && linkedModel && localModel !== linkedModel) {
954
1000
  drift.push(`model changed ${linkedModel} \u2192 ${localModel}`);
955
1001
  }
956
- const before = new Set(link.mcpServers ?? []);
1002
+ const before = new Set(link2.mcpServers ?? []);
957
1003
  const nowKept = Object.keys(local.keptServers);
958
1004
  const newServers = nowKept.filter((n) => !before.has(n));
959
1005
  const goneServers = [...before].filter((n) => !nowKept.includes(n));
@@ -968,16 +1014,16 @@ async function syncClone(local, link, opts) {
968
1014
  ` left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
969
1015
  );
970
1016
  }
971
- writeLink(local.configRoot, { ...link, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
972
- console.log(`Synced ${link.agentId}.`);
1017
+ writeLink(local.configRoot, { ...link2, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
1018
+ console.log(`Synced ${link2.agentId}.`);
973
1019
  await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
974
- if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentId}`);
1020
+ if (opts.open) openBrowser(`${apiBase()}/agents/${link2.agentId}`);
975
1021
  }
976
1022
  async function clone(opts) {
977
1023
  const harness2 = detectHarness(opts.harness);
978
1024
  const local = readLocal(harness2);
979
- const link = opts.new ? void 0 : readLink(local.configRoot);
980
- if (link) await syncClone(local, link, opts);
1025
+ const link2 = opts.new ? void 0 : readLink(local.configRoot);
1026
+ if (link2) await syncClone(local, link2, opts);
981
1027
  else await createClone(local, opts);
982
1028
  }
983
1029
 
@@ -1000,7 +1046,7 @@ import { homedir as homedir2 } from "os";
1000
1046
  import { basename as basename4, dirname as dirname3, join as join4, relative, sep } from "path";
1001
1047
  import { parse as parseToml2 } from "smol-toml";
1002
1048
  var MAX_TREE = 5e3;
1003
- var SKILL_LINE = /Base directory for this skill: (\/[^\s"'\\<>]+)/g;
1049
+ var SKILL_LINE = /Base directory for this skill: (\/[^\n\r"'\\<>]+)/g;
1004
1050
  function git(cwd, ...args) {
1005
1051
  const run2 = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 1e4, maxBuffer: 32 * 1024 * 1024 });
1006
1052
  return run2.status === 0 ? run2.stdout : null;
@@ -1046,8 +1092,7 @@ function skillRoots(cwd, home) {
1046
1092
  ];
1047
1093
  }
1048
1094
  function pluginSkill(plugin, skill, home) {
1049
- const base = join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "plugins");
1050
- const walk = (dir, depth) => {
1095
+ const walk = (dir, depth, underPlugin) => {
1051
1096
  if (depth > 7) return null;
1052
1097
  let entries;
1053
1098
  try {
@@ -1058,13 +1103,13 @@ function pluginSkill(plugin, skill, home) {
1058
1103
  for (const entry2 of entries) {
1059
1104
  if (!entry2.isDirectory() || entry2.name === "node_modules" || entry2.name === ".git") continue;
1060
1105
  const path = join4(dir, entry2.name);
1061
- if (entry2.name === skill && basename4(dir) === "skills" && path.includes(`${sep}${plugin}${sep}`) && isFile(join4(path, "SKILL.md"))) return path;
1062
- const found = walk(path, depth + 1);
1106
+ if (entry2.name === skill && basename4(dir) === "skills" && (underPlugin || path.includes(`${sep}${plugin}${sep}`)) && isFile(join4(path, "SKILL.md"))) return realpathSync(path);
1107
+ const found = walk(path, depth + 1, underPlugin);
1063
1108
  if (found) return found;
1064
1109
  }
1065
1110
  return null;
1066
1111
  };
1067
- return walk(base, 0);
1112
+ return walk(join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "plugins"), 0, false) ?? walk(join4(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions", "skills-plugin"), 0, true);
1068
1113
  }
1069
1114
  function findSkill(name, cwd, home) {
1070
1115
  const [scope, skill] = name.includes(":") ? name.split(":", 2) : [null, name];
@@ -1084,23 +1129,24 @@ function loadedSkills(records, touched, cwd, home = homedir2()) {
1084
1129
  if (![...dirs.values()].includes(real)) dirs.set(name, real);
1085
1130
  return true;
1086
1131
  };
1132
+ const called = [];
1087
1133
  for (const record of records) {
1088
1134
  const texts = [];
1089
1135
  strings(record, texts);
1090
- for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].replace(/[.,;:)]+$/, ""));
1136
+ for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].trim().replace(/[.,;:)]+$/, ""));
1091
1137
  const content = record?.message?.content;
1092
1138
  if (!Array.isArray(content)) continue;
1093
1139
  for (const block of content) {
1094
1140
  const b = block;
1095
- if (b?.type !== "tool_use" || b.name !== "Skill" || typeof b.input?.skill !== "string") continue;
1096
- const name = b.input.skill.replace(/^\//, "");
1097
- if (!name || dirs.has(name) || [...dirs.keys()].some((key) => key.endsWith(`:${name}`) || name.endsWith(`:${key}`))) continue;
1098
- const dir = findSkill(name, cwd, home);
1099
- if (!dir || !add(dir, name)) missing.add(name);
1141
+ if (b?.type === "tool_use" && b.name === "Skill" && typeof b.input?.skill === "string") called.push(b.input.skill.replace(/^\//, ""));
1100
1142
  }
1101
1143
  }
1102
1144
  for (const path of touched) if (basename4(path) === "SKILL.md") add(dirname3(path));
1103
- for (const name of dirs.keys()) missing.delete(name);
1145
+ for (const name of called) {
1146
+ if (!name || dirs.has(name) || [...dirs.keys()].some((key) => key.endsWith(`:${name}`) || name.endsWith(`:${key}`))) continue;
1147
+ const dir = findSkill(name, cwd, home);
1148
+ if (!dir || !add(dir, name)) missing.add(name);
1149
+ }
1104
1150
  return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
1105
1151
  }
1106
1152
  function readJson2(path) {
@@ -1697,7 +1743,7 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1697
1743
  const scratch = mkdtempSync(join6(tmpdir(), "sky-environment-"));
1698
1744
  const path = join6(scratch, "environment.tar.gz");
1699
1745
  const leftOut = [];
1700
- const written = /* @__PURE__ */ new Set();
1746
+ const written2 = /* @__PURE__ */ new Set();
1701
1747
  const hash = createHash2("sha256");
1702
1748
  let bytes = 0;
1703
1749
  let redacted2 = 0;
@@ -1764,9 +1810,9 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1764
1810
  }
1765
1811
  await write(tarPadding(stat.size));
1766
1812
  }
1767
- written.add(rel);
1813
+ written2.add(rel);
1768
1814
  }
1769
- const layout = importLayout(resources, cwd, platform, { files: written.size, paths: written });
1815
+ const layout = importLayout(resources, cwd, platform, { files: written2.size, paths: written2 });
1770
1816
  for (const [entry2, data] of layout?.entries ?? []) {
1771
1817
  await write(tarHeader(entry2, data.length));
1772
1818
  await write(data);
@@ -1781,7 +1827,7 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1781
1827
  folder: layout?.folder ?? "imported-session",
1782
1828
  bytes,
1783
1829
  sha256: hash.digest("hex"),
1784
- files: written.size + (layout?.entries.size ?? 0),
1830
+ files: written2.size + (layout?.entries.size ?? 0),
1785
1831
  redacted: redacted2,
1786
1832
  leftOut,
1787
1833
  dispose: () => rmSync2(scratch, { recursive: true, force: true })
@@ -2294,507 +2340,92 @@ import { createHash as createHash4, randomBytes } from "crypto";
2294
2340
  import { createServer } from "http";
2295
2341
  import { hostname } from "os";
2296
2342
  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
- }
2343
+
2344
+ // src/mcp-register.ts
2345
+ import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
2346
+ import { spawnSync as spawnSync3 } from "child_process";
2347
+ import { homedir as homedir6 } from "os";
2348
+ import { join as join10 } from "path";
2349
+ import { parse as parseToml3 } from "smol-toml";
2350
+ import { isMap, parseDocument } from "yaml";
2351
+
2352
+ // src/local-agents.ts
2353
+ import { existsSync as existsSync6 } from "fs";
2354
+ import { homedir as homedir5 } from "os";
2355
+ import { join as join8 } from "path";
2356
+ function localAgents(env = process.env, home = homedir5()) {
2357
+ const claude = env.CLAUDE_CONFIG_DIR || join8(home, ".claude");
2358
+ const codex = env.CODEX_HOME || join8(home, ".codex");
2359
+ const cursor = join8(home, ".cursor");
2360
+ return [
2361
+ { id: "claude_code", name: "Claude Code", root: claude, skillsDir: join8(claude, "skills") },
2362
+ { id: "codex", name: "Codex", root: codex, skillsDir: join8(codex, "skills") },
2363
+ { id: "cursor", name: "Cursor", root: cursor, skillsDir: join8(cursor, "skills") }
2364
+ ];
2306
2365
  }
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
- );
2366
+ function detectLocalAgents(env = process.env, home = homedir5()) {
2367
+ return localAgents(env, home).filter((agent2) => existsSync6(agent2.root));
2322
2368
  }
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();
2369
+ function selectLocalAgents(ids, env = process.env, home = homedir5()) {
2370
+ if (!ids?.length) return detectLocalAgents(env, home);
2371
+ const known = localAgents(env, home);
2372
+ return ids.map((id) => {
2373
+ const agent2 = known.find((candidate) => candidate.id === id);
2374
+ if (!agent2) throw new Error(`unknown agent ${id} \u2014 expected ${known.map((candidate) => candidate.id).join(" | ")}`);
2375
+ return agent2;
2355
2376
  });
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
2377
  }
2390
2378
 
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)}`;
2379
+ // src/skill.ts
2380
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2381
+ import { join as join9 } from "path";
2382
+
2383
+ // src/catalog-file.ts
2384
+ var CATALOG_FILE = "catalog.md";
2385
+ var CATEGORY_ORDER = ["websites", "content", "video", "code", "research", "general"];
2386
+ function categoryOf(agent2) {
2387
+ return CATEGORY_ORDER.find((category) => agent2.categories.includes(category)) ?? agent2.categories[0] ?? "general";
2408
2388
  }
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}`;
2389
+ function describe(value) {
2390
+ 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)}`);
2391
+ return parts.length ? parts.join(", ") : null;
2412
2392
  }
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
- });
2393
+ function entry(agent2) {
2394
+ const engine = agent2.engine_type ? ` \xB7 engine ${agent2.engine_type}` : "";
2395
+ const lines = [`### @${agent2.handle} \u2014 ${agent2.name}${engine}`, "", agent2.tagline];
2396
+ if (agent2.does.length) lines.push("", `Does: ${agent2.does.join("; ")}`);
2397
+ if (agent2.does_not.length) lines.push(`Does not: ${agent2.does_not.join("; ")}`);
2398
+ const { mcp_servers: mcpServers2, ...rest } = agent2.requirements;
2399
+ if (typeof mcpServers2 === "boolean") lines.push(mcpServers2 ? "Can use MCP servers." : "Cannot use MCP servers.");
2400
+ const details = describe(rest);
2401
+ if (details) lines.push(`Details: ${details}`);
2402
+ const pricing = describe(agent2.pricing);
2403
+ if (pricing) lines.push(`Pricing: ${pricing}`);
2404
+ return lines.join("\n");
2449
2405
  }
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 } : {}
2406
+ function renderCatalog(agents, generatedAt) {
2407
+ const header2 = [
2408
+ "# AgentSky agent catalog",
2409
+ "",
2410
+ `Every agent you can hire, as of ${generatedAt.toISOString()}. Start one with \`start_session\` and its handle, written with the @.`,
2411
+ "Run `npx @agentsky/sky setup` again to refresh this file; `search_catalog` always has the current list."
2412
+ ];
2413
+ if (!agents.length) return [...header2, "", "The catalog was empty when this file was written. Use `search_catalog`.", ""].join("\n");
2414
+ const own = agents.filter((agent2) => agent2.visibility === "workspace");
2415
+ const shared = agents.filter((agent2) => agent2.visibility !== "workspace");
2416
+ const sections = [];
2417
+ if (own.length) sections.push(["## Your workspace's agents", ...own.map(entry)].join("\n\n"));
2418
+ const groups = /* @__PURE__ */ new Map();
2419
+ for (const agent2 of shared) groups.set(categoryOf(agent2), [...groups.get(categoryOf(agent2)) ?? [], agent2]);
2420
+ const ordered = [...groups.keys()].sort((a, b) => {
2421
+ const rank = (category) => CATEGORY_ORDER.includes(category) ? CATEGORY_ORDER.indexOf(category) : CATEGORY_ORDER.length;
2422
+ return rank(a) - rank(b) || a.localeCompare(b);
2459
2423
  });
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");
2424
+ for (const category of ordered) {
2425
+ const title = category.charAt(0).toUpperCase() + category.slice(1);
2426
+ sections.push([`## ${title}`, ...groups.get(category).map(entry)].join("\n\n"));
2427
+ }
2428
+ return [...header2, "", sections.join("\n\n"), ""].join("\n");
2798
2429
  }
2799
2430
 
2800
2431
  // src/skill.ts
@@ -3145,185 +2776,765 @@ your application has checked who they are. By hand: \`sky task show <id>\` and
3145
2776
  \`sky task answer <id> <request> --text "..."\` (MCP: \`get_task\`,
3146
2777
  \`answer_request\`).
3147
2778
 
3148
- ## 7. What the application keeps
2779
+ ## 7. What the application keeps
2780
+
2781
+ The agent decides; the application acts. Keep these in the application's own
2782
+ database:
2783
+
2784
+ - **One job per event**, with a unique event key. The same key with the same
2785
+ payload returns the job it made; with a different payload it is a conflict.
2786
+ - **The session id**, saved before you start polling, so a restart picks the
2787
+ job up where it was.
2788
+ - **A confirmation receipt** from the signed-in customer before any side
2789
+ effect.
2790
+ - **A unique action key per job**, so the side effect happens once. Agent
2791
+ retries do not make it happen once; the key does.
2792
+ - **Webhooks to the customer** with an event id, retried until acknowledged.
2793
+
2794
+ ## Before launch
2795
+
2796
+ Check one real session:
2797
+
2798
+ - \`GET /v1/sessions/{id}\` lists only the upload under \`resources\`.
2799
+ - \`GET /v1/hook_rules\` and \`GET /v1/universes/{slug}/policies\` show the rules
2800
+ above.
2801
+ - Ask the agent directly to run a shell command. The answer has to be the
2802
+ platform's refusal.
2803
+ - A file id from another customer's workspace is refused at session create.
2804
+ `;
2805
+ }
2806
+ function workspacesReferenceDoc() {
2807
+ return `# Workspaces in AgentSky
2808
+
2809
+ A workspace (also called a universe) groups agents and sessions. Every API call
2810
+ and MCP tool accepts an optional workspace slug.
2811
+
2812
+ ## How to choose
2813
+
2814
+ 1. The person named one \u2192 use it.
2815
+ 2. Check the git root for \`.agentsky.json\` with a \`"workspace"\` key \u2192 use that.
2816
+ 3. Neither \u2192 use the account default (personal workspace). Never ask.
2817
+
2818
+ ## Applying it
2819
+
2820
+ - MCP tools: pass \`workspace: "<slug>"\` on tools that accept it.
2821
+ - sky CLI: \`--workspace <slug>\` on any command, or set \`SKY_WORKSPACE=<slug>\`.
2822
+ - Tools that take a \`task_id\` or \`session_id\` inherit that task's workspace
2823
+ automatically; one task never spans workspaces.
2824
+
2825
+ ## Listing workspaces
2826
+
2827
+ \`sky workspace list\` (or \`sky universe list\`) prints your workspaces, with
2828
+ \`*\` marking the current default.
2829
+
2830
+ ## .agentsky.json
2831
+
2832
+ To set a per-repo default, create \`.agentsky.json\` at the git root:
2833
+
2834
+ \`\`\`json
2835
+ {"workspace": "my-team-slug"}
2836
+ \`\`\`
2837
+ `;
2838
+ }
2839
+ function installedVersion(dir) {
2840
+ try {
2841
+ const marker = JSON.parse(readFileSync9(join9(dir, SKILL_MARKER), "utf8"));
2842
+ return typeof marker.version === "string" ? marker.version : null;
2843
+ } catch {
2844
+ return null;
2845
+ }
2846
+ }
2847
+ function isInstalledSkill(dir) {
2848
+ return installedVersion(dir) !== null;
2849
+ }
2850
+ function installSkill(agent2, opts = {}) {
2851
+ const dir = join9(agent2.skillsDir, SKILL_NAME);
2852
+ const previous = installedVersion(dir);
2853
+ const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
2854
+ if (opts.dryRun) return { agent: agent2.id, path: dir, outcome };
2855
+ if (outcome !== "current") {
2856
+ mkdirSync2(dir, { recursive: true });
2857
+ writeFileSync4(join9(dir, "SKILL.md"), skillDocument());
2858
+ writeFileSync4(join9(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
2859
+ `);
2860
+ }
2861
+ if (opts.catalog) {
2862
+ mkdirSync2(dir, { recursive: true });
2863
+ writeFileSync4(join9(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
2864
+ }
2865
+ const refsDir = join9(dir, REFERENCES_DIR);
2866
+ mkdirSync2(refsDir, { recursive: true });
2867
+ writeFileSync4(join9(refsDir, "import.md"), importReferenceDoc());
2868
+ writeFileSync4(join9(refsDir, "workspaces.md"), workspacesReferenceDoc());
2869
+ writeFileSync4(join9(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
2870
+ return { agent: agent2.id, path: dir, outcome };
2871
+ }
2872
+
2873
+ // src/mcp-register.ts
2874
+ var MCP_SERVER_NAME = SKILL_NAME;
2875
+ function detectHermes(env = process.env, home = homedir6()) {
2876
+ const root = env.HERMES_HOME || join10(home, ".hermes");
2877
+ return existsSync7(root) ? { id: "hermes", name: "Hermes", root } : null;
2878
+ }
2879
+ function detectMcpClients(env = process.env, home = homedir6()) {
2880
+ const hermes = detectHermes(env, home);
2881
+ return [...detectLocalAgents(env, home), ...hermes ? [hermes] : []];
2882
+ }
2883
+ function systemCommandRunner() {
2884
+ return {
2885
+ has: (command) => spawnSync3(command, ["--version"], { stdio: "ignore" }).status === 0,
2886
+ run: (command, args) => {
2887
+ const result = spawnSync3(command, args, { encoding: "utf8" });
2888
+ return { status: result.status, stderr: result.stderr ?? "" };
2889
+ }
2890
+ };
2891
+ }
2892
+ function mcpUrlFor(apiBase2) {
2893
+ return `${apiBase2.replace(/\/$/, "")}/mcp`;
2894
+ }
2895
+ function mcpCommands(agent2, opts) {
2896
+ if (agent2 === "claude_code") {
2897
+ return {
2898
+ command: "claude",
2899
+ remove: ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME],
2900
+ add: ["mcp", "add", "--scope", "user", "--transport", "http", MCP_SERVER_NAME, opts.mcpUrl, "--header", `Authorization: Bearer ${opts.token}`]
2901
+ };
2902
+ }
2903
+ return null;
2904
+ }
2905
+ function redact(text, token) {
2906
+ return text.split(token).join(`${token.slice(0, 10)}\u2026`);
2907
+ }
2908
+ function registerMcpServer(client, opts, runner = systemCommandRunner()) {
2909
+ if (client.id === "codex") return registerCodex(client, opts);
2910
+ if (client.id === "cursor") return registerCursor(client, opts);
2911
+ if (client.id === "hermes") return registerHermes(client, opts);
2912
+ const commands = mcpCommands(client.id, opts);
2913
+ if (!commands) return { agent: client.id, outcome: "unsupported" };
2914
+ if (!runner.has(commands.command)) return { agent: client.id, outcome: "no_command", detail: `${commands.command} is not on PATH` };
2915
+ if (opts.dryRun) return { agent: client.id, outcome: "planned", detail: redact(`run ${[commands.command, ...commands.add].join(" ")}`, opts.token) };
2916
+ runner.run(commands.command, commands.remove);
2917
+ const added = runner.run(commands.command, commands.add);
2918
+ if (added.status !== 0) return { agent: client.id, outcome: "failed", detail: redact(added.stderr.trim() || `exit ${added.status}`, opts.token) };
2919
+ return { agent: client.id, outcome: "registered" };
2920
+ }
2921
+ function mcpConfigPath(client, env = process.env, home = homedir6()) {
2922
+ switch (client.id) {
2923
+ case "claude_code":
2924
+ return join10(env.CLAUDE_CONFIG_DIR || home, ".claude.json");
2925
+ case "codex":
2926
+ return join10(client.root, "config.toml");
2927
+ case "cursor":
2928
+ return join10(client.root, "mcp.json");
2929
+ case "hermes":
2930
+ return join10(client.root, "config.yaml");
2931
+ }
2932
+ }
2933
+ function readText(path) {
2934
+ try {
2935
+ return readFileSync10(path, "utf8");
2936
+ } catch {
2937
+ return null;
2938
+ }
2939
+ }
2940
+ function hasMcpServer(client, env = process.env, home = homedir6()) {
2941
+ const raw = readText(mcpConfigPath(client, env, home));
2942
+ if (raw === null) return false;
2943
+ try {
2944
+ if (client.id === "codex") return Boolean(parseToml3(raw).mcp_servers?.[MCP_SERVER_NAME]);
2945
+ if (client.id === "hermes") return parseDocument(raw).hasIn(["mcp_servers", MCP_SERVER_NAME]);
2946
+ return Boolean(JSON.parse(raw).mcpServers?.[MCP_SERVER_NAME]);
2947
+ } catch {
2948
+ return false;
2949
+ }
2950
+ }
2951
+ function planned(client, path, entry2, opts) {
2952
+ return { agent: client.id, outcome: "planned", detail: `write to ${path}: ${redact(entry2, opts.token)}` };
2953
+ }
2954
+ function written(client, path, text) {
2955
+ try {
2956
+ writeFileSync5(path, text, { encoding: "utf8", mode: 384 });
2957
+ } catch (err) {
2958
+ return { agent: client.id, outcome: "failed", detail: String(err) };
2959
+ }
2960
+ return { agent: client.id, outcome: "registered" };
2961
+ }
2962
+ function tomlTableKey(name) {
2963
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2964
+ return `(?:${escaped}|"${escaped}"|'${escaped}')`;
2965
+ }
2966
+ function withoutTomlServer(text, name) {
2967
+ const ours = new RegExp(`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*${tomlTableKey(name)}\\s*(?:\\.[^\\]]*)?\\]\\s*(?:#.*)?$`);
2968
+ let skipping = false;
2969
+ const kept = [];
2970
+ for (const line of text.split(/\r?\n/)) {
2971
+ if (/^\s*\[/.test(line)) skipping = ours.test(line);
2972
+ if (!skipping) kept.push(line);
2973
+ }
2974
+ return kept.join("\n").replace(/\n+$/, "");
2975
+ }
2976
+ function registerCodex(client, opts) {
2977
+ const path = mcpConfigPath(client);
2978
+ const block = [
2979
+ `[mcp_servers.${MCP_SERVER_NAME}]`,
2980
+ `url = ${JSON.stringify(opts.mcpUrl)}`,
2981
+ `http_headers = { Authorization = ${JSON.stringify(`Bearer ${opts.token}`)} }`
2982
+ ].join("\n");
2983
+ if (opts.dryRun) return planned(client, path, block, opts);
2984
+ const raw = readText(path) ?? "";
2985
+ let before;
2986
+ try {
2987
+ before = parseToml3(raw);
2988
+ } catch (err) {
2989
+ return { agent: client.id, outcome: "failed", detail: `${path} is not valid TOML, left as it was (${String(err).split("\n")[0]})` };
2990
+ }
2991
+ const rest = withoutTomlServer(raw, MCP_SERVER_NAME);
2992
+ const text = `${rest ? `${rest}
3149
2993
 
3150
- The agent decides; the application acts. Keep these in the application's own
3151
- database:
2994
+ ` : ""}${block}
2995
+ `;
2996
+ try {
2997
+ const after = parseToml3(text);
2998
+ const others = Object.keys(before.mcp_servers ?? {}).filter((name) => name !== MCP_SERVER_NAME);
2999
+ 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");
3000
+ } catch (err) {
3001
+ return { agent: client.id, outcome: "failed", detail: `could not edit ${path} safely, left as it was (${String(err).split("\n")[0]})` };
3002
+ }
3003
+ return written(client, path, text);
3004
+ }
3005
+ function registerCursor(client, opts) {
3006
+ const path = mcpConfigPath(client);
3007
+ const entry2 = { url: opts.mcpUrl, headers: { Authorization: `Bearer ${opts.token}` } };
3008
+ if (opts.dryRun) return planned(client, path, JSON.stringify({ [MCP_SERVER_NAME]: entry2 }, null, 2), opts);
3009
+ let existing = {};
3010
+ try {
3011
+ const parsed = JSON.parse(readText(path) ?? "");
3012
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
3013
+ } catch {
3014
+ existing = {};
3015
+ }
3016
+ const updated = { ...existing, mcpServers: { ...existing.mcpServers ?? {}, [MCP_SERVER_NAME]: entry2 } };
3017
+ return written(client, path, `${JSON.stringify(updated, null, 2)}
3018
+ `);
3019
+ }
3020
+ function registerHermes(client, opts) {
3021
+ const path = mcpConfigPath(client);
3022
+ const entry2 = { url: opts.mcpUrl, headers: { Authorization: `Bearer ${opts.token}` } };
3023
+ if (opts.dryRun) return planned(client, path, `mcp_servers.${MCP_SERVER_NAME}: ${JSON.stringify(entry2)}`, opts);
3024
+ const doc = parseDocument(readText(path) ?? "");
3025
+ 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]})` };
3026
+ if (doc.contents !== null && !isMap(doc.contents)) {
3027
+ return { agent: client.id, outcome: "failed", detail: `${path} is not a YAML mapping, left as it was` };
3028
+ }
3029
+ doc.setIn(["mcp_servers", MCP_SERVER_NAME], doc.createNode(entry2));
3030
+ return written(client, path, doc.toString());
3031
+ }
3032
+ function refreshMcpRegistrations(opts, clients = detectMcpClients(), runner = systemCommandRunner()) {
3033
+ return clients.filter((client) => hasMcpServer(client)).map((client) => registerMcpServer(client, opts, runner));
3034
+ }
3152
3035
 
3153
- - **One job per event**, with a unique event key. The same key with the same
3154
- payload returns the job it made; with a different payload it is a conflict.
3155
- - **The session id**, saved before you start polling, so a restart picks the
3156
- job up where it was.
3157
- - **A confirmation receipt** from the signed-in customer before any side
3158
- effect.
3159
- - **A unique action key per job**, so the side effect happens once. Agent
3160
- retries do not make it happen once; the key does.
3161
- - **Webhooks to the customer** with an event id, retried until acknowledged.
3036
+ // src/shell-env.ts
3037
+ import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
3038
+ import { homedir as homedir7 } from "os";
3039
+ import { basename as basename6, dirname as dirname5, join as join11 } from "path";
3040
+ var TOKEN_ENV = "AGENTSKY_API_KEY";
3041
+ var HOOK_MARK = "# AgentSky API token, kept current by sky";
3042
+ function shellEnvFiles() {
3043
+ const dir = dirname5(configPath());
3044
+ return { sh: join11(dir, "env.sh"), fish: join11(dir, "env.fish") };
3045
+ }
3046
+ function quoted(value) {
3047
+ return `'${value.replace(/'/g, `'\\''`)}'`;
3048
+ }
3049
+ function shellStartupFiles(env = process.env, home = homedir7(), platform = process.platform) {
3050
+ const shell = basename6(env.SHELL ?? "");
3051
+ const files2 = [];
3052
+ const zshenv = join11(env.ZDOTDIR || home, ".zshenv");
3053
+ if (shell === "zsh" || platform === "darwin" || existsSync8(zshenv)) files2.push({ path: zshenv, kind: "sh" });
3054
+ const bashrc = join11(home, ".bashrc");
3055
+ if (shell === "bash" || existsSync8(bashrc)) files2.push({ path: bashrc, kind: "sh" });
3056
+ const bashProfile = join11(home, ".bash_profile");
3057
+ if (existsSync8(bashProfile)) files2.push({ path: bashProfile, kind: "sh" });
3058
+ const fish = join11(env.XDG_CONFIG_HOME || join11(home, ".config"), "fish");
3059
+ if (shell === "fish" || existsSync8(fish)) files2.push({ path: join11(fish, "conf.d", "agentsky.fish"), kind: "fish" });
3060
+ return files2;
3061
+ }
3062
+ function writeShellEnv(token, opts = {}) {
3063
+ const { sh, fish } = shellEnvFiles();
3064
+ const startup = shellStartupFiles(opts.env, opts.home, opts.platform);
3065
+ if (opts.dryRun) return { outcome: "planned", files: [sh, fish, ...startup.map((file) => file.path)] };
3066
+ const header2 = "# Written by sky. `sky auth login` and `sky auth logout` rewrite this file.\n";
3067
+ const hooked = [];
3068
+ try {
3069
+ mkdirSync3(dirname5(sh), { recursive: true });
3070
+ writeFileSync6(sh, `${header2}${token ? `export ${TOKEN_ENV}=${quoted(token)}` : `unset ${TOKEN_ENV}`}
3071
+ `, { mode: 384 });
3072
+ writeFileSync6(fish, `${header2}${token ? `set -gx ${TOKEN_ENV} ${quoted(token)}` : `set -e ${TOKEN_ENV}`}
3073
+ `, { mode: 384 });
3074
+ for (const file of startup) {
3075
+ const existing = existsSync8(file.path) ? readFileSync11(file.path, "utf8") : "";
3076
+ if (existing.includes(HOOK_MARK)) continue;
3077
+ const line = file.kind === "fish" ? `${HOOK_MARK}
3078
+ test -f ${quoted(fish)}; and source ${quoted(fish)}
3079
+ ` : `${HOOK_MARK}
3080
+ [ -f ${quoted(sh)} ] && . ${quoted(sh)}
3081
+ `;
3082
+ mkdirSync3(dirname5(file.path), { recursive: true });
3083
+ appendFileSync(file.path, `${existing && !existing.endsWith("\n") ? "\n" : ""}${line}`);
3084
+ hooked.push(file.path);
3085
+ }
3086
+ } catch (err) {
3087
+ return { outcome: "failed", files: [sh, fish, ...hooked], detail: String(err) };
3088
+ }
3089
+ return { outcome: token ? "written" : "cleared", files: [sh, fish, ...hooked] };
3090
+ }
3162
3091
 
3163
- ## Before launch
3092
+ // src/local-access.ts
3093
+ function syncLocalAccess(token, clients = detectMcpClients()) {
3094
+ const lines = [];
3095
+ const shell = writeShellEnv(token);
3096
+ if (shell.outcome === "failed") lines.push(` ${TOKEN_ENV} not updated for new shells: ${shell.detail}`);
3097
+ else lines.push(token ? ` new shells get ${TOKEN_ENV} for this login` : ` new shells no longer get ${TOKEN_ENV}`);
3098
+ if (token) {
3099
+ for (const registered of refreshMcpRegistrations({ token, mcpUrl: mcpUrlFor(apiBase()) }, clients)) {
3100
+ const name = clients.find((client) => client.id === registered.agent)?.name ?? registered.agent;
3101
+ 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\``);
3102
+ }
3103
+ return lines;
3104
+ }
3105
+ const holding = clients.filter((client) => hasMcpServer(client)).map((client) => client.name);
3106
+ 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`);
3107
+ return lines;
3108
+ }
3164
3109
 
3165
- Check one real session:
3110
+ // src/login.ts
3111
+ function b64url(buf) {
3112
+ return buf.toString("base64url");
3113
+ }
3114
+ function openBrowser2(url) {
3115
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3116
+ try {
3117
+ spawn2(cmd, [url], { stdio: "ignore", detached: true }).unref();
3118
+ } catch {
3119
+ }
3120
+ }
3121
+ async function login(opts) {
3122
+ const verifier = b64url(randomBytes(32));
3123
+ const challenge = b64url(createHash4("sha256").update(verifier).digest());
3124
+ const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
3125
+ saveConfig({
3126
+ ...loadConfig(),
3127
+ apiBase: apiBase(),
3128
+ token: result.token,
3129
+ email: result.user.email ?? void 0,
3130
+ currentUniverse: void 0
3131
+ // personal by default
3132
+ });
3133
+ console.log(
3134
+ `Logged in as ${result.user.email ?? "you"}${result.universe ? ` (universe: ${result.universe.slug})` : ""}`
3135
+ );
3136
+ for (const line of syncLocalAccess(result.token)) console.log(line);
3137
+ }
3138
+ async function browserLogin(verifier, challenge) {
3139
+ const state = b64url(randomBytes(16));
3140
+ const code = await new Promise((resolve10, reject) => {
3141
+ const server = createServer((req, res) => {
3142
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
3143
+ if (url.pathname !== "/callback") {
3144
+ res.writeHead(404).end();
3145
+ return;
3146
+ }
3147
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
3148
+ res.end("<p>Logged in \u2014 you can close this tab and return to your terminal.</p>");
3149
+ const got = url.searchParams.get("code");
3150
+ if (url.searchParams.get("state") !== state || !got) {
3151
+ reject(new Error("login callback state mismatch"));
3152
+ } else {
3153
+ resolve10(got);
3154
+ }
3155
+ server.close();
3156
+ });
3157
+ server.listen(0, "127.0.0.1", () => {
3158
+ const address = server.address();
3159
+ const port = typeof address === "object" && address ? address.port : 0;
3160
+ const authorizeUrl = `${consoleBase()}/cli/authorize?state=${state}&port=${port}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
3161
+ console.log("Opening your browser to authorize this machine\u2026");
3162
+ console.log(`If it doesn't open, visit:
3163
+ ${authorizeUrl}`);
3164
+ openBrowser2(authorizeUrl);
3165
+ });
3166
+ setTimeout(() => {
3167
+ server.close();
3168
+ reject(new Error("login timed out after 10 minutes"));
3169
+ }, 10 * 60 * 1e3).unref();
3170
+ });
3171
+ return api(
3172
+ "POST",
3173
+ "/cli/token-exchange",
3174
+ { code, verifier, clientName: `cli ${hostname()}` },
3175
+ { auth: false }
3176
+ );
3177
+ }
3178
+ async function headlessLogin(verifier, challenge) {
3179
+ const request = b64url(randomBytes(24));
3180
+ const authorizeUrl = `${consoleBase()}/cli/authorize?request=${request}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
3181
+ console.log(`Visit this URL on any device to authorize:
3182
+ ${authorizeUrl}
3183
+ `);
3184
+ process.stdout.write("Waiting for approval\u2026");
3185
+ const deadline = Date.now() + 10 * 60 * 1e3;
3186
+ while (Date.now() < deadline) {
3187
+ await new Promise((r) => setTimeout(r, 3e3));
3188
+ const res = await fetch(
3189
+ `${apiBase()}/cli/poll?request=${request}&verifier=${encodeURIComponent(verifier)}&clientName=${encodeURIComponent(`cli ${hostname()}`)}`,
3190
+ { headers: { "user-agent": USER_AGENT } }
3191
+ );
3192
+ if (!res.ok) {
3193
+ const body2 = await res.json().catch(() => null);
3194
+ throw new Error(body2?.error?.message ?? `poll failed (${res.status})`);
3195
+ }
3196
+ const body = await res.json();
3197
+ if (body.status === "approved") {
3198
+ console.log(" approved.");
3199
+ return body;
3200
+ }
3201
+ process.stdout.write(".");
3202
+ }
3203
+ throw new Error("login timed out after 10 minutes");
3204
+ }
3166
3205
 
3167
- - \`GET /v1/sessions/{id}\` lists only the upload under \`resources\`.
3168
- - \`GET /v1/hook_rules\` and \`GET /v1/universes/{slug}/policies\` show the rules
3169
- above.
3170
- - Ask the agent directly to run a shell command. The answer has to be the
3171
- platform's refusal.
3172
- - A file id from another customer's workspace is refused at session create.
3173
- `;
3206
+ // src/connectors.ts
3207
+ async function listApps(query, limit = 50) {
3208
+ const apps = [];
3209
+ let cursor = null;
3210
+ do {
3211
+ const params = new URLSearchParams();
3212
+ if (query) params.set("q", query);
3213
+ if (cursor) params.set("cursor", cursor);
3214
+ const qs = params.toString();
3215
+ const page = await api("GET", `/connectors/apps${qs ? `?${qs}` : ""}`);
3216
+ apps.push(...page.apps);
3217
+ cursor = page.next_cursor;
3218
+ } while (cursor && apps.length < limit);
3219
+ return apps.slice(0, limit);
3220
+ }
3221
+ function connectionLine2(c) {
3222
+ return `${c.id} ${c.app_name} (${c.app}) ${c.label} since ${c.created_at.slice(0, 10)}`;
3223
+ }
3224
+ function workspaceConnectionLine(c) {
3225
+ const owner = c.workspace_owned ? "the workspace" : c.mine ? "you" : c.shared_by?.name ?? c.shared_by?.user_id ?? "someone";
3226
+ return `${c.connection_id} ${c.app_name} (${c.app}) ${c.label} shared by ${owner}`;
3227
+ }
3228
+ function browserReturn(start, opts = {}) {
3229
+ return new Promise((resolve10, reject) => {
3230
+ const server = createServer2((req, res) => {
3231
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
3232
+ if (url.pathname !== "/done" && url.pathname !== "/failed") {
3233
+ res.writeHead(404).end();
3234
+ return;
3235
+ }
3236
+ const ok = url.pathname === "/done";
3237
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
3238
+ 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>");
3239
+ server.close();
3240
+ if (ok) resolve10(url.searchParams.get("account_id") ?? void 0);
3241
+ else reject(new ApiError(400, "connect_failed", "the app was not connected"));
3242
+ });
3243
+ server.listen(0, "127.0.0.1", () => {
3244
+ const address = server.address();
3245
+ const port = typeof address === "object" && address ? address.port : 0;
3246
+ const base = `http://127.0.0.1:${port}`;
3247
+ start(`${base}/done`, `${base}/failed`).then(
3248
+ (authorizeUrl) => {
3249
+ console.log(`Authorize access in your browser:
3250
+ ${authorizeUrl}`);
3251
+ if (opts.open !== false) openBrowser2(authorizeUrl);
3252
+ },
3253
+ (err) => {
3254
+ server.close();
3255
+ reject(err);
3256
+ }
3257
+ );
3258
+ });
3259
+ setTimeout(() => {
3260
+ server.close();
3261
+ reject(new ApiError(408, "connect_timeout", "nobody finished the authorization within 10 minutes"));
3262
+ }, opts.timeoutMs ?? 10 * 6e4).unref();
3263
+ });
3264
+ }
3265
+ async function connectApp(app, opts = {}) {
3266
+ const base = opts.workspace ? `/universes/${encodeURIComponent(opts.workspace)}/connections` : "/connectors/connections";
3267
+ const accountId = await browserReturn(async (success_url, error_url) => {
3268
+ const { url } = await api("POST", base, { app, success_url, error_url });
3269
+ return url;
3270
+ }, { open: opts.open });
3271
+ const done = await api("POST", `${base}/complete`, {
3272
+ app,
3273
+ ...accountId ? { account_id: accountId } : {}
3274
+ });
3275
+ return done.connection;
3174
3276
  }
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
3188
-
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.
3193
-
3194
- ## Listing workspaces
3195
-
3196
- \`sky workspace list\` (or \`sky universe list\`) prints your workspaces, with
3197
- \`*\` marking the current default.
3198
-
3199
- ## .agentsky.json
3200
3277
 
3201
- To set a per-repo default, create \`.agentsky.json\` at the git root:
3278
+ // src/routine-triggers.ts
3279
+ var triggerPath = (id, resource) => `/routines/${encodeURIComponent(id)}/${resource}`;
3280
+ function inboundLine(u) {
3281
+ if (!u) return "no inbound URL \u2014 make one with --create";
3282
+ 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"}`;
3283
+ }
3284
+ function precheckLine(p) {
3285
+ if (!p) return "no precheck: every scheduled fire runs";
3286
+ const last = p.last_result ? `last check ${p.last_result}${p.last_error ? ` (${p.last_error.type}: ${p.last_error.message})` : ""}` : "not checked yet";
3287
+ return `fires only when ${p.url} changes \xB7 ${last}`;
3288
+ }
3289
+ function triggerLine(t, available = true) {
3290
+ if (!t) return "no chat trigger";
3291
+ const where = t.thread_id ? `thread ${t.thread_id} on ${t.connection_id}` : `any conversation on ${t.connection_id}`;
3292
+ const what = t.text_prefix ? `messages starting "${t.text_prefix}"` : "every message";
3293
+ return `${what} in ${where} start a run${available ? "" : " (chat messages are not offered to routines in this deployment)"}`;
3294
+ }
3295
+ function deliverLine(d) {
3296
+ return d ? `answers post to thread ${d.thread_id} on ${d.connection_id}` : "answers are posted nowhere";
3297
+ }
3298
+ function triggerBody(opts) {
3299
+ if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
3300
+ return { connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {}, ...opts.prefix ? { text_prefix: opts.prefix } : {} };
3301
+ }
3302
+ function deliverBody(opts) {
3303
+ if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
3304
+ return { type: "channel_thread", connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {} };
3305
+ }
3306
+ async function triggerRequest(id, resource, change) {
3307
+ const path = triggerPath(id, resource);
3308
+ if (change.remove) return api("DELETE", path);
3309
+ if (change.create) return api("POST", path);
3310
+ if (change.body) return api("PUT", path, change.body);
3311
+ return api("GET", path);
3312
+ }
3202
3313
 
3203
- \`\`\`json
3204
- {"workspace": "my-team-slug"}
3205
- \`\`\`
3206
- `;
3314
+ // src/session-workspace.ts
3315
+ import { spawnSync as spawnSync4 } from "child_process";
3316
+ import { createHash as createHash5, randomUUID } from "crypto";
3317
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
3318
+ import { basename as basename7, join as join12, relative as relative3, resolve as resolve5 } from "path";
3319
+ var SINGLE_IMPORT_MAX = 25 * 1024 * 1024;
3320
+ var PART_SIZE = 8 * 1024 * 1024;
3321
+ var SKIPPED2 = /* @__PURE__ */ new Set([".git", "node_modules"]);
3322
+ var workspacePath = (id) => `/sessions/${encodeURIComponent(id)}/workspace`;
3323
+ async function listFiles2(id, path, hidden) {
3324
+ const query = new URLSearchParams();
3325
+ if (path) query.set("path", path);
3326
+ if (hidden) query.set("hidden", "true");
3327
+ const qs = query.toString();
3328
+ return api("GET", `${workspacePath(id)}/files${qs ? `?${qs}` : ""}`);
3207
3329
  }
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;
3330
+ function entryLine(e) {
3331
+ const size = e.type === "directory" ? "-" : String(e.size ?? 0);
3332
+ return `${e.type === "directory" ? "d" : "-"} ${size.padStart(10)} ${(e.mtime ?? "").slice(0, 16).replace("T", " ")} ${e.name}${e.type === "directory" ? "/" : ""}`;
3333
+ }
3334
+ async function readFile(id, path) {
3335
+ return (await apiBytes("GET", `${workspacePath(id)}/files/content?path=${encodeURIComponent(path)}`)).bytes;
3336
+ }
3337
+ function renderChanges(changes) {
3338
+ if (!changes.repos.length) return [`no git repository under ${changes.root}`];
3339
+ return changes.repos.flatMap((r) => [
3340
+ `${r.path === "." ? changes.root : r.path}: ${r.status ? "changed" : "clean"}`,
3341
+ ...(r.stat ?? r.status ?? "").split("\n").filter(Boolean).map((line) => ` ${line}`)
3342
+ ]);
3343
+ }
3344
+ async function workspacePatch(id, repo) {
3345
+ const { patch, ...meta } = await api("POST", `${workspacePath(id)}/patch`, { repo });
3346
+ return { meta, patch: Buffer.from(patch, "base64") };
3347
+ }
3348
+ function folderFiles(dir) {
3349
+ const git2 = spawnSync4("git", ["-C", dir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8" });
3350
+ const top = spawnSync4("git", ["-C", dir, "rev-parse", "--show-prefix"], { encoding: "utf8" });
3351
+ if (git2.status === 0 && top.status === 0) {
3352
+ return git2.stdout.split("\0").filter((path) => path && statSync6(join12(dir, path), { throwIfNoEntry: false })?.isFile());
3214
3353
  }
3354
+ const files2 = [];
3355
+ const walk = (at) => {
3356
+ for (const entry2 of readdirSync5(at, { withFileTypes: true })) {
3357
+ if (SKIPPED2.has(entry2.name)) continue;
3358
+ const full = join12(at, entry2.name);
3359
+ if (entry2.isDirectory()) walk(full);
3360
+ else if (entry2.isFile()) files2.push(relative3(dir, full));
3361
+ }
3362
+ };
3363
+ walk(dir);
3364
+ return files2;
3215
3365
  }
3216
- function isInstalledSkill(dir) {
3217
- return installedVersion(dir) !== null;
3366
+ function folderArchive(dir, files2) {
3367
+ const tar = spawnSync4("tar", ["--format=ustar", "-czf", "-", "-C", dir, "-T", "-"], {
3368
+ input: files2.join("\n"),
3369
+ env: { ...process.env, COPYFILE_DISABLE: "1" },
3370
+ maxBuffer: 2 * 1024 * 1024 * 1024
3371
+ });
3372
+ if (tar.status !== 0) throw new ApiError(400, "archive_failed", `tar failed: ${tar.stderr.toString().trim()}`);
3373
+ return tar.stdout;
3218
3374
  }
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
- `);
3375
+ async function importFolder(id, dir, opts = {}) {
3376
+ const root = resolve5(dir);
3377
+ const name = opts.name ?? basename7(root);
3378
+ const archive = opts.archive ?? folderArchive(root, folderFiles(root));
3379
+ if (archive.length <= SINGLE_IMPORT_MAX) {
3380
+ const res = await apiBytes("POST", `${workspacePath(id)}/import?name=${encodeURIComponent(name)}`, { bytes: archive, mediaType: "application/gzip" });
3381
+ return res.json();
3229
3382
  }
3230
- if (opts.catalog) {
3231
- mkdirSync2(dir, { recursive: true });
3232
- writeFileSync4(join10(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
3383
+ const sha256 = createHash5("sha256").update(archive).digest("hex");
3384
+ let upload = await api("POST", `${workspacePath(id)}/imports`, { name, size: archive.length, sha256, part_size: PART_SIZE });
3385
+ const uploadPath = `${workspacePath(id)}/imports/${encodeURIComponent(upload.id)}`;
3386
+ for (let part = 0; part < upload.parts; part += 1) {
3387
+ if (upload.received.includes(part)) continue;
3388
+ const bytes = archive.subarray(part * upload.part_size, (part + 1) * upload.part_size);
3389
+ await apiBytes("PUT", `${uploadPath}/parts/${part}`, { bytes, mediaType: "application/octet-stream" });
3233
3390
  }
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 };
3391
+ upload = await api("POST", `${uploadPath}/complete`);
3392
+ while (upload.state !== "imported" && upload.state !== "failed") {
3393
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
3394
+ upload = await api("GET", uploadPath);
3395
+ }
3396
+ if (upload.state === "failed") throw new ApiError(422, upload.error?.code ?? "import_failed", upload.error?.message ?? "the import failed");
3397
+ return { path: upload.path ?? name, files: upload.files ?? 0 };
3240
3398
  }
3241
-
3242
- // src/mcp-register.ts
3243
- var MCP_SERVER_NAME = SKILL_NAME;
3244
- var CODEX_TOKEN_ENV = "AGENTSKY_API_KEY";
3245
- function systemCommandRunner() {
3399
+ function renderAccess(a) {
3400
+ const lines = [
3401
+ `${a.session_id}${a.title ? ` \u2014 ${a.title}` : ""}`,
3402
+ ` ${a.visibility === "private" ? "private to its owner" : `visible to ${a.workspace.name} (${a.workspace.slug})`} \xB7 you are its ${a.access}`,
3403
+ ` owner: ${a.owner.name ?? a.owner.email ?? "unknown"}`
3404
+ ];
3405
+ if (a.viewers) lines.push(a.viewers.length ? ` viewers: ${a.viewers.map((v) => v.email).join(", ")}` : " viewers: none");
3406
+ return lines;
3407
+ }
3408
+ function accessPatch(opts) {
3409
+ if (opts.private && opts.members) throw new ApiError(400, "invalid_request", "pass --private or --members, not both");
3246
3410
  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
- }
3411
+ ...opts.private ? { visibility: "private" } : opts.members ? { visibility: "workspace" } : {},
3412
+ ...opts.addViewer?.length ? { add_viewers: opts.addViewer } : {},
3413
+ ...opts.removeViewer?.length ? { remove_viewers: opts.removeViewer } : {}
3252
3414
  };
3253
3415
  }
3254
- function mcpUrlFor(apiBase2) {
3255
- return `${apiBase2.replace(/\/$/, "")}/mcp`;
3416
+ function engineChange(model, thinking) {
3417
+ if (!model && !thinking) return null;
3418
+ return {
3419
+ ...model ? { llm: model } : {},
3420
+ ...thinking ? { reasoningEffort: thinking === "default" ? null : thinking } : {}
3421
+ };
3256
3422
  }
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
- };
3423
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
3424
+ async function handoff(id, targetAgent, opts = {}) {
3425
+ let { handoff: h } = await api("POST", `/sessions/${encodeURIComponent(id)}/handoff`, {
3426
+ target_agent: targetAgent,
3427
+ ...opts.fromMessage ? { last_message_id: opts.fromMessage } : {}
3428
+ }, { idempotencyKey: opts.idempotencyKey ?? randomUUID() });
3429
+ opts.onStatus?.(h);
3430
+ while (opts.wait && !TERMINAL.has(h.status)) {
3431
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
3432
+ const next = (await api("GET", `/session-handoffs/${encodeURIComponent(h.id)}`)).handoff;
3433
+ if (next.status !== h.status) opts.onStatus?.(next);
3434
+ h = next;
3264
3435
  }
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
- };
3436
+ return h;
3437
+ }
3438
+
3439
+ // src/render.ts
3440
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
3441
+ var red = (s) => `\x1B[31m${s}\x1B[0m`;
3442
+ function clip(value, max = 120) {
3443
+ const text = typeof value === "string" ? value : JSON.stringify(value);
3444
+ return text.length > max ? `${text.slice(0, max)}\u2026` : text;
3445
+ }
3446
+ function renderEvent(event, data) {
3447
+ const part = data.part;
3448
+ switch (event) {
3449
+ case "agent.message":
3450
+ console.log(String(data.text ?? ""));
3451
+ break;
3452
+ case "agent.reasoning":
3453
+ console.log(dim(`\xB7 ${clip(part?.text ?? "")}`));
3454
+ break;
3455
+ case "agent.tool_use":
3456
+ console.log(dim(`\u2192 ${String(part?.tool_name ?? "tool")}(${clip(part?.args ?? {}, 80)})`));
3457
+ break;
3458
+ case "agent.tool_result":
3459
+ console.log(dim(`\u2190 ${String(part?.tool_name ?? "tool")} ${String(part?.status ?? "")}`));
3460
+ break;
3461
+ case "agent.status":
3462
+ console.log(dim(`\u2026 ${String(part?.level ?? "")} ${String(part?.text ?? "")}`.trim()));
3463
+ break;
3464
+ case "user.interrupt":
3465
+ console.log(dim("\u23F9 interrupting"));
3466
+ break;
3467
+ case "session.error": {
3468
+ const error = data.error;
3469
+ const message2 = String(error?.message ?? error?.type ?? "unknown");
3470
+ if (error?.retry_status === "retrying") console.error(dim(`retrying: ${message2}`));
3471
+ else console.error(red(`error: ${message2}`));
3472
+ break;
3473
+ }
3474
+ case "session.status_terminated":
3475
+ console.log(dim("session terminated"));
3476
+ break;
3477
+ default:
3478
+ break;
3271
3479
  }
3272
- return null;
3273
- }
3274
- function redact(text, token) {
3275
- return text.split(token).join(`${token.slice(0, 10)}\u2026`);
3276
3480
  }
3277
- function registerMcpServer(agent2, opts, runner = systemCommandRunner()) {
3278
- if (agent2.id === "cursor") {
3279
- return registerCursorMcpServer(agent2, opts);
3481
+
3482
+ // src/session-read.ts
3483
+ var TURN_TYPES = ["user.message", "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
3484
+ function latestTurn(events) {
3485
+ let start = 0;
3486
+ for (let index = events.length - 1; index >= 0; index -= 1) {
3487
+ if (events[index].type === "user.message") {
3488
+ start = index + 1;
3489
+ break;
3490
+ }
3280
3491
  }
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}` };
3492
+ const reading = { finished: false, messages: [], errors: [] };
3493
+ for (const event of events.slice(start)) {
3494
+ if (event.type === "agent.message" && event.text?.trim()) reading.messages.push(event.text);
3495
+ else if (event.type === "session.error" && event.error?.retry_status !== "retrying") {
3496
+ reading.errors.push(event.error?.message ?? event.error?.type ?? "unknown error");
3497
+ } else if (event.type === "session.status_terminated") reading.finished = true;
3498
+ else if (event.type === "session.status_idle" && event.stop_reason?.type !== "requires_action") reading.finished = true;
3299
3499
  }
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;
3306
- }
3307
- } catch {
3500
+ return reading;
3501
+ }
3502
+ async function history2(sessionId) {
3503
+ const events = [];
3504
+ let cursor = null;
3505
+ for (; ; ) {
3506
+ const query = new URLSearchParams({ limit: "500" });
3507
+ for (const type of TURN_TYPES) query.append("types[]", type);
3508
+ if (cursor) query.set("cursor", cursor);
3509
+ const page = await api(
3510
+ "GET",
3511
+ `/sessions/${encodeURIComponent(sessionId)}/events?${query}`
3512
+ );
3513
+ events.push(...page.events);
3514
+ if (!page.hasMore || !page.cursor) return events;
3515
+ cursor = page.cursor;
3308
3516
  }
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) };
3517
+ }
3518
+ async function readTurn(sessionId, opts = {}) {
3519
+ const deadline = Date.now() + (opts.timeoutSeconds ?? 600) * 1e3;
3520
+ for (; ; ) {
3521
+ const reading = latestTurn(await history2(sessionId));
3522
+ if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
3523
+ await new Promise((resolve10) => setTimeout(resolve10, opts.intervalMs ?? 3e3));
3317
3524
  }
3318
- return { agent: agent2.id, outcome: "registered" };
3319
3525
  }
3320
3526
 
3527
+ // src/setup.ts
3528
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
3529
+ import { dirname as dirname7, join as join15 } from "path";
3530
+ import { createInterface as createInterface3 } from "readline";
3531
+
3321
3532
  // src/setup-local.ts
3322
3533
  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";
3534
+ import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
3535
+ import { homedir as homedir8 } from "os";
3536
+ import { dirname as dirname6, isAbsolute as isAbsolute2, join as join13, resolve as resolve6 } from "path";
3537
+ import { parse as parseToml4 } from "smol-toml";
3327
3538
  var MAX_SKILL_FILE_BYTES = 1024 * 1024;
3328
3539
  var MAX_IMPORT_DEPTH = 5;
3329
3540
  var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
@@ -3347,11 +3558,11 @@ function redacted(data) {
3347
3558
  function skillFiles2(root, prefix) {
3348
3559
  const files2 = [];
3349
3560
  for (const entry2 of readdirSync6(root, { withFileTypes: true })) {
3350
- const path = join11(root, entry2.name);
3561
+ const path = join13(root, entry2.name);
3351
3562
  if (entry2.isDirectory()) {
3352
3563
  if (!SKIPPED_DIRS.has(entry2.name)) files2.push(...skillFiles2(path, `${prefix}/${entry2.name}`));
3353
3564
  } else if (entry2.isFile() && !sensitive(entry2.name) && statSync7(path).size <= MAX_SKILL_FILE_BYTES) {
3354
- const bytes = redacted(readFileSync11(path));
3565
+ const bytes = redacted(readFileSync12(path));
3355
3566
  if (bytes) files2.push({ path: `${prefix}/${entry2.name}`, bytes });
3356
3567
  }
3357
3568
  }
@@ -3361,9 +3572,9 @@ function readLocalSkills(agents) {
3361
3572
  const skills2 = [];
3362
3573
  const ignored = [];
3363
3574
  for (const agent2 of agents) {
3364
- if (!existsSync7(agent2.skillsDir)) continue;
3575
+ if (!existsSync9(agent2.skillsDir)) continue;
3365
3576
  for (const entry2 of readdirSync6(agent2.skillsDir, { withFileTypes: true })) {
3366
- const dir = join11(agent2.skillsDir, entry2.name);
3577
+ const dir = join13(agent2.skillsDir, entry2.name);
3367
3578
  let isDir2 = entry2.isDirectory();
3368
3579
  if (entry2.isSymbolicLink()) {
3369
3580
  try {
@@ -3372,8 +3583,8 @@ function readLocalSkills(agents) {
3372
3583
  isDir2 = false;
3373
3584
  }
3374
3585
  }
3375
- if (!isDir2 || !existsSync7(join11(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
3376
- const name = frontmatterName(readFileSync11(join11(dir, "SKILL.md"), "utf8"));
3586
+ if (!isDir2 || !existsSync9(join13(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
3587
+ const name = frontmatterName(readFileSync12(join13(dir, "SKILL.md"), "utf8"));
3377
3588
  if (!name) {
3378
3589
  ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
3379
3590
  continue;
@@ -3401,11 +3612,11 @@ function inlineImports(content, from, home, depth, seen) {
3401
3612
  }
3402
3613
  const target = fence ? void 0 : line.match(/^@(\S+\.(?:md|markdown|txt))$/i)?.[1];
3403
3614
  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);
3615
+ const path = target.startsWith("~/") ? join13(home, target.slice(2)) : isAbsolute2(target) ? target : resolve6(dirname6(from), target);
3405
3616
  if (seen.includes(path)) return "";
3406
3617
  let imported;
3407
3618
  try {
3408
- imported = readFileSync11(path, "utf8");
3619
+ imported = readFileSync12(path, "utf8");
3409
3620
  } catch {
3410
3621
  return line;
3411
3622
  }
@@ -3413,15 +3624,15 @@ function inlineImports(content, from, home, depth, seen) {
3413
3624
  return inlineImports(imported, path, home, depth + 1, seen).trim();
3414
3625
  }).join("\n");
3415
3626
  }
3416
- function readGlobalInstructions(agents, home = homedir6()) {
3627
+ function readGlobalInstructions(agents, home = homedir8()) {
3417
3628
  const found = [];
3418
3629
  for (const agent2 of agents) {
3419
3630
  const file = INSTRUCTION_FILES2[agent2.id];
3420
3631
  if (!file) continue;
3421
- const path = join11(agent2.root, file);
3632
+ const path = join13(agent2.root, file);
3422
3633
  let raw;
3423
3634
  try {
3424
- raw = readFileSync11(path, "utf8");
3635
+ raw = readFileSync12(path, "utf8");
3425
3636
  } catch {
3426
3637
  continue;
3427
3638
  }
@@ -3440,7 +3651,7 @@ ${item.content.trim()}`).join("\n\n");
3440
3651
  }
3441
3652
  function readJson3(path) {
3442
3653
  try {
3443
- return JSON.parse(readFileSync11(path, "utf8"));
3654
+ return JSON.parse(readFileSync12(path, "utf8"));
3444
3655
  } catch {
3445
3656
  return void 0;
3446
3657
  }
@@ -3454,18 +3665,19 @@ function bearerOf(def) {
3454
3665
  return match?.[1] ?? null;
3455
3666
  }
3456
3667
  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 ?? {};
3668
+ if (agent2.id === "claude_code") return readJson3(join13(home, ".claude.json"))?.mcpServers ?? {};
3669
+ if (agent2.id === "cursor") return readJson3(join13(agent2.root, "mcp.json"))?.mcpServers ?? {};
3459
3670
  try {
3460
- return parseToml3(readFileSync11(join11(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
3671
+ return parseToml4(readFileSync12(join13(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
3461
3672
  } catch {
3462
3673
  return {};
3463
3674
  }
3464
3675
  }
3465
- function findLocalSecrets(agents, home = homedir6()) {
3676
+ function findLocalSecrets(agents, home = homedir8()) {
3466
3677
  const secrets2 = [];
3467
3678
  for (const agent2 of agents) {
3468
3679
  for (const [name, def] of Object.entries(serversOf(agent2, home))) {
3680
+ if (name === SKILL_NAME) continue;
3469
3681
  if (!def || typeof def !== "object" || typeof def.url !== "string" || !/^https:\/\//i.test(def.url)) continue;
3470
3682
  const token = bearerOf(def);
3471
3683
  if (!token || /^\$\{?[A-Za-z_]/.test(token)) continue;
@@ -3478,9 +3690,9 @@ function findLocalSecrets(agents, home = homedir6()) {
3478
3690
 
3479
3691
  // src/subscriptions.ts
3480
3692
  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";
3693
+ import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
3694
+ import { homedir as homedir9 } from "os";
3695
+ import { join as join14 } from "path";
3484
3696
  import { createInterface as createInterface2 } from "readline";
3485
3697
  var ALIASES = {
3486
3698
  claude: "anthropic",
@@ -3575,12 +3787,12 @@ async function connectClaude() {
3575
3787
  const expiry = sub.expiresAt ? ` (expires ~${sub.expiresAt.slice(0, 10)})` : "";
3576
3788
  console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
3577
3789
  }
3578
- function codexSignIn(env = process.env, home = homedir7()) {
3579
- const path = join12(env.CODEX_HOME || join12(home, ".codex"), "auth.json");
3580
- if (!existsSync8(path)) {
3790
+ function codexSignIn(env = process.env, home = homedir9()) {
3791
+ const path = join14(env.CODEX_HOME || join14(home, ".codex"), "auth.json");
3792
+ if (!existsSync10(path)) {
3581
3793
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
3582
3794
  }
3583
- const raw = readFileSync12(path, "utf8");
3795
+ const raw = readFileSync13(path, "utf8");
3584
3796
  try {
3585
3797
  const parsed = JSON.parse(raw);
3586
3798
  return { path, raw, account: parsed.tokens?.account_id };
@@ -3588,12 +3800,12 @@ function codexSignIn(env = process.env, home = homedir7()) {
3588
3800
  throw new ApiError(400, "invalid_codex_auth", `${path} isn't valid JSON \u2014 re-run \`codex login\``);
3589
3801
  }
3590
3802
  }
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)) {
3803
+ function cursorSignIn(env = process.env, home = homedir9()) {
3804
+ const path = join14(env.XDG_CONFIG_HOME || join14(home, ".config"), "cursor", "auth.json");
3805
+ if (!existsSync10(path)) {
3594
3806
  throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
3595
3807
  }
3596
- const raw = readFileSync12(path, "utf8");
3808
+ const raw = readFileSync13(path, "utf8");
3597
3809
  try {
3598
3810
  const parsed = JSON.parse(raw);
3599
3811
  if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") throw new Error("shape");
@@ -3752,11 +3964,11 @@ function planProviders(names2) {
3752
3964
  }));
3753
3965
  }
3754
3966
  function reportPath() {
3755
- return join13(dirname6(configPath()), "setup.json");
3967
+ return join15(dirname7(configPath()), "setup.json");
3756
3968
  }
3757
3969
  function loadReport() {
3758
3970
  try {
3759
- const saved = JSON.parse(readFileSync13(reportPath(), "utf8"));
3971
+ const saved = JSON.parse(readFileSync14(reportPath(), "utf8"));
3760
3972
  return { agents: saved.agents ?? [], plans: saved.plans ?? [], ...saved.secrets ? { secrets: saved.secrets } : {} };
3761
3973
  } catch {
3762
3974
  return { agents: [], plans: [] };
@@ -3769,8 +3981,8 @@ function mergeReport(base, next) {
3769
3981
  return { agents, plans, ...secrets2 ? { secrets: secrets2 } : {} };
3770
3982
  }
3771
3983
  function saveReport(report) {
3772
- mkdirSync3(dirname6(reportPath()), { recursive: true });
3773
- writeFileSync6(reportPath(), `${JSON.stringify(report, null, 2)}
3984
+ mkdirSync4(dirname7(reportPath()), { recursive: true });
3985
+ writeFileSync7(reportPath(), `${JSON.stringify(report, null, 2)}
3774
3986
  `, { mode: 384 });
3775
3987
  }
3776
3988
  function confirm3(question) {
@@ -3788,7 +4000,7 @@ function kb2(bytes) {
3788
4000
  function describe2(event) {
3789
4001
  switch (event.type) {
3790
4002
  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).";
4003
+ 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
4004
  case "catalog":
3793
4005
  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
4006
  case "skill":
@@ -3798,12 +4010,23 @@ function describe2(event) {
3798
4010
  case "registered":
3799
4011
  return ` agentsky MCP server registered for ${event.agent}${event.detail ? ` (${event.detail})` : ""}`;
3800
4012
  case "planned":
3801
- return ` would run: ${event.detail}`;
4013
+ return ` ${event.agent}: would ${event.detail}`;
3802
4014
  case "unsupported":
3803
4015
  return null;
3804
4016
  default:
3805
4017
  return ` agentsky MCP server not registered for ${event.agent}: ${event.detail ?? event.outcome}`;
3806
4018
  }
4019
+ case "env":
4020
+ switch (event.outcome) {
4021
+ case "written":
4022
+ return ` new shells get ${event.variable} for this login (open a new terminal, or run: . ${event.files[0]})`;
4023
+ case "planned":
4024
+ return ` would write ${event.variable} to ${event.files.join(", ")}`;
4025
+ case "cleared":
4026
+ return null;
4027
+ default:
4028
+ return ` ${event.variable} not set for new shells: ${event.detail ?? event.outcome}`;
4029
+ }
3807
4030
  case "plan":
3808
4031
  return ` ${event.provider} plan: ${event.outcome === "already" ? "already connected" : event.outcome}${event.detail ? ` (${event.detail})` : ""}`;
3809
4032
  case "found": {
@@ -3893,8 +4116,11 @@ async function runSetup(opts) {
3893
4116
  };
3894
4117
  const agreed = async (question) => !interactive || confirm3(question);
3895
4118
  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 })) });
4119
+ const named = opts.agents?.filter((id) => id !== "hermes");
4120
+ const agents = opts.agents?.length && !named?.length ? [] : selectLocalAgents(named);
4121
+ const hermes = !opts.agents?.length || opts.agents.includes("hermes") ? detectHermes() : null;
4122
+ const mcpOnly = hermes ? [hermes] : [];
4123
+ emit({ type: "agents", agents: [...agents, ...mcpOnly].map((agent2) => ({ id: agent2.id, name: agent2.name })) });
3898
4124
  const needsToken = !opts.dryRun && (has("skill") || has("logins") || has("secrets") || has("import") && !opts.key);
3899
4125
  if (needsToken && !apiToken()) {
3900
4126
  if (opts.json) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
@@ -3916,10 +4142,14 @@ async function runSetup(opts) {
3916
4142
  held = new Map((claimed.body.skills ?? []).map((skill) => [skill.name, skill.fingerprint]));
3917
4143
  };
3918
4144
  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?")) {
4145
+ 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
4146
  const { catalog: catalog2, detail } = await readCatalog();
3921
4147
  emit(catalog2 ? { type: "catalog", outcome: "read", agents: catalog2.length } : { type: "catalog", outcome: "unavailable", ...detail ? { detail } : {} });
3922
4148
  const token = apiToken();
4149
+ const registerMcp = (client) => {
4150
+ const registered = token ? registerMcpServer(client, { token, mcpUrl: mcpUrlFor(apiBase()), dryRun: opts.dryRun }) : { agent: client.id, outcome: "skipped", detail: "not logged in" };
4151
+ emit({ type: "mcp", agent: client.id, outcome: registered.outcome, ...registered.detail ? { detail: registered.detail } : {} });
4152
+ };
3923
4153
  for (const agent2 of agents) {
3924
4154
  try {
3925
4155
  const installed = installSkill(agent2, { dryRun: opts.dryRun, catalog: catalog2 });
@@ -3931,8 +4161,12 @@ async function runSetup(opts) {
3931
4161
  run2.agents.push({ id: agent2.id, skill: "failed" });
3932
4162
  continue;
3933
4163
  }
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 } : {} });
4164
+ registerMcp(agent2);
4165
+ }
4166
+ for (const client of mcpOnly) registerMcp(client);
4167
+ if (token) {
4168
+ const shell = writeShellEnv(token, { dryRun: opts.dryRun });
4169
+ emit({ type: "env", variable: TOKEN_ENV, outcome: shell.outcome, files: shell.files, ...shell.detail ? { detail: shell.detail } : {} });
3936
4170
  }
3937
4171
  await tell();
3938
4172
  }
@@ -4119,17 +4353,17 @@ function renderArtifact(artifact2) {
4119
4353
  }
4120
4354
 
4121
4355
  // 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";
4356
+ import { readdirSync as readdirSync7, readFileSync as readFileSync15, statSync as statSync8 } from "fs";
4357
+ import { basename as basename8, join as join16, relative as relative4, resolve as resolve7 } from "path";
4124
4358
  function skillBundle(dir) {
4125
4359
  const root = resolve7(dir);
4126
- const top = basename7(root);
4360
+ const top = basename8(root);
4127
4361
  const files2 = [];
4128
4362
  const walk = (path) => {
4129
4363
  for (const name of readdirSync7(path).sort()) {
4130
- const full = join14(path, name);
4364
+ const full = join16(path, name);
4131
4365
  if (statSync8(full).isDirectory()) walk(full);
4132
- else files2.push({ path: `${top}/${relative4(root, full)}`, bytes: readFileSync14(full) });
4366
+ else files2.push({ path: `${top}/${relative4(root, full)}`, bytes: readFileSync15(full) });
4133
4367
  }
4134
4368
  };
4135
4369
  walk(root);
@@ -4139,7 +4373,7 @@ function skillBundle(dir) {
4139
4373
  async function uploadSkill(dir, displayName) {
4140
4374
  const form = new FormData();
4141
4375
  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)));
4376
+ form.append("displayName", displayName ?? basename8(resolve7(dir)));
4143
4377
  return apiForm("POST", "/skills", form);
4144
4378
  }
4145
4379
  async function listSkills() {
@@ -4168,8 +4402,8 @@ async function getSkillUsage(since) {
4168
4402
 
4169
4403
  // src/task-export.ts
4170
4404
  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";
4405
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
4406
+ import { dirname as dirname8, join as join17 } from "path";
4173
4407
  import { stringify as stringifyToml2 } from "smol-toml";
4174
4408
  var TEXT_MAX = 2e4;
4175
4409
  function scrub(text, names2) {
@@ -4457,14 +4691,14 @@ async function exportTaskHarbor(taskRef, outDir, opts) {
4457
4691
  fileBytes.set(file.path, bytes);
4458
4692
  }
4459
4693
  const files2 = buildHarborTask(record, { ...opts, fileBytes });
4460
- const written = [];
4694
+ const written2 = [];
4461
4695
  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);
4696
+ const target = join17(outDir, path);
4697
+ mkdirSync5(dirname8(target), { recursive: true });
4698
+ writeFileSync8(target, content, typeof content === "string" ? { mode: path.endsWith(".sh") || path.endsWith(".py") ? 493 : 420 } : void 0);
4699
+ written2.push(path);
4466
4700
  }
4467
- return { written, record };
4701
+ return { written: written2, record };
4468
4702
  }
4469
4703
  function parseRedactNames(values) {
4470
4704
  return (values ?? []).map((value) => {
@@ -4773,6 +5007,44 @@ function renderBrowserProfile(profile) {
4773
5007
  return lines.join("\n");
4774
5008
  }
4775
5009
 
5010
+ // src/mac.ts
5011
+ var REACH_KINDS = ["folder", "browser", "screen", "host"];
5012
+ var OPEN = /* @__PURE__ */ new Set(["pending", "asking"]);
5013
+ var MAX_WAIT_SECONDS = 25;
5014
+ async function listMacs() {
5015
+ return (await api("GET", "/desktop/machines")).machines;
5016
+ }
5017
+ async function askMac(machine, kind, reason) {
5018
+ if (!REACH_KINDS.includes(kind)) throw new Error(`kind must be one of ${REACH_KINDS.join(", ")}`);
5019
+ return api("POST", `/desktop/machines/${encodeURIComponent(machine)}/reach-requests`, { kind, reason });
5020
+ }
5021
+ async function readReachRequest(id, waitSeconds = 0) {
5022
+ const wait = Math.min(Math.max(Math.floor(waitSeconds), 0), MAX_WAIT_SECONDS);
5023
+ return (await api("GET", `/desktop/reach-requests/${encodeURIComponent(id)}${wait ? `?wait=${wait}` : ""}`)).reach_request;
5024
+ }
5025
+ async function waitForAnswer(id, budgetSeconds) {
5026
+ const until = Date.now() + budgetSeconds * 1e3;
5027
+ for (; ; ) {
5028
+ const left = Math.ceil((until - Date.now()) / 1e3);
5029
+ const request = await readReachRequest(id, Math.max(left, 0));
5030
+ if (!OPEN.has(request.status) || left <= 0) return request;
5031
+ }
5032
+ }
5033
+ async function takeBack(mount) {
5034
+ await api("DELETE", `/desktop/mounts/${encodeURIComponent(mount)}`);
5035
+ }
5036
+ function renderMac(mac2) {
5037
+ const lends = mac2.reach ? mac2.reach.kinds.join(", ") || "nothing" : "unknown (update the Mac app)";
5038
+ const screen = mac2.reach && !mac2.reach.kinds.includes("screen") ? ` screen: ${mac2.reach.screen}` : "";
5039
+ const lines = [`${mac2.id} ${mac2.name} ${mac2.online ? "online" : "offline"} can lend: ${lends}${screen}`];
5040
+ for (const mount of mac2.mounts ?? []) lines.push(` ${mount.id} ${mount.kind.padEnd(7)} ${mount.root ?? mount.label}`);
5041
+ return lines.join("\n");
5042
+ }
5043
+ function renderReachRequest(request) {
5044
+ if (request.status === "granted" && request.mount) return `${request.id} granted ${request.mount.id} (${request.mount.kind}${request.mount.root ? ` ${request.mount.root}` : ""})`;
5045
+ return `${request.id} ${request.status}${request.error ? ` ${request.error.code}: ${request.error.message}` : ""}`;
5046
+ }
5047
+
4776
5048
  // src/compare.ts
4777
5049
  async function listComparisons(opts = {}) {
4778
5050
  const qs = opts.archived ? "?archived=true" : "";
@@ -4793,6 +5065,53 @@ async function listThreads2(opts = {}) {
4793
5065
  const { threads } = await api("GET", `/threads${qs}`);
4794
5066
  return threads;
4795
5067
  }
5068
+ async function startThread(body) {
5069
+ return api("POST", "/threads", body);
5070
+ }
5071
+ var threadPath = (id) => `/threads/${encodeURIComponent(id)}`;
5072
+ async function getThread(id) {
5073
+ return api("GET", threadPath(id));
5074
+ }
5075
+ async function organizeThread(id, change) {
5076
+ return api("PATCH", threadPath(id), change);
5077
+ }
5078
+ async function deleteThread(id) {
5079
+ return api("DELETE", threadPath(id));
5080
+ }
5081
+ async function sendThreadMessage(id, body) {
5082
+ return api("POST", `${threadPath(id)}/messages`, body);
5083
+ }
5084
+ async function addThreadAgent(id, body) {
5085
+ return api("POST", `${threadPath(id)}/agents`, body);
5086
+ }
5087
+ async function removeThreadAgent(id, columnId) {
5088
+ return api("DELETE", `${threadPath(id)}/agents/${encodeURIComponent(columnId)}`);
5089
+ }
5090
+ async function interruptThread(id, columnId) {
5091
+ return api("POST", `${threadPath(id)}/interrupt`, columnId ? { columnId } : {});
5092
+ }
5093
+ async function forkThread(id, body) {
5094
+ return api("POST", `${threadPath(id)}/fork`, body);
5095
+ }
5096
+ async function changeThreadModel(id, change, columnId) {
5097
+ return api("PATCH", `${threadPath(id)}/engine${columnId ? `?column=${encodeURIComponent(columnId)}` : ""}`, change);
5098
+ }
5099
+ async function searchThreads(q, opts = {}) {
5100
+ const params = new URLSearchParams({ q, ...opts.threads ? { threads: opts.threads } : {}, ...opts.cursor ? { cursor: opts.cursor } : {} });
5101
+ return api("GET", `/threads/search?${params}`);
5102
+ }
5103
+ async function threadOptions() {
5104
+ return api("GET", "/threads/options");
5105
+ }
5106
+ async function joinThread(sessionId) {
5107
+ return api("POST", "/threads/joinable", { sessionId });
5108
+ }
5109
+ async function listProjects() {
5110
+ return api("GET", "/projects");
5111
+ }
5112
+ async function rememberProject(project2) {
5113
+ return api("PUT", "/projects", { project: project2 });
5114
+ }
4796
5115
  function renderComparisonSummary(c) {
4797
5116
  const agents = c.agents.map((a) => `${a.agentType}/${a.llm}`).join(" vs ");
4798
5117
  const status = c.statusLine ?? c.attention ?? (c.archived ? "archived" : "active");
@@ -4805,20 +5124,129 @@ function renderThreadSummary(t) {
4805
5124
  return `${t.id.slice(0, 12)} ${flag}${t.title.slice(0, 40).padEnd(44)} [${agents}] ${status}`;
4806
5125
  }
4807
5126
 
5127
+ // src/library.ts
5128
+ var artifactPath = (id) => `/library/artifacts/${encodeURIComponent(id)}`;
5129
+ async function listLibrary(query = {}) {
5130
+ const params = new URLSearchParams(Object.entries(query).filter((entry2) => Boolean(entry2[1])));
5131
+ const qs = params.size ? `?${params}` : "";
5132
+ return api("GET", `/library/artifacts${qs}`);
5133
+ }
5134
+ async function uploadToLibrary(file, requestId, workspace2) {
5135
+ const form = new FormData();
5136
+ form.append("file", new Blob([new Uint8Array(file.bytes)], { type: file.mediaType }), file.name);
5137
+ form.append("requestId", requestId);
5138
+ if (workspace2) form.append("workspace", workspace2);
5139
+ return apiForm("POST", "/library/artifacts", form);
5140
+ }
5141
+ async function getLibraryArtifact(id) {
5142
+ return api("GET", artifactPath(id));
5143
+ }
5144
+ async function deleteLibraryArtifact(id, revision) {
5145
+ return api("DELETE", artifactPath(id), { revision });
5146
+ }
5147
+ async function shareLibraryArtifact(id, enabled) {
5148
+ const { currentVersion } = await getLibraryArtifact(id);
5149
+ return api("PATCH", `${artifactPath(id)}/share`, { enabled, revision: currentVersion });
5150
+ }
5151
+ async function placeLibraryArtifact(id, workspace2) {
5152
+ return api("PATCH", `${artifactPath(id)}/workspace`, { workspace: workspace2 });
5153
+ }
5154
+ async function downloadLibraryArtifact(id, version) {
5155
+ return apiBytes("GET", `${artifactPath(id)}/download${version ? `?version=${version}` : ""}`);
5156
+ }
5157
+ function renderLibraryArtifact(artifact2) {
5158
+ const version = artifact2.currentVersion ? `v${artifact2.currentVersion}` : "";
5159
+ return `${artifact2.id} ${artifact2.name} ${version}${artifact2.shared ? " shared" : ""}`;
5160
+ }
5161
+
5162
+ // src/billing-plans.ts
5163
+ var readCodingPlan = () => api("GET", "/billing/coding-plan");
5164
+ function joinCodingPlan(dollars2, requestId) {
5165
+ const amount = Number(dollars2.trim().replace(/^\$/, ""));
5166
+ if (!Number.isInteger(amount) || amount <= 0) throw new Error(`<dollars> must be whole dollars, got ${JSON.stringify(dollars2)}`);
5167
+ return api("POST", "/billing/coding-plan", { amount_usd: amount, request_id: requestId });
5168
+ }
5169
+ var cancelCodingPlan = () => api("DELETE", "/billing/coding-plan");
5170
+ function renderCodingPlan(plan) {
5171
+ if (!plan.active) {
5172
+ const lines2 = [`coding plan: not a member (enrollment ${plan.enrollment}; monthly amounts $${plan.amounts_usd.join(", $")})`];
5173
+ if (plan.charge_failure) lines2.push(` last charge failed: ${plan.charge_failure}`);
5174
+ return lines2.join("\n");
5175
+ }
5176
+ const until = plan.current_period_end?.slice(0, 10) ?? "?";
5177
+ const lines = [`coding plan: $${plan.monthly_credit_usd}/month, paid until ${until}, ${plan.renews ? "renews" : "does not renew"}`];
5178
+ if (plan.charge_failure) lines.push(` last charge failed: ${plan.charge_failure}`);
5179
+ return lines.join("\n");
5180
+ }
5181
+ function readRequestUsage(opts) {
5182
+ const query = usageQuery(opts);
5183
+ const cursor = opts.cursor ? `${query ? "&" : "?"}cursor=${encodeURIComponent(opts.cursor)}` : "";
5184
+ return api("GET", `/billing/usage/requests${query}${cursor}`);
5185
+ }
5186
+ function renderRequestUsage(page) {
5187
+ const lines = page.rows.map((row) => `${row.at.slice(0, 19).replace("T", " ")} ${(row.apiKeyName ?? "").padEnd(16).slice(0, 16)} ${(row.model ?? "-").padEnd(28).slice(0, 28)} ${String(row.httpStatus ?? "-").padStart(3)} ${String(row.inputTokens ?? 0).padStart(9)} in ${String(row.outputTokens ?? 0).padStart(8)} out ${`$${(row.billedCostUsd ?? 0).toFixed(6)}`.padStart(11)}`);
5188
+ if (!lines.length) lines.push("no API requests in this range");
5189
+ if (page.nextCursor) lines.push(`more: --cursor ${page.nextCursor}`);
5190
+ return lines.join("\n");
5191
+ }
5192
+ var readReferralLinks = () => api("GET", "/billing/referral-links");
5193
+ function referralLinkBody(opts) {
5194
+ if (opts.price === void 0 === !opts.sku?.length) throw new Error("pass --price for every model, or --sku id=multiplier for each SKU, not both");
5195
+ const body = {};
5196
+ if (opts.price !== void 0) body.customer_multiplier = multiplier("--price", opts.price);
5197
+ if (opts.sku?.length) {
5198
+ body.sku_prices = opts.sku.map((pair) => {
5199
+ const at = pair.lastIndexOf("=");
5200
+ if (at <= 0) throw new Error(`--sku must be id=multiplier, got ${JSON.stringify(pair)}`);
5201
+ return { sku_id: pair.slice(0, at), multiplier: multiplier("--sku", pair.slice(at + 1)) };
5202
+ });
5203
+ }
5204
+ if (opts.email) body.audience_email = opts.email;
5205
+ if (opts.uses) body.max_redemptions = wholeNumber2("--uses", opts.uses);
5206
+ if (opts.days) body.expires_in_days = wholeNumber2("--days", opts.days);
5207
+ return body;
5208
+ }
5209
+ var createReferralLink = (opts) => api("POST", "/billing/referral-links", referralLinkBody(opts));
5210
+ var disableReferralLink = (id) => api("POST", `/billing/referral-links/${encodeURIComponent(id)}/disable`);
5211
+ function renderReferralLinks(dashboard) {
5212
+ const lines = [`priced from ${dashboard.source_plan.name}: customers pay at least ${Math.round(dashboard.source_plan.multiplier * 100)}% of public price`];
5213
+ for (const link2 of dashboard.links) {
5214
+ const state = link2.disabled_at ? "disabled" : `expires ${link2.expires_at.slice(0, 10)}`;
5215
+ const audience = link2.audience_email ? ` for ${link2.audience_email} (${link2.email_status})` : "";
5216
+ lines.push(` ${link2.id} ${Math.round(link2.customer_multiplier * 100)}% ${link2.redeemed_count}/${link2.max_redemptions} used ${state}${audience}`);
5217
+ lines.push(` ${link2.url}`);
5218
+ }
5219
+ if (!dashboard.links.length) lines.push(" no links yet");
5220
+ const earned = dashboard.earnings.reduce((sum, earning) => sum + earning.amount_usd, 0);
5221
+ lines.push(`earned $${earned.toFixed(2)} from ${dashboard.earnings.length} customer${dashboard.earnings.length === 1 ? "" : "s"}`);
5222
+ return lines.join("\n");
5223
+ }
5224
+ function multiplier(flag, value) {
5225
+ const trimmed = value.trim();
5226
+ const percent = trimmed.endsWith("%");
5227
+ const number = Number(percent ? trimmed.slice(0, -1) : trimmed);
5228
+ if (!Number.isFinite(number) || number <= 0) throw new Error(`${flag} must be a share of public price like 0.8 or 80%, got ${JSON.stringify(value)}`);
5229
+ return percent ? number / 100 : number;
5230
+ }
5231
+ function wholeNumber2(flag, value) {
5232
+ if (!/^\d+$/.test(value.trim())) throw new Error(`${flag} must be a whole number, got ${JSON.stringify(value)}`);
5233
+ return Number(value.trim());
5234
+ }
5235
+
4808
5236
  // 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";
5237
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
5238
+ import { dirname as dirname9, join as join18, resolve as resolve8 } from "path";
4811
5239
  var HARNESS_FILE = "harness.json";
4812
5240
  function harnessFilePath(dir) {
4813
- return join16(resolve8(dir), HARNESS_FILE);
5241
+ return join18(resolve8(dir), HARNESS_FILE);
4814
5242
  }
4815
5243
  function readHarnessFile(dir) {
4816
5244
  const path = harnessFilePath(dir);
4817
- if (!existsSync9(path)) {
5245
+ if (!existsSync11(path)) {
4818
5246
  throw new Error(`no ${HARNESS_FILE} found in ${resolve8(dir)} \u2014 run \`sky harness init\` first`);
4819
5247
  }
4820
5248
  try {
4821
- return JSON.parse(readFileSync15(path, "utf8"));
5249
+ return JSON.parse(readFileSync16(path, "utf8"));
4822
5250
  } catch (err) {
4823
5251
  throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
4824
5252
  }
@@ -4845,11 +5273,11 @@ var INIT_COMMENT = `# harness.json \u2014 workspace harness profile skeleton
4845
5273
  `;
4846
5274
  function harnessInit(dir) {
4847
5275
  const path = harnessFilePath(dir);
4848
- if (existsSync9(path)) {
5276
+ if (existsSync11(path)) {
4849
5277
  throw new Error(`${path} already exists \u2014 delete it to re-init`);
4850
5278
  }
4851
- mkdirSync5(dirname8(path), { recursive: true });
4852
- writeFileSync8(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
5279
+ mkdirSync6(dirname9(path), { recursive: true });
5280
+ writeFileSync9(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
4853
5281
  `);
4854
5282
  console.log(`Created ${path}`);
4855
5283
  console.log(INIT_COMMENT);
@@ -5010,6 +5438,7 @@ auth.command("logout").description("forget the stored token (revoke it in Develo
5010
5438
  run(async () => {
5011
5439
  saveConfig({ ...loadConfig(), token: void 0, email: void 0 });
5012
5440
  console.log(`Logged out. (Token revocation: Developer \u2192 API keys.)`);
5441
+ for (const line of syncLocalAccess(void 0)) console.log(line);
5013
5442
  })
5014
5443
  );
5015
5444
  auth.command("whoami").description("who the current token acts as").action(
@@ -5042,6 +5471,19 @@ auth.command("usage <provider>").description("plan allowance left on a connected
5042
5471
  auth.command("switch-to-credits <provider>").description("move the agents on a model subscription onto platform credits (do this before disconnecting)").option("--account <id>", "an additional account (default: the provider's first account)").option("-y, --yes", "skip the confirmation prompt").action(run(async (name, opts) => switchToCredits(name, opts)));
5043
5472
  auth.command("accounts").description("list connected model subscriptions and the agents they power").action(run(async () => listSubscriptions()));
5044
5473
  auth.command("disconnect <provider>").description("disconnect a model subscription (agents on it fail until switched)").option("-y, --yes", "skip the confirmation prompt").action(run(async (name, opts) => disconnect(name, opts)));
5474
+ auth.command("apps").description("the registered apps you let into your account, with the scopes you allowed").action(
5475
+ run(async () => {
5476
+ const grants = await listAppGrants();
5477
+ if (!grants.length) console.log("No apps have access to your account.");
5478
+ for (const grant of grants) console.log(renderAppGrant(grant));
5479
+ })
5480
+ );
5481
+ auth.command("revoke-app <clientId>").description("remove an app's access to your account; its tokens stop working at once (admin scope)").action(
5482
+ run(async (clientId) => {
5483
+ await revokeAppGrant(clientId);
5484
+ console.log(`Removed ${clientId}'s access to your account`);
5485
+ })
5486
+ );
5045
5487
  var universe = program.command("universe").description("grouping boundary for agents");
5046
5488
  universe.command("list").action(
5047
5489
  run(async () => {
@@ -5088,7 +5530,7 @@ universe.command("invite <emails...>").description("invite people by email; prin
5088
5530
  run(async (emails, opts) => {
5089
5531
  const result = await invite(await workspaceSlug(opts.universe), emails, opts);
5090
5532
  for (const i of result.invites) console.log(`invited ${i.email} as ${i.role} ${i.id}`);
5091
- for (const link of result.links) console.log(` ${link.email} joins with: sky universe join ${link.token}`);
5533
+ for (const link2 of result.links) console.log(` ${link2.email} joins with: sky universe join ${link2.token}`);
5092
5534
  if (!result.invites.length) console.log("nobody new to invite \u2014 everyone listed is already a member");
5093
5535
  })
5094
5536
  );
@@ -5405,12 +5847,12 @@ session.command("share <ref>").description("print the session's share link, turn
5405
5847
  console.log(`Sharing is off for ${id}`);
5406
5848
  return;
5407
5849
  }
5408
- const link = await api("POST", path, {
5850
+ const link2 = await api("POST", path, {
5409
5851
  access: opts.view ? "view" : "prompt",
5410
5852
  ...opts.rotate ? { rotate: true } : {}
5411
5853
  });
5412
- if (link.shareUrl) console.log(link.shareUrl);
5413
- else console.log(`view-only token: ${link.token} \u2014 read with GET /links/${link.token}/events`);
5854
+ if (link2.shareUrl) console.log(link2.shareUrl);
5855
+ else console.log(`view-only token: ${link2.token} \u2014 read with GET /links/${link2.token}/events`);
5414
5856
  })
5415
5857
  );
5416
5858
  session.command("usage <ref>").description("what the session has cost so far, and its newest billed entries").option("--limit <n>", "entries to list (default 20)").option("--json", "print the API's answer as JSON").action(
@@ -5558,7 +6000,7 @@ sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1
5558
6000
  run(async (ref, path, opts) => {
5559
6001
  const bytes = await readFile(await resolveSessionRef(ref), path);
5560
6002
  if (opts.out) {
5561
- writeFileSync9(opts.out, bytes);
6003
+ writeFileSync10(opts.out, bytes);
5562
6004
  console.error(`saved ${bytes.length} bytes to ${opts.out}`);
5563
6005
  } else process.stdout.write(bytes);
5564
6006
  })
@@ -5575,7 +6017,7 @@ sessionWorkspace.command("patch <ref> [repo]").description("the work in a reposi
5575
6017
  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
6018
  if (!patch.length) console.error("nothing changed");
5577
6019
  else if (opts.out) {
5578
- writeFileSync9(opts.out, patch);
6020
+ writeFileSync10(opts.out, patch);
5579
6021
  console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
5580
6022
  } else process.stdout.write(patch);
5581
6023
  })
@@ -5720,7 +6162,7 @@ catalog.command("search [words...]").description("find an agent for a piece of w
5720
6162
  Start one: sky session create @<handle> --message "<the job>"`);
5721
6163
  })
5722
6164
  );
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(
6165
+ 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
6166
  run(async (opts) => {
5725
6167
  const installs = selectLocalAgents(opts.agent).map((a) => installSkill(a, { dryRun: opts.dryRun }));
5726
6168
  if (opts.json) {
@@ -5736,7 +6178,7 @@ program.command("skill").description("the AgentSky skill for the agents on this
5736
6178
  })
5737
6179
  );
5738
6180
  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(
6181
+ 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
6182
  run(async (steps, opts) => {
5741
6183
  const unknown = steps.find((step) => !SETUP_STEPS.includes(step));
5742
6184
  if (unknown) throw new Error(`unknown step ${unknown} \u2014 expected ${SETUP_STEPS.join(" | ")}`);
@@ -5784,8 +6226,8 @@ secrets.command("unset <id> <key>").action(
5784
6226
  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
6227
  run(async (opts) => {
5786
6228
  const path = resolve9(opts.file);
5787
- if (!existsSync10(path)) {
5788
- const name = basename8(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
6229
+ if (!existsSync12(path)) {
6230
+ const name = basename9(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
5789
6231
  scaffoldAgentFile(path, name);
5790
6232
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
5791
6233
  }
@@ -6067,8 +6509,8 @@ files.command("rm <id>").description("delete a file (a session that already rece
6067
6509
  );
6068
6510
  files.command("download <id>").description("download a session file's bytes").requiredOption("-o, --out <path>", "where to write the bytes").action(
6069
6511
  run(async (id, opts) => {
6070
- const written = await downloadFileTo(id, opts.out);
6071
- console.log(`Wrote ${written} bytes to ${opts.out}`);
6512
+ const written2 = await downloadFileTo(id, opts.out);
6513
+ console.log(`Wrote ${written2} bytes to ${opts.out}`);
6072
6514
  })
6073
6515
  );
6074
6516
  var task = program.command("task").description("a task's shared files and open requests (the task is its first session's id)");
@@ -6083,14 +6525,14 @@ task.command("get <task> <path>").description("read one task file; prints it, or
6083
6525
  run(async (taskId, path, opts) => {
6084
6526
  const bytes = await readTaskFile(taskId, path);
6085
6527
  if (opts.out) {
6086
- writeFileSync9(opts.out, bytes);
6528
+ writeFileSync10(opts.out, bytes);
6087
6529
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6088
6530
  } else process.stdout.write(bytes);
6089
6531
  })
6090
6532
  );
6091
6533
  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
6534
  run(async (taskId, path, local) => {
6093
- const { file, conflict } = await writeTaskFile(taskId, path, readFileSync16(local), mediaTypeFor(local));
6535
+ const { file, conflict } = await writeTaskFile(taskId, path, readFileSync17(local), mediaTypeFor(local));
6094
6536
  console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
6095
6537
  })
6096
6538
  );
@@ -6104,9 +6546,9 @@ task.command("export <task>").description("write the task's session record as a
6104
6546
  run(async (taskId, opts) => {
6105
6547
  const tier = opts.tier === "gold" || opts.tier === "exploratory" ? opts.tier : "silver";
6106
6548
  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");
6549
+ const { written: written2, record } = await exportTaskHarbor(taskId, outDir, { tier, redactNames: parseRedactNames(opts.redact) });
6550
+ 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"})`);
6551
+ if (!written2.includes("environment/Dockerfile")) console.log("environment/: not resettable from the record \u2014 see environment/README.md before running with Harbor");
6110
6552
  })
6111
6553
  );
6112
6554
  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(
@@ -6227,6 +6669,56 @@ reflections.command("list").description("the workspace's proposals, newest first
6227
6669
  );
6228
6670
  reflections.command("approve <id>").description("write the proposal through the skill or memory store").action(run(async (id) => decideReflection(id, "approve")));
6229
6671
  reflections.command("reject <id>").description("decline the proposal; nothing is written").action(run(async (id) => decideReflection(id, "reject")));
6672
+ var library = program.command("library").description("your artifact Library on agentsky.dev: files and sites your agents saved or you uploaded");
6673
+ library.command("list").description("your Library, newest first").option("--search <text>", "only names that match").option("--cursor <cursor>", "the next page").option("--json", "print as JSON").action(
6674
+ run(async (opts) => {
6675
+ const page = await listLibrary({ q: opts.search, cursor: opts.cursor });
6676
+ if (opts.json) {
6677
+ console.log(JSON.stringify(page, null, 2));
6678
+ return;
6679
+ }
6680
+ if (!page.artifacts.length) console.log("no artifacts");
6681
+ for (const a of page.artifacts) console.log(renderLibraryArtifact(a));
6682
+ if (page.nextCursor) console.log(`more: --cursor ${page.nextCursor}`);
6683
+ })
6684
+ );
6685
+ library.command("upload <path>").description("save a file from this computer to your Library").option("--workspace <key>", "place it in this workspace").action(
6686
+ run(async (path, opts) => {
6687
+ const bytes = readFileSync17(path);
6688
+ const { artifact: artifact2 } = await uploadToLibrary({ bytes, name: basename9(path), mediaType: "application/octet-stream" }, `sky-upload-${crypto.randomUUID()}`, opts.workspace);
6689
+ console.log(renderLibraryArtifact(artifact2));
6690
+ })
6691
+ );
6692
+ library.command("show <id>").description("one artifact and its saved versions").action(run(async (id) => {
6693
+ console.log(JSON.stringify(await getLibraryArtifact(id), null, 2));
6694
+ }));
6695
+ library.command("delete <id>").description("delete your artifact with all its versions").action(
6696
+ run(async (id) => {
6697
+ const { currentVersion } = await getLibraryArtifact(id);
6698
+ await deleteLibraryArtifact(id, currentVersion ?? 0);
6699
+ console.log(`deleted ${id}`);
6700
+ })
6701
+ );
6702
+ for (const [verb, enabled, description] of [
6703
+ ["share", true, "turn on the artifact's public link"],
6704
+ ["unshare", false, "turn off the artifact's public link"]
6705
+ ]) {
6706
+ library.command(`${verb} <id>`).description(description).action(run(async (id) => {
6707
+ const state = await shareLibraryArtifact(id, enabled);
6708
+ console.log(state.url ?? `${verb}: ${id}`);
6709
+ }));
6710
+ }
6711
+ library.command("place <id> <workspace>").description('place your artifact in a workspace, or "me" to take it back to you alone').action(run(async (id, workspace2) => {
6712
+ await placeLibraryArtifact(id, workspace2 === "me" ? null : workspace2);
6713
+ console.log(`placed ${id}`);
6714
+ }));
6715
+ library.command("download <id> <out>").description("save one version's bytes to a file").option("--version <n>", "a saved version (the current one when omitted)").action(
6716
+ run(async (id, out, opts) => {
6717
+ const { bytes } = await downloadLibraryArtifact(id, opts.version ? Number(opts.version) : void 0);
6718
+ writeFileSync10(out, bytes);
6719
+ console.log(`wrote ${bytes.length} bytes to ${out}`);
6720
+ })
6721
+ );
6230
6722
  var artifact = program.command("artifact").description("artifacts: publish a task's files as a named, shareable result");
6231
6723
  artifact.command("list").description("the workspace's artifacts, or one task's with --task").option("--task <task>", "only this task's artifacts").action(
6232
6724
  run(async (opts) => {
@@ -6244,7 +6736,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
6244
6736
  run(async (id, path, opts) => {
6245
6737
  const bytes = await readArtifactFile(id, path);
6246
6738
  if (opts.out) {
6247
- writeFileSync9(opts.out, bytes);
6739
+ writeFileSync10(opts.out, bytes);
6248
6740
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6249
6741
  } else process.stdout.write(bytes);
6250
6742
  })
@@ -6494,6 +6986,19 @@ program.command("config").description("show the CLI config location and current
6494
6986
  console.log(` token: ${config.token ? `${config.token.slice(0, 10)}\u2026` : "(none)"}`);
6495
6987
  console.log(` universe: ${config.currentUniverse ?? "(default)"}`);
6496
6988
  });
6989
+ var link = program.command("link").description("a session someone shared with you by a guest link: send to it and stop it");
6990
+ link.command("send <link> <text...>").description("send a message through a guest link (/s/<token> URL or token); it runs and bills as the session's owner").option("--idempotency-key <key>", "repeat a send without sending twice (default: a new key)").action(
6991
+ run(async (ref, words, opts) => {
6992
+ await sendViaLink(ref, words.join(" "), opts.idempotencyKey ?? `sky-${crypto.randomUUID()}`);
6993
+ console.log("Sent. Read the reply with GET /links/<token>/events.");
6994
+ })
6995
+ );
6996
+ link.command("stop <link>").description("stop the turn running on the session a guest link opens").action(
6997
+ run(async (ref) => {
6998
+ await stopViaLink(ref);
6999
+ console.log("Stopped.");
7000
+ })
7001
+ );
6497
7002
  var billing = program.command("billing").description("your balance, auto top-up, usage across sessions and API keys, and buying credit");
6498
7003
  billing.command("show").description("balance, whether a card is saved, and auto top-up").action(
6499
7004
  run(async () => {
@@ -6520,6 +7025,11 @@ billing.command("topup <dollars>").description("buy credit with your saved card:
6520
7025
  console.log(renderTopup(result));
6521
7026
  })
6522
7027
  );
7028
+ billing.command("claim <link>").description("claim a credit invite link (agentsky.dev/i/<code>, or just the code) for your own balance").action(
7029
+ run(async (link2) => {
7030
+ console.log(renderCreditInviteClaim(await claimCreditInvite(link2)));
7031
+ })
7032
+ );
6523
7033
  billing.command("auto-topup").description("show auto top-up, or change it: --on/--off, --threshold <5|10|20>, --amount <10|20|50|100>").option("--on", "charge the saved card when the balance falls under the threshold").option("--off", "stop charging automatically").option("--threshold <dollars>", "the balance that triggers a charge").option("--amount <dollars>", "the credit each charge adds").action(
6524
7034
  run(async (opts) => {
6525
7035
  const body = autoTopupBody(opts);
@@ -6542,13 +7052,42 @@ usageOptions(
6542
7052
  run(async (opts) => {
6543
7053
  const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
6544
7054
  if (opts.out) {
6545
- writeFileSync9(opts.out, bytes);
7055
+ writeFileSync10(opts.out, bytes);
6546
7056
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6547
7057
  } else {
6548
7058
  process.stdout.write(bytes);
6549
7059
  }
6550
7060
  })
6551
7061
  );
7062
+ billing.command("requests").description("one row per API request your developer API keys made, newest first, a page at a time").option("--from <day>", "first UTC day, YYYY-MM-DD (default: 29 days ago)").option("--to <day>", "first UTC day not included (default: tomorrow)").option("--key <id>", "only this API key").option("--cursor <cursor>", "the next page, as printed after the previous one").option("--json", "print the API's answer as JSON").action(
7063
+ run(async (opts) => {
7064
+ const page = await readRequestUsage(opts);
7065
+ console.log(opts.json ? JSON.stringify(page, null, 2) : renderRequestUsage(page));
7066
+ })
7067
+ );
7068
+ var codingPlan = billing.command("coding-plan").description("the coding plan: monthly credit on your saved card, API gateway calls billed at cost");
7069
+ codingPlan.command("show").description("whether you are a member, the monthly amount, and whether it renews").action(run(async () => console.log(renderCodingPlan(await readCodingPlan()))));
7070
+ codingPlan.command("join <dollars>").description("join with your saved card (charged now), or change your monthly amount from the next renewal").option("--request-id <id>", "repeat a request without charging twice (default: a new id)").action(
7071
+ run(async (dollars2, opts) => {
7072
+ console.log(renderCodingPlan(await joinCodingPlan(dollars2, opts.requestId ?? `sky-${crypto.randomUUID()}`)));
7073
+ })
7074
+ );
7075
+ codingPlan.command("cancel").description("stop the plan renewing; the paid month stays").action(run(async () => console.log(renderCodingPlan(await cancelCodingPlan()))));
7076
+ var referralLinks = billing.command("referral-links").description("customer discount links for the API gateway (accounts on a discount plan)");
7077
+ referralLinks.command("list").description("your links, their use, and what referred customers earned you").action(run(async () => console.log(renderReferralLinks(await readReferralLinks()))));
7078
+ referralLinks.command("create").description("create a link priced at no less than your own price; --email sends it to one customer").option("--price <share>", "what the customer pays for every model, as a share of public price: 0.8 or 80%").option("--sku <id=share>", "a price for one SKU; repeat for several", collect).option("--email <address>", "email the link to this customer; it then redeems once").option("--uses <n>", "how many customers can redeem it (default 100)").option("--days <n>", "days until it expires (default 30)").action(
7079
+ run(async (opts) => {
7080
+ const { url, email_status: emailStatus } = await createReferralLink(opts);
7081
+ console.log(emailStatus ? `${url}
7082
+ email: ${emailStatus}` : url);
7083
+ })
7084
+ );
7085
+ referralLinks.command("disable <id>").description("stop a link being redeemed; customers already enrolled keep their price").action(
7086
+ run(async (id) => {
7087
+ await disableReferralLink(id);
7088
+ console.log(`Disabled ${id}`);
7089
+ })
7090
+ );
6552
7091
  var keys = program.command("keys").description("your own API keys: list, create, revoke, restore (admin scope)");
6553
7092
  keys.command("list").action(
6554
7093
  run(async () => {
@@ -6869,6 +7408,48 @@ vendor.command("usage <id>").description("the agents and tasks that used this ve
6869
7408
  for (const t of tasks) console.log(` task ${t.session_id} ${t.title ?? "(untitled)"} ${t.last_used_at.slice(0, 10)}`);
6870
7409
  })
6871
7410
  );
7411
+ var mac = program.command("mac").description("your Macs: what each lends to agents now, and asking one for a folder, a browser window, its screen or its apps and files");
7412
+ mac.command("list").description("your Macs: online or not, what each can lend, and the folders, browser windows and screens lent now").option("--json", "print as JSON").action(
7413
+ run(async (opts) => {
7414
+ const macs = await listMacs();
7415
+ if (opts.json) {
7416
+ console.log(JSON.stringify(macs, null, 2));
7417
+ return;
7418
+ }
7419
+ if (!macs.length) console.log("no Macs connected; sign in to the AgentSky Mac app");
7420
+ for (const entry2 of macs) console.log(renderMac(entry2));
7421
+ })
7422
+ );
7423
+ mac.command("ask <machine> <kind>").description("ask a Mac to lend a folder, a browser window, its screen or its apps and files (host: local chats, call notes, uploads, saves, local git); the person answers on the Mac. Prints the MCP URL a session attaches once it is granted").requiredOption("--reason <text>", "why, in one sentence the person reads on their Mac").option("--no-wait", "return as soon as the Mac has the ask").option("--json", "print as JSON").action(
7424
+ run(async (machine, kind, opts) => {
7425
+ const asked = await askMac(machine, kind, opts.reason);
7426
+ const answer = opts.wait ? await waitForAnswer(asked.reach_request.id, 240) : asked.reach_request;
7427
+ if (opts.json) {
7428
+ console.log(JSON.stringify({ reach_request: answer, mcp_url: asked.mcp_url }, null, 2));
7429
+ return;
7430
+ }
7431
+ console.log(renderReachRequest(answer));
7432
+ console.log(`mcp_url: ${asked.mcp_url}`);
7433
+ if (answer.status === "granted") console.log("attach it as an MCP server: mcp_servers on POST /v1/sessions (MCP start_session: sources), or PATCH /v1/sessions/{id}");
7434
+ else if (!["pending", "asking"].includes(answer.status)) process.exitCode = 1;
7435
+ })
7436
+ );
7437
+ mac.command("ask-status <id>").description("where an ask stands: pending, asking, granted, denied, unavailable or expired").option("--wait <seconds>", "wait up to this long (at most 25) for the person to answer", "0").option("--json", "print as JSON").action(
7438
+ run(async (id, opts) => {
7439
+ const answer = await readReachRequest(id, Number(opts.wait) || 0);
7440
+ if (opts.json) {
7441
+ console.log(JSON.stringify(answer, null, 2));
7442
+ return;
7443
+ }
7444
+ console.log(renderReachRequest(answer));
7445
+ })
7446
+ );
7447
+ mac.command("take-back <mount>").description("end a folder, browser window, screen or apps and files lent from a Mac; every later call to it fails").action(
7448
+ run(async (mount) => {
7449
+ await takeBack(mount);
7450
+ console.log(`took back ${mount}`);
7451
+ })
7452
+ );
6872
7453
  var browserProfile = program.command("browser-profile").description("saved-login profiles: the sites your agents stay signed in to");
6873
7454
  browserProfile.command("list").description("your workspace's browser profiles: the sites your agents stay signed in to").option("--json", "print as JSON").action(
6874
7455
  run(async (opts) => {
@@ -6932,10 +7513,16 @@ notifications.command("get").description("show the current notification preferen
6932
7513
  console.log(JSON.stringify(prefs, null, 2));
6933
7514
  return;
6934
7515
  }
6935
- console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done}`);
7516
+ console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done} marketing=${prefs.email.marketing}`);
6936
7517
  console.log(`push: needs_you=${prefs.push.needs_you} done=${prefs.push.done}`);
6937
7518
  })
6938
7519
  );
7520
+ notifications.command("unsubscribe-marketing").description("stop AgentSky marketing email to your address; login and account email continue").action(
7521
+ run(async () => {
7522
+ const { marketing } = await unsubscribeMarketing();
7523
+ console.log(marketing ? "Marketing email is still on." : "Unsubscribed from AgentSky marketing email.");
7524
+ })
7525
+ );
6939
7526
  notifications.command("set").description("update notification preferences (pass at least one flag)").option("--email-needs-you <bool>", "email when the task needs your input: true or false").option("--email-done <bool>", "email when the task finishes: true or false").option("--push-needs-you <bool>", "push notification when the task needs your input: true or false").option("--push-done <bool>", "push notification when the task finishes: true or false").action(
6940
7527
  run(async (opts) => {
6941
7528
  const parseBool = (v) => v === void 0 ? void 0 : v === "true" ? true : v === "false" ? false : (() => {
@@ -7093,7 +7680,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
7093
7680
  }));
7094
7681
  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
7682
  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-NMBMU4GS.js");
7683
+ const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-ZRU624EY.js");
7097
7684
  if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
7098
7685
  const scopes = parseContentScopes(opts.capture);
7099
7686
  const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
@@ -7128,7 +7715,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
7128
7715
  }));
7129
7716
  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
7717
  try {
7131
- const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-NMBMU4GS.js");
7718
+ const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-ZRU624EY.js");
7132
7719
  const chunks = [];
7133
7720
  for await (const chunk of process.stdin) chunks.push(chunk);
7134
7721
  const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
@@ -7141,14 +7728,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
7141
7728
  }
7142
7729
  });
7143
7730
  traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
7144
- const { readState, flush, grantSender } = await import("./trace-NMBMU4GS.js");
7731
+ const { readState, flush, grantSender } = await import("./trace-ZRU624EY.js");
7145
7732
  const state = readState();
7146
7733
  if (!state) throw new Error("no trace is running here");
7147
7734
  const outcome = await flush(grantSender(state), { attempts: 4 });
7148
7735
  console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
7149
7736
  }));
7150
7737
  traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
7151
- const { readState, spoolStatus } = await import("./trace-NMBMU4GS.js");
7738
+ const { readState, spoolStatus } = await import("./trace-ZRU624EY.js");
7152
7739
  const state = readState();
7153
7740
  if (!state) {
7154
7741
  console.log("No trace is running here.");
@@ -7158,7 +7745,7 @@ traceCmd.command("status").description("the running trace and its spool").action
7158
7745
  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
7746
  }));
7160
7747
  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-NMBMU4GS.js");
7748
+ const { readState } = await import("./trace-ZRU624EY.js");
7162
7749
  const ref = id ?? readState()?.trace_id;
7163
7750
  if (!ref) throw new Error("name a trace, or start one here");
7164
7751
  const bundle = await api("GET", `/traces/${ref}`);
@@ -7185,7 +7772,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
7185
7772
  for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
7186
7773
  }));
7187
7774
  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-NMBMU4GS.js");
7775
+ const { readState, record, flush, grantSender, clearState } = await import("./trace-ZRU624EY.js");
7189
7776
  const state = readState();
7190
7777
  if (!state) throw new Error("no trace is running here");
7191
7778
  if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
@@ -7195,7 +7782,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
7195
7782
  console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
7196
7783
  }));
7197
7784
  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) => {
7198
- const lib = await import("./trace-library-OWYB75AR.js");
7785
+ const lib = await import("./trace-library-QOQUAL5H.js");
7199
7786
  if (opts.hook) {
7200
7787
  try {
7201
7788
  let raw = payload ?? "";
@@ -7254,10 +7841,10 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
7254
7841
  console.log(`Added ${imported.sessions} ${imported.agent} session${imported.sessions === 1 ? "" : "s"} from ${input}.`);
7255
7842
  }
7256
7843
  if (opts.installSchedule) {
7257
- const { join: join17 } = await import("path");
7258
- const { homedir: homedir8 } = await import("os");
7844
+ const { join: join19 } = await import("path");
7845
+ const { homedir: homedir10 } = await import("os");
7259
7846
  const { execFileSync } = await import("child_process");
7260
- const log = join17(homedir8(), ".config", "sky", "trace-sync.log");
7847
+ const log = join19(homedir10(), ".config", "sky", "trace-sync.log");
7261
7848
  const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
7262
7849
  try {
7263
7850
  execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
@@ -7267,11 +7854,11 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
7267
7854
  console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
7268
7855
  }
7269
7856
  if (opts.status) {
7270
- const { readFileSync: readFileSync17 } = await import("fs");
7271
- const { join: join17 } = await import("path");
7857
+ const { readFileSync: readFileSync18 } = await import("fs");
7858
+ const { join: join19 } = await import("path");
7272
7859
  let state = { sessions: {} };
7273
7860
  try {
7274
- state = JSON.parse(readFileSync17(join17(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
7861
+ state = JSON.parse(readFileSync18(join19(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
7275
7862
  } catch {
7276
7863
  }
7277
7864
  const counts = /* @__PURE__ */ new Map();
@@ -7304,7 +7891,7 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
7304
7891
  })();
7305
7892
  });
7306
7893
  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) => {
7307
- const { libraryRequest } = await import("./trace-library-OWYB75AR.js");
7894
+ const { libraryRequest } = await import("./trace-library-QOQUAL5H.js");
7308
7895
  const workspace2 = opts.workspace ?? currentUniverse();
7309
7896
  if (!workspace2) throw new Error("name a workspace with --workspace");
7310
7897
  const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
@@ -7321,7 +7908,7 @@ traceCmd.command("search <query...>").description("search every session in a wor
7321
7908
  if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
7322
7909
  }));
7323
7910
  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) => {
7324
- const { libraryRequest } = await import("./trace-library-OWYB75AR.js");
7911
+ const { libraryRequest } = await import("./trace-library-QOQUAL5H.js");
7325
7912
  const workspace2 = opts.workspace ?? currentUniverse();
7326
7913
  if (!workspace2) throw new Error("name a workspace with --workspace");
7327
7914
  const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
@@ -7437,6 +8024,140 @@ thread.command("list").description("your workspace threads, most recently update
7437
8024
  for (const t of threads) console.log(renderThreadSummary(t));
7438
8025
  })
7439
8026
  );
8027
+ thread.command("start <text>").description("start a thread and send it its first message, as the web composer does").requiredOption("--agent <type>", "the agent's harness type (e.g. claude_code, codex)").option("--llm <model>", "the model (the harness default when omitted)").option("--effort <level>", "thinking level").option("--vs <type:model...>", "more agents that get the same message, side by side", (val, acc) => [...acc, val], []).option("--saved-agent <id>", "run on one of your saved agents").option("--workspace <slug>", "start it in this team workspace").option("--plan", "send the first message in Plan mode").option("--json", "print the new thread as JSON").action(
8028
+ run(async (text, opts) => {
8029
+ const compared = (opts.vs ?? []).map((v) => {
8030
+ const [agentType, llm] = v.split(":");
8031
+ if (!agentType) throw new ApiError(400, "invalid_request", `--vs must be type[:model], got "${v}"`);
8032
+ return { agentType, ...llm ? { llm } : {} };
8033
+ });
8034
+ const result = await startThread({
8035
+ requestId: `sky-thread-${crypto.randomUUID()}`,
8036
+ agentType: opts.agent,
8037
+ text,
8038
+ ...opts.llm ? { llm: opts.llm } : {},
8039
+ ...opts.effort ? { reasoningEffort: opts.effort } : {},
8040
+ ...compared.length ? { compare: compared } : {},
8041
+ ...opts.savedAgent ? { agentId: opts.savedAgent } : {},
8042
+ ...opts.workspace ? { workspace: opts.workspace } : {},
8043
+ ...opts.plan ? { interactionMode: "plan" } : {}
8044
+ });
8045
+ if (opts.json) {
8046
+ console.log(JSON.stringify(result, null, 2));
8047
+ return;
8048
+ }
8049
+ console.log(`started ${result.playgroundId}`);
8050
+ for (const col of result.columns) console.log(` agent ${col.columnId} session ${col.sessionId}`);
8051
+ })
8052
+ );
8053
+ thread.command("show <id>").description("one thread with every agent and the session each one runs in").option("--json", "print as JSON").action(
8054
+ run(async (id, opts) => {
8055
+ const detail = await getThread(id);
8056
+ if (opts.json) {
8057
+ console.log(JSON.stringify(detail, null, 2));
8058
+ return;
8059
+ }
8060
+ console.log(`thread ${detail.id} ${detail.title}`);
8061
+ for (const col of detail.columns) console.log(` agent ${col.id} session ${col.sessionId} ${col.blind ? "(hidden until the vote)" : `${col.agentType}/${col.llm}`} ${col.status ?? col.podStatus ?? ""}`);
8062
+ })
8063
+ );
8064
+ thread.command("rename <id> <title>").description("rename a thread").action(run(async (id, title) => {
8065
+ await organizeThread(id, { title });
8066
+ console.log(`renamed ${id}`);
8067
+ }));
8068
+ for (const [verb, change, description] of [
8069
+ ["pin", { pinned: true }, "pin a thread to the top of the rail"],
8070
+ ["unpin", { pinned: false }, "unpin a thread"],
8071
+ ["archive", { archived: true }, "put a thread away"],
8072
+ ["unarchive", { archived: false }, "bring an archived thread back"],
8073
+ ["mark-read", { seen: true }, "mark a thread read"],
8074
+ ["mark-unread", { seen: false }, "mark a thread unread"]
8075
+ ]) {
8076
+ thread.command(`${verb} <id>`).description(description).action(run(async (id) => {
8077
+ await organizeThread(id, change);
8078
+ console.log(`${verb}: ${id}`);
8079
+ }));
8080
+ }
8081
+ thread.command("delete <id>").description("delete a thread with its sessions").action(run(async (id) => {
8082
+ await deleteThread(id);
8083
+ console.log(`deleted ${id}`);
8084
+ }));
8085
+ thread.command("send <id> <text>").description("send one message to every agent of the thread, or to the ones --agent names").option("--agent <columnId...>", "only these agents (column ids from `sky thread show`)").option("--json", "print each agent's outcome as JSON").action(
8086
+ run(async (id, text, opts) => {
8087
+ const { results } = await sendThreadMessage(id, { text, deliveryId: crypto.randomUUID(), ...opts.agent?.length ? { columnIds: opts.agent } : {} });
8088
+ if (opts.json) {
8089
+ console.log(JSON.stringify(results, null, 2));
8090
+ return;
8091
+ }
8092
+ for (const [column, outcome] of Object.entries(results)) console.log(` ${column} ${outcome.ok ? "sent" : `not sent: ${outcome.error ?? "refused"}`}`);
8093
+ })
8094
+ );
8095
+ thread.command("add-agent <id>").description("add an agent to the thread, in a session of its own").requiredOption("--agent <type>", "the agent's harness type").option("--llm <model>", "the model").option("--effort <level>", "thinking level").action(
8096
+ run(async (id, opts) => {
8097
+ const { columns } = await addThreadAgent(id, { requestId: `sky-agent-${crypto.randomUUID()}`, agentType: opts.agent, ...opts.llm ? { llm: opts.llm } : {}, ...opts.effort ? { reasoningEffort: opts.effort } : {} });
8098
+ for (const col of columns) console.log(`added agent ${col.columnId} session ${col.sessionId}`);
8099
+ })
8100
+ );
8101
+ thread.command("remove-agent <id> <columnId>").description("take one agent out of the thread; its session is left as it is").action(run(async (id, columnId) => {
8102
+ await removeThreadAgent(id, columnId);
8103
+ console.log(`removed ${columnId}`);
8104
+ }));
8105
+ thread.command("stop <id>").description("stop what the thread's agents are doing").option("--agent <columnId>", "stop only this agent").action(run(async (id, opts) => {
8106
+ await interruptThread(id, opts.agent);
8107
+ console.log(`stopped ${id}`);
8108
+ }));
8109
+ thread.command("fork <id>").description("continue one agent's conversation in a thread of its own").option("--agent <columnId>", "the agent to fork (the first when omitted)").option("--at <messageId>", "carry the conversation only up to this message").option("--to <type:model>", "continue on another agent or model").action(
8110
+ run(async (id, opts) => {
8111
+ const [agentType, llm] = (opts.to ?? "").split(":");
8112
+ const { playgroundId } = await forkThread(id, {
8113
+ requestId: `sky-fork-${crypto.randomUUID()}`,
8114
+ ...opts.agent ? { columnId: opts.agent } : {},
8115
+ ...opts.at ? { messageId: opts.at } : {},
8116
+ ...agentType ? { target: { agentType, ...llm ? { llm } : {} } } : {}
8117
+ });
8118
+ console.log(`forked into ${playgroundId}`);
8119
+ })
8120
+ );
8121
+ thread.command("model <id>").description("change the model or thinking level one agent of the thread runs on, from its next turn").option("--agent <columnId>", "the agent (the first when omitted)").option("--llm <model>", "the new model").option("--effort <level>", 'the new thinking level, or "default"').action(
8122
+ run(async (id, opts) => {
8123
+ if (!opts.llm && !opts.effort) throw new ApiError(400, "invalid_request", "Provide --llm or --effort.");
8124
+ const result = await changeThreadModel(id, { ...opts.llm ? { llm: opts.llm } : {}, ...opts.effort ? { reasoningEffort: opts.effort === "default" ? null : opts.effort } : {} }, opts.agent);
8125
+ console.log(`now ${result.llm}${result.reasoningEffort ? ` (${result.reasoningEffort})` : ""}`);
8126
+ })
8127
+ );
8128
+ thread.command("search <query>").description("search the words of your conversations").option("--archived", "search archived threads only").option("--all", "search active and archived threads").option("--cursor <cursor>", "the next page").action(
8129
+ run(async (query, opts) => {
8130
+ const threads = opts.all ? "all" : opts.archived ? "archived" : void 0;
8131
+ console.log(JSON.stringify(await searchThreads(query, { ...threads ? { threads } : {}, ...opts.cursor ? { cursor: opts.cursor } : {} }), null, 2));
8132
+ })
8133
+ );
8134
+ thread.command("options").description("what a new thread can start on: agents, models, thinking levels and grantable tools").action(run(async () => {
8135
+ console.log(JSON.stringify(await threadOptions(), null, 2));
8136
+ }));
8137
+ thread.command("join <sessionId>").description("open a teammate's thread, or one shared with you, as the session link does").action(
8138
+ run(async (sessionId) => {
8139
+ const opened = await joinThread(sessionId);
8140
+ console.log(opened.readOnly ? `read-only: session ${opened.sessionId}` : `opened ${opened.playgroundId}`);
8141
+ })
8142
+ );
8143
+ var project = program.command("project").description("the GitHub repositories a new thread can start on");
8144
+ project.command("list").description("repositories a new thread can start on, and the one you chose last").option("--json", "print as JSON").action(
8145
+ run(async (opts) => {
8146
+ const listed = await listProjects();
8147
+ if (opts.json) {
8148
+ console.log(JSON.stringify(listed, null, 2));
8149
+ return;
8150
+ }
8151
+ if (listed.unavailable) console.log(listed.unavailable.message);
8152
+ for (const repo of listed.repos) console.log(`${repo.fullName === listed.last?.repoFullName ? "* " : " "}${repo.fullName}`);
8153
+ })
8154
+ );
8155
+ project.command("use [repo]").description("remember a repository (owner/name) for your next new thread, or none when omitted").option("--branch <branch>", "the branch", "main").action(
8156
+ run(async (repo, opts) => {
8157
+ const { project: remembered } = await rememberProject(repo ? { repoFullName: repo, branch: opts.branch } : null);
8158
+ console.log(remembered ? `next thread starts on ${remembered.repoFullName}@${remembered.branch}` : "next thread starts on no repository");
8159
+ })
8160
+ );
7440
8161
  session.command("feedback <id>").description("read the thumbs-up/down feedback on this session's messages").option("--json", "print as JSON").action(
7441
8162
  run(async (id, opts) => {
7442
8163
  const result = await api(