@agentsky/sky 2.22.0 → 2.24.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
@@ -4,12 +4,13 @@ import {
4
4
  api,
5
5
  apiBytes,
6
6
  apiForm,
7
+ apiRaw,
7
8
  downloadFileTo,
8
9
  postEvents,
9
10
  resolveSessionRef,
10
11
  streamEvents,
11
12
  uploadFile
12
- } from "./chunk-EBH3KXSU.js";
13
+ } from "./chunk-UHMKEYJN.js";
13
14
  import {
14
15
  CLI_VERSION,
15
16
  USER_AGENT,
@@ -20,7 +21,7 @@ import {
20
21
  currentUniverse,
21
22
  loadConfig,
22
23
  saveConfig
23
- } from "./chunk-XT3W75V3.js";
24
+ } from "./chunk-RAFHOB5S.js";
24
25
  import {
25
26
  AGENT_TYPES,
26
27
  MODEL_SUBSCRIPTION_PLAN_PHRASE,
@@ -42,17 +43,17 @@ import {
42
43
  skillRefSchema,
43
44
  splitMcpServers,
44
45
  supersededBy
45
- } from "./chunk-YMYQT6KR.js";
46
+ } from "./chunk-277AZSSG.js";
46
47
  import "./chunk-MLKGABMK.js";
47
48
 
48
49
  // src/index.ts
49
- import { existsSync as existsSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
50
+ import { existsSync as existsSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
50
51
  import { basename as basename9, resolve as resolve9 } from "path";
51
52
  import { createInterface as createInterface6 } from "readline";
52
53
  import { Command } from "commander";
53
54
 
54
55
  // src/files.ts
55
- import { readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
56
+ import { existsSync, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
56
57
  import { basename as basename2, extname, join as join2 } from "path";
57
58
 
58
59
  // src/resumable-upload.ts
@@ -250,6 +251,16 @@ function eventsFor(text, files2, system, interactionMode2) {
250
251
  if (system?.trim()) events.push({ type: "system.message", content: [{ type: "text", text: system }] });
251
252
  return events;
252
253
  }
254
+ function isFileId(ref) {
255
+ return /^file_[A-Za-z0-9]+$/.test(ref);
256
+ }
257
+ async function attachment(ref, opts = {}) {
258
+ if (opts.byId || isFileId(ref) && !existsSync(ref)) {
259
+ if (!isFileId(ref)) throw new Error(`--file-id takes a stored file's id (file_\u2026), got ${JSON.stringify(ref)}`);
260
+ return { file: await api("GET", `/files/${encodeURIComponent(ref)}`), uploaded: false };
261
+ }
262
+ return { file: await uploadLocalFile(ref), uploaded: true };
263
+ }
253
264
 
254
265
  // src/goal.ts
255
266
  var CHECK_SEPARATOR = " :: ";
@@ -410,6 +421,52 @@ function renderApiKey(key) {
410
421
  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
422
  }
412
423
 
424
+ // src/account-access.ts
425
+ function linkToken(ref) {
426
+ const fromUrl = /\/s\/([A-Za-z0-9_-]+)\/?(?:[?#].*)?$/.exec(ref.trim());
427
+ const token = fromUrl ? fromUrl[1] : ref.trim();
428
+ 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");
429
+ return token;
430
+ }
431
+ async function sendViaLink(ref, text, idempotencyKey) {
432
+ await api("POST", `/links/${encodeURIComponent(linkToken(ref))}/messages`, { text }, { idempotencyKey });
433
+ }
434
+ async function stopViaLink(ref) {
435
+ await api("POST", `/links/${encodeURIComponent(linkToken(ref))}/interrupt`);
436
+ }
437
+ function inviteCode(ref) {
438
+ const fromUrl = /\/i\/([^/?#]+)\/?(?:[?#].*)?$/.exec(ref.trim());
439
+ const code = fromUrl ? decodeURIComponent(fromUrl[1]) : ref.trim();
440
+ if (!code) throw new ApiError(400, "invalid_request", "a credit invite is an /i/<code> URL or its code");
441
+ return code;
442
+ }
443
+ async function claimCreditInvite(ref) {
444
+ return api("POST", "/billing/credit-invites/claim", { code: inviteCode(ref) });
445
+ }
446
+ function renderCreditInviteClaim(claim) {
447
+ const amount = `$${claim.amount_usd.toFixed(2)}`;
448
+ return claim.status === "granted" ? `Added ${amount} of credit to your balance` : `Already claimed: this link added ${amount} to your balance`;
449
+ }
450
+ async function listAppGrants() {
451
+ return (await api("GET", "/oauth_grants")).oauth_grants;
452
+ }
453
+ async function revokeAppGrant(clientId) {
454
+ await api("DELETE", `/oauth_grants/${encodeURIComponent(clientId)}`);
455
+ }
456
+ function renderAppGrant(grant) {
457
+ const since = grant.allowed_at ? ` since ${grant.allowed_at.slice(0, 10)}` : "";
458
+ const review = grant.review_status === "approved" ? "" : " (unreviewed)";
459
+ return `${grant.client_id} ${grant.name || "(unnamed)"} ${grant.scopes.join(" ")}${since}${review}`;
460
+ }
461
+ async function unsubscribeMarketing() {
462
+ const { notification_preferences } = await api(
463
+ "PATCH",
464
+ "/notification-preferences",
465
+ { email: { marketing: false } }
466
+ );
467
+ return { marketing: notification_preferences.email.marketing };
468
+ }
469
+
413
470
  // src/context.ts
414
471
  function memoryBody(operation, version) {
415
472
  return { ...operation, expectedVersion: version };
@@ -448,9 +505,9 @@ function renderContextPage(page) {
448
505
  }
449
506
 
450
507
  // src/agent-file.ts
451
- import { existsSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
508
+ import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
452
509
  import { dirname as dirname2, resolve as resolve2 } from "path";
453
- import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
510
+ import { parse as parseToml } from "smol-toml";
454
511
  var fileInstructionSchema = external_exports.object({
455
512
  name: external_exports.string(),
456
513
  content: external_exports.string().optional(),
@@ -466,7 +523,8 @@ var agentFileSchema = external_exports.object({
466
523
  type: external_exports.enum(AGENT_TYPES).optional(),
467
524
  llm: external_exports.string().optional(),
468
525
  reasoning_effort: external_exports.enum(REASONING_EFFORT_VALUES).optional(),
469
- capabilities: capabilitiesSchema.optional(),
526
+ capabilities: capabilitiesSchema.removeDefault().optional(),
527
+ // omitted = leave the agent's as they are
470
528
  prompt: external_exports.string().optional(),
471
529
  prompt_file: external_exports.string().optional(),
472
530
  instructions: external_exports.array(fileInstructionSchema).optional(),
@@ -523,13 +581,40 @@ function readAgentFile(path) {
523
581
  return { file, spec };
524
582
  }
525
583
  function stampId(path, id) {
526
- const doc = parseToml(readFileSync3(path, "utf8"));
527
- doc.id = id;
528
- delete doc.slug;
529
- writeFileSync2(path, stringifyToml(doc));
584
+ writeFileSync2(path, stampIdText(readFileSync3(path, "utf8"), id));
585
+ }
586
+ function stampIdText(text, id) {
587
+ const out = [];
588
+ let topLevel = true;
589
+ let openString = null;
590
+ let stamped = false;
591
+ let nameAt = -1;
592
+ for (const line of text.split("\n")) {
593
+ if (openString) {
594
+ if (line.includes(openString)) openString = null;
595
+ out.push(line);
596
+ continue;
597
+ }
598
+ if (/^\s*\[/.test(line)) topLevel = false;
599
+ if (topLevel && /^\s*(id|slug)\s*=/.test(line)) {
600
+ if (/^\s*id\s*=/.test(line) && !stamped) {
601
+ out.push(`id = ${JSON.stringify(id)}`);
602
+ stamped = true;
603
+ }
604
+ continue;
605
+ }
606
+ if (topLevel && nameAt < 0 && /^\s*name\s*=/.test(line)) nameAt = out.length;
607
+ const opener = /=\s*("""|''')/.exec(line);
608
+ if (opener && !line.slice(opener.index + opener[0].length).includes(opener[1])) openString = opener[1];
609
+ out.push(line);
610
+ }
611
+ if (!stamped) out.splice(nameAt + 1, 0, `id = ${JSON.stringify(id)}`);
612
+ const next = out.join("\n");
613
+ if (parseToml(next).id !== id) throw new Error("could not stamp the agent id into the file");
614
+ return next;
530
615
  }
531
616
  function scaffoldAgentFile(path, name) {
532
- if (existsSync(path)) throw new Error(`${path} already exists`);
617
+ if (existsSync2(path)) throw new Error(`${path} already exists`);
533
618
  writeFileSync2(
534
619
  path,
535
620
  `# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
@@ -550,6 +635,38 @@ You are ${name}, a helpful long-lived agent.
550
635
  `
551
636
  );
552
637
  }
638
+ function deployPatch(spec) {
639
+ return {
640
+ displayName: spec.displayName,
641
+ ...spec.agentType ? { agentType: spec.agentType } : {},
642
+ ...spec.llm ? { llm: spec.llm } : {},
643
+ ...spec.capabilities ? { capabilities: spec.capabilities } : {},
644
+ ...spec.reasoningEffort ? { reasoningEffort: spec.reasoningEffort } : {},
645
+ ...spec.skills ? { skills: spec.skills } : {},
646
+ ...spec.hiringPolicy ? { hiringPolicy: spec.hiringPolicy } : {},
647
+ ...spec.environmentId ? { environmentId: spec.environmentId } : {}
648
+ };
649
+ }
650
+ function deployReport(id, spec, before, after, promptChanged) {
651
+ if (spec.agentType && after.agentType !== spec.agentType) {
652
+ throw new Error(`${id} is still ${after.agentType}: the API did not apply type = "${spec.agentType}"`);
653
+ }
654
+ if (spec.llm && after.llm !== spec.llm) {
655
+ throw new Error(`${id} still runs ${after.llm}: the API did not apply llm = "${spec.llm}"`);
656
+ }
657
+ const changed = [];
658
+ if (promptChanged) changed.push("prompt");
659
+ if (after.version !== before.version) changed.push("settings");
660
+ if (!changed.length) return [`${id} is up to date \u2014 nothing to deploy (version ${after.version})`];
661
+ const lines = [`Deployed ${id}: ${changed.join(" and ")} updated (version ${before.version} \u2192 ${after.version})`];
662
+ if (after.agentType !== before.agentType || after.llm !== before.llm) {
663
+ lines.push(` now ${after.agentType} \xB7 ${after.llm} for new sessions (was ${before.agentType} \xB7 ${before.llm})`);
664
+ }
665
+ if (spec.skills && after.version !== before.version) {
666
+ lines.push(` Skills apply to new sessions; running sessions keep the skills they started with \u2014 start one with \`sky session create ${id}\`.`);
667
+ }
668
+ return lines;
669
+ }
553
670
 
554
671
  // src/catalog.ts
555
672
  var bare = (handle) => handle.replace(/^@/, "");
@@ -616,7 +733,7 @@ function renderCatalogAgent(agent2) {
616
733
 
617
734
  // src/clone.ts
618
735
  import { spawn } from "child_process";
619
- import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
736
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
620
737
  import { homedir } from "os";
621
738
  import { basename as basename3, join as join3 } from "path";
622
739
  import { createInterface } from "readline";
@@ -682,9 +799,9 @@ function readClaudeCode() {
682
799
  sources.push("global CLAUDE.md");
683
800
  }
684
801
  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 });
802
+ const project2 = readIfExists(join3(process.cwd(), name));
803
+ if (project2?.trim()) {
804
+ parts.push({ label: `Project ${name}`, content: project2 });
688
805
  sources.push(`project ${name}`);
689
806
  break;
690
807
  }
@@ -722,9 +839,9 @@ function detectHarness(explicit) {
722
839
  }
723
840
  return explicit;
724
841
  }
725
- if (existsSync2(claudeConfigRoot())) return "claude_code";
726
- if (existsSync2(join3(homedir(), ".codex"))) return "codex";
727
- if (existsSync2(join3(homedir(), ".openclaw"))) return "openclaw";
842
+ if (existsSync3(claudeConfigRoot())) return "claude_code";
843
+ if (existsSync3(join3(homedir(), ".codex"))) return "codex";
844
+ if (existsSync3(join3(homedir(), ".openclaw"))) return "openclaw";
728
845
  throw new Error(
729
846
  "no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
730
847
  );
@@ -739,11 +856,11 @@ function linkPath(configRoot) {
739
856
  return join3(configRoot, "agentsky-link.json");
740
857
  }
741
858
  function readLink(configRoot) {
742
- const link = readJson(linkPath(configRoot));
743
- return link?.agentId ? link : void 0;
859
+ const link2 = readJson(linkPath(configRoot));
860
+ return link2?.agentId ? link2 : void 0;
744
861
  }
745
- function writeLink(configRoot, link) {
746
- writeFileSync3(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
862
+ function writeLink(configRoot, link2) {
863
+ writeFileSync3(linkPath(configRoot), `${JSON.stringify(link2, null, 2)}
747
864
  `);
748
865
  }
749
866
  var DEFAULT_VAULT_NAME = "default";
@@ -909,14 +1026,14 @@ async function createClone(local, opts) {
909
1026
  await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
910
1027
  if (opts.open) openBrowser(url);
911
1028
  }
912
- async function syncClone(local, link, opts) {
913
- const since = link.lastSyncedAt ?? link.clonedAt;
1029
+ async function syncClone(local, link2, opts) {
1030
+ const since = link2.lastSyncedAt ?? link2.clonedAt;
914
1031
  console.log(
915
- `Syncing ${link.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
1032
+ `Syncing ${link2.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
916
1033
  );
917
1034
  if (opts.dryRun) {
918
1035
  console.log(
919
- ` would push prompt (${kb(local.prompt)}) to ${link.agentId}. Dry run \u2014 nothing sent.`
1036
+ ` would push prompt (${kb(local.prompt)}) to ${link2.agentId}. Dry run \u2014 nothing sent.`
920
1037
  );
921
1038
  return;
922
1039
  }
@@ -924,24 +1041,24 @@ async function syncClone(local, link, opts) {
924
1041
  try {
925
1042
  ({ agent: agent2 } = await api(
926
1043
  "GET",
927
- `/agents/${encodeURIComponent(link.agentId)}`
1044
+ `/agents/${encodeURIComponent(link2.agentId)}`
928
1045
  ));
929
1046
  } catch (error) {
930
1047
  if (error instanceof ApiError && error.status === 404) {
931
1048
  throw new Error(
932
- `linked agent ${link.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
1049
+ `linked agent ${link2.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
933
1050
  );
934
1051
  }
935
1052
  throw error;
936
1053
  }
937
1054
  if (agent2.agentType !== local.harness) {
938
1055
  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`
1056
+ `linked agent ${link2.agentId} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
940
1057
  );
941
1058
  }
942
1059
  const result = await api(
943
1060
  "PUT",
944
- `/agents/${encodeURIComponent(link.agentId)}/prompt`,
1061
+ `/agents/${encodeURIComponent(link2.agentId)}/prompt`,
945
1062
  { prompt: local.prompt }
946
1063
  );
947
1064
  console.log(
@@ -949,11 +1066,11 @@ async function syncClone(local, link, opts) {
949
1066
  );
950
1067
  const drift = [];
951
1068
  const localModel = local.model ?? null;
952
- const linkedModel = link.llm ?? agent2.llm;
1069
+ const linkedModel = link2.llm ?? agent2.llm;
953
1070
  if (localModel && linkedModel && localModel !== linkedModel) {
954
1071
  drift.push(`model changed ${linkedModel} \u2192 ${localModel}`);
955
1072
  }
956
- const before = new Set(link.mcpServers ?? []);
1073
+ const before = new Set(link2.mcpServers ?? []);
957
1074
  const nowKept = Object.keys(local.keptServers);
958
1075
  const newServers = nowKept.filter((n) => !before.has(n));
959
1076
  const goneServers = [...before].filter((n) => !nowKept.includes(n));
@@ -968,39 +1085,39 @@ async function syncClone(local, link, opts) {
968
1085
  ` left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
969
1086
  );
970
1087
  }
971
- writeLink(local.configRoot, { ...link, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
972
- console.log(`Synced ${link.agentId}.`);
1088
+ writeLink(local.configRoot, { ...link2, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
1089
+ console.log(`Synced ${link2.agentId}.`);
973
1090
  await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
974
- if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentId}`);
1091
+ if (opts.open) openBrowser(`${apiBase()}/agents/${link2.agentId}`);
975
1092
  }
976
1093
  async function clone(opts) {
977
1094
  const harness2 = detectHarness(opts.harness);
978
1095
  const local = readLocal(harness2);
979
- const link = opts.new ? void 0 : readLink(local.configRoot);
980
- if (link) await syncClone(local, link, opts);
1096
+ const link2 = opts.new ? void 0 : readLink(local.configRoot);
1097
+ if (link2) await syncClone(local, link2, opts);
981
1098
  else await createClone(local, opts);
982
1099
  }
983
1100
 
984
1101
  // src/import-local.ts
985
- import { closeSync as closeSync3, existsSync as existsSync5, openSync as openSync3, readdirSync as readdirSync4, readFileSync as readFileSync8, readSync as readSync3, statSync as statSync5 } from "fs";
1102
+ import { closeSync as closeSync3, existsSync as existsSync6, openSync as openSync3, readdirSync as readdirSync4, readFileSync as readFileSync8, readSync as readSync3, statSync as statSync5 } from "fs";
986
1103
  import { homedir as homedir4 } from "os";
987
1104
  import { join as join7 } from "path";
988
1105
  import { createHash as createHash3 } from "crypto";
989
1106
 
990
1107
  // src/import-files.ts
991
1108
  import { isUtf8 } from "buffer";
992
- import { existsSync as existsSync4, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
1109
+ import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
993
1110
  import { homedir as homedir3 } from "os";
994
1111
  import { dirname as dirname4, extname as extname2, isAbsolute, join as join5, relative as relative2, resolve as resolve4, sep as sep2, basename as basename5 } from "path";
995
1112
 
996
1113
  // src/import-workspace.ts
997
1114
  import { spawnSync } from "child_process";
998
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
1115
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
999
1116
  import { homedir as homedir2 } from "os";
1000
1117
  import { basename as basename4, dirname as dirname3, join as join4, relative, sep } from "path";
1001
1118
  import { parse as parseToml2 } from "smol-toml";
1002
1119
  var MAX_TREE = 5e3;
1003
- var SKILL_LINE = /Base directory for this skill: (\/[^\s"'\\<>]+)/g;
1120
+ var SKILL_LINE = /Base directory for this skill: (\/[^\n\r"'\\<>]+)/g;
1004
1121
  function git(cwd, ...args) {
1005
1122
  const run2 = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 1e4, maxBuffer: 32 * 1024 * 1024 });
1006
1123
  return run2.status === 0 ? run2.stdout : null;
@@ -1046,8 +1163,7 @@ function skillRoots(cwd, home) {
1046
1163
  ];
1047
1164
  }
1048
1165
  function pluginSkill(plugin, skill, home) {
1049
- const base = join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "plugins");
1050
- const walk = (dir, depth) => {
1166
+ const walk = (dir, depth, underPlugin) => {
1051
1167
  if (depth > 7) return null;
1052
1168
  let entries;
1053
1169
  try {
@@ -1058,13 +1174,13 @@ function pluginSkill(plugin, skill, home) {
1058
1174
  for (const entry2 of entries) {
1059
1175
  if (!entry2.isDirectory() || entry2.name === "node_modules" || entry2.name === ".git") continue;
1060
1176
  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);
1177
+ if (entry2.name === skill && basename4(dir) === "skills" && (underPlugin || path.includes(`${sep}${plugin}${sep}`)) && isFile(join4(path, "SKILL.md"))) return realpathSync(path);
1178
+ const found = walk(path, depth + 1, underPlugin);
1063
1179
  if (found) return found;
1064
1180
  }
1065
1181
  return null;
1066
1182
  };
1067
- return walk(base, 0);
1183
+ 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
1184
  }
1069
1185
  function findSkill(name, cwd, home) {
1070
1186
  const [scope, skill] = name.includes(":") ? name.split(":", 2) : [null, name];
@@ -1084,23 +1200,24 @@ function loadedSkills(records, touched, cwd, home = homedir2()) {
1084
1200
  if (![...dirs.values()].includes(real)) dirs.set(name, real);
1085
1201
  return true;
1086
1202
  };
1203
+ const called = [];
1087
1204
  for (const record of records) {
1088
1205
  const texts = [];
1089
1206
  strings(record, texts);
1090
- for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].replace(/[.,;:)]+$/, ""));
1207
+ for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].trim().replace(/[.,;:)]+$/, ""));
1091
1208
  const content = record?.message?.content;
1092
1209
  if (!Array.isArray(content)) continue;
1093
1210
  for (const block of content) {
1094
1211
  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);
1212
+ if (b?.type === "tool_use" && b.name === "Skill" && typeof b.input?.skill === "string") called.push(b.input.skill.replace(/^\//, ""));
1100
1213
  }
1101
1214
  }
1102
1215
  for (const path of touched) if (basename4(path) === "SKILL.md") add(dirname3(path));
1103
- for (const name of dirs.keys()) missing.delete(name);
1216
+ for (const name of called) {
1217
+ if (!name || dirs.has(name) || [...dirs.keys()].some((key) => key.endsWith(`:${name}`) || name.endsWith(`:${key}`))) continue;
1218
+ const dir = findSkill(name, cwd, home);
1219
+ if (!dir || !add(dir, name)) missing.add(name);
1220
+ }
1104
1221
  return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
1105
1222
  }
1106
1223
  function readJson2(path) {
@@ -1224,7 +1341,7 @@ function skillFiles(skill) {
1224
1341
  else if (entry2.isFile()) out.push({ path, name: `skills/${skill.name}/${relative(skill.dir, path).split(sep).join("/")}` });
1225
1342
  }
1226
1343
  };
1227
- if (existsSync3(skill.dir)) walk(skill.dir, 0);
1344
+ if (existsSync4(skill.dir)) walk(skill.dir, 0);
1228
1345
  return out;
1229
1346
  }
1230
1347
  function codexThreadName(sessionId, home = homedir2()) {
@@ -1350,13 +1467,13 @@ function touchedPaths(records, cwd, home = homedir3()) {
1350
1467
  }
1351
1468
  function gitRoot(dir) {
1352
1469
  for (let d = dir; ; d = dirname4(d)) {
1353
- if (existsSync4(join5(d, ".git"))) return d;
1470
+ if (existsSync5(join5(d, ".git"))) return d;
1354
1471
  if (d === dirname4(d)) return null;
1355
1472
  }
1356
1473
  }
1357
1474
  function instructionPaths(cwd, harness2, home = homedir3()) {
1358
1475
  const out = [];
1359
- if (cwd && existsSync4(cwd)) {
1476
+ if (cwd && existsSync5(cwd)) {
1360
1477
  const root = gitRoot(cwd) ?? cwd;
1361
1478
  for (let d = resolve4(cwd); ; d = dirname4(d)) {
1362
1479
  for (const name of INSTRUCTION_FILES) out.push(join5(d, name));
@@ -1879,7 +1996,7 @@ function sniffTitle(filePath) {
1879
1996
  function listClaudeCodeSessions(limit) {
1880
1997
  const root = process.env.CLAUDE_CONFIG_DIR ?? join7(homedir4(), ".claude");
1881
1998
  const projectsDir = join7(root, "projects");
1882
- if (!existsSync5(projectsDir)) return [];
1999
+ if (!existsSync6(projectsDir)) return [];
1883
2000
  const sessions = [];
1884
2001
  try {
1885
2002
  for (const encoded of readdirSync4(projectsDir)) {
@@ -1905,7 +2022,7 @@ function listClaudeCodeSessions(limit) {
1905
2022
  let discoveryError;
1906
2023
  const subagentFiles = [];
1907
2024
  const subagentDir = join7(projectDir, sessionId, "subagents");
1908
- if (existsSync5(subagentDir)) {
2025
+ if (existsSync6(subagentDir)) {
1909
2026
  try {
1910
2027
  const subFiles = readdirSync4(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
1911
2028
  for (const f of subFiles) {
@@ -2183,7 +2300,7 @@ async function importSession(opts) {
2183
2300
  }
2184
2301
  const result = await response.json();
2185
2302
  const finalTraceId = result.traceId ?? traceId;
2186
- const environment = opts.files === false || opts.environment === false || !cwd || !existsSync5(cwd) ? null : await uploadEnvironment(base, key, finalTraceId, cwd, resourceLines, opts.onProgress).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
2303
+ const environment = opts.files === false || opts.environment === false || !cwd || !existsSync6(cwd) ? null : await uploadEnvironment(base, key, finalTraceId, cwd, resourceLines, opts.onProgress).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
2187
2304
  return {
2188
2305
  traceId: finalTraceId,
2189
2306
  url: `${base}/playground/new?import=${encodeURIComponent(finalTraceId)}`,
@@ -2296,7 +2413,7 @@ import { hostname } from "os";
2296
2413
  import { spawn as spawn2 } from "child_process";
2297
2414
 
2298
2415
  // src/mcp-register.ts
2299
- import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
2416
+ import { existsSync as existsSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
2300
2417
  import { spawnSync as spawnSync3 } from "child_process";
2301
2418
  import { homedir as homedir6 } from "os";
2302
2419
  import { join as join10 } from "path";
@@ -2304,7 +2421,7 @@ import { parse as parseToml3 } from "smol-toml";
2304
2421
  import { isMap, parseDocument } from "yaml";
2305
2422
 
2306
2423
  // src/local-agents.ts
2307
- import { existsSync as existsSync6 } from "fs";
2424
+ import { existsSync as existsSync7 } from "fs";
2308
2425
  import { homedir as homedir5 } from "os";
2309
2426
  import { join as join8 } from "path";
2310
2427
  function localAgents(env = process.env, home = homedir5()) {
@@ -2318,7 +2435,7 @@ function localAgents(env = process.env, home = homedir5()) {
2318
2435
  ];
2319
2436
  }
2320
2437
  function detectLocalAgents(env = process.env, home = homedir5()) {
2321
- return localAgents(env, home).filter((agent2) => existsSync6(agent2.root));
2438
+ return localAgents(env, home).filter((agent2) => existsSync7(agent2.root));
2322
2439
  }
2323
2440
  function selectLocalAgents(ids, env = process.env, home = homedir5()) {
2324
2441
  if (!ids?.length) return detectLocalAgents(env, home);
@@ -2441,6 +2558,12 @@ Details: \`${REFERENCES_DIR}/workspaces.md\` in this skill's folder.
2441
2558
  cheaper or different agent to get around a refusal.
2442
2559
  - \`get_usage\` shows the balance; check it before starting several cloud sessions
2443
2560
  at once so the person is told once, up front, instead of after the first fails.
2561
+ - Anything else the person can do on agentsky.dev or in the Mac app (rename or
2562
+ archive a thread, edit a Library file or skill, change an agent's model, invite
2563
+ a member, \u2026) is an API operation: find it with \`search_api\`, read its
2564
+ parameters with \`describe_api\`, and run it with \`call_api\`. It acts as the
2565
+ person with their own key, so make only the change they asked for. Where MCP
2566
+ is missing, \`sky api search <words>\` and \`sky api <METHOD> <path>\` do the same.
2444
2567
 
2445
2568
  ## How to pick
2446
2569
 
@@ -2828,7 +2951,7 @@ function installSkill(agent2, opts = {}) {
2828
2951
  var MCP_SERVER_NAME = SKILL_NAME;
2829
2952
  function detectHermes(env = process.env, home = homedir6()) {
2830
2953
  const root = env.HERMES_HOME || join10(home, ".hermes");
2831
- return existsSync7(root) ? { id: "hermes", name: "Hermes", root } : null;
2954
+ return existsSync8(root) ? { id: "hermes", name: "Hermes", root } : null;
2832
2955
  }
2833
2956
  function detectMcpClients(env = process.env, home = homedir6()) {
2834
2957
  const hermes = detectHermes(env, home);
@@ -2988,7 +3111,7 @@ function refreshMcpRegistrations(opts, clients = detectMcpClients(), runner = sy
2988
3111
  }
2989
3112
 
2990
3113
  // src/shell-env.ts
2991
- import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
3114
+ import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
2992
3115
  import { homedir as homedir7 } from "os";
2993
3116
  import { basename as basename6, dirname as dirname5, join as join11 } from "path";
2994
3117
  var TOKEN_ENV = "AGENTSKY_API_KEY";
@@ -3004,13 +3127,13 @@ function shellStartupFiles(env = process.env, home = homedir7(), platform = proc
3004
3127
  const shell = basename6(env.SHELL ?? "");
3005
3128
  const files2 = [];
3006
3129
  const zshenv = join11(env.ZDOTDIR || home, ".zshenv");
3007
- if (shell === "zsh" || platform === "darwin" || existsSync8(zshenv)) files2.push({ path: zshenv, kind: "sh" });
3130
+ if (shell === "zsh" || platform === "darwin" || existsSync9(zshenv)) files2.push({ path: zshenv, kind: "sh" });
3008
3131
  const bashrc = join11(home, ".bashrc");
3009
- if (shell === "bash" || existsSync8(bashrc)) files2.push({ path: bashrc, kind: "sh" });
3132
+ if (shell === "bash" || existsSync9(bashrc)) files2.push({ path: bashrc, kind: "sh" });
3010
3133
  const bashProfile = join11(home, ".bash_profile");
3011
- if (existsSync8(bashProfile)) files2.push({ path: bashProfile, kind: "sh" });
3134
+ if (existsSync9(bashProfile)) files2.push({ path: bashProfile, kind: "sh" });
3012
3135
  const fish = join11(env.XDG_CONFIG_HOME || join11(home, ".config"), "fish");
3013
- if (shell === "fish" || existsSync8(fish)) files2.push({ path: join11(fish, "conf.d", "agentsky.fish"), kind: "fish" });
3136
+ if (shell === "fish" || existsSync9(fish)) files2.push({ path: join11(fish, "conf.d", "agentsky.fish"), kind: "fish" });
3014
3137
  return files2;
3015
3138
  }
3016
3139
  function writeShellEnv(token, opts = {}) {
@@ -3026,7 +3149,7 @@ function writeShellEnv(token, opts = {}) {
3026
3149
  writeFileSync6(fish, `${header2}${token ? `set -gx ${TOKEN_ENV} ${quoted(token)}` : `set -e ${TOKEN_ENV}`}
3027
3150
  `, { mode: 384 });
3028
3151
  for (const file of startup) {
3029
- const existing = existsSync8(file.path) ? readFileSync11(file.path, "utf8") : "";
3152
+ const existing = existsSync9(file.path) ? readFileSync11(file.path, "utf8") : "";
3030
3153
  if (existing.includes(HOOK_MARK)) continue;
3031
3154
  const line = file.kind === "fish" ? `${HOOK_MARK}
3032
3155
  test -f ${quoted(fish)}; and source ${quoted(fish)}
@@ -3397,12 +3520,42 @@ function clip(value, max = 120) {
3397
3520
  const text = typeof value === "string" ? value : JSON.stringify(value);
3398
3521
  return text.length > max ? `${text.slice(0, max)}\u2026` : text;
3399
3522
  }
3400
- function renderEvent(event, data) {
3523
+ function createEventRenderer(out = process.stdout) {
3524
+ const seen = /* @__PURE__ */ new Set();
3525
+ let openStream = null;
3526
+ const closeLine = () => {
3527
+ if (openStream === null) return;
3528
+ out.write("\n");
3529
+ openStream = null;
3530
+ };
3531
+ return (event, data) => {
3532
+ const id = typeof data.id === "string" ? data.id : void 0;
3533
+ if (id) {
3534
+ if (seen.has(id)) return;
3535
+ seen.add(id);
3536
+ }
3537
+ if (event !== "agent.message") {
3538
+ if (event && event !== "keepalive") closeLine();
3539
+ renderOther(event, data);
3540
+ return;
3541
+ }
3542
+ const parts = Array.isArray(data.parts) ? data.parts : [];
3543
+ const streamId = typeof parts[0]?.stream_id === "string" ? parts[0].stream_id : null;
3544
+ const text = String(data.text ?? "");
3545
+ if (streamId === null) {
3546
+ closeLine();
3547
+ out.write(`${text}
3548
+ `);
3549
+ } else {
3550
+ if (openStream !== streamId) closeLine();
3551
+ out.write(text);
3552
+ openStream = streamId;
3553
+ }
3554
+ };
3555
+ }
3556
+ function renderOther(event, data) {
3401
3557
  const part = data.part;
3402
3558
  switch (event) {
3403
- case "agent.message":
3404
- console.log(String(data.text ?? ""));
3405
- break;
3406
3559
  case "agent.reasoning":
3407
3560
  console.log(dim(`\xB7 ${clip(part?.text ?? "")}`));
3408
3561
  break;
@@ -3434,11 +3587,12 @@ function renderEvent(event, data) {
3434
3587
  }
3435
3588
 
3436
3589
  // src/session-read.ts
3437
- var TURN_TYPES = ["user.message", "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
3590
+ var TURN_STARTS = /* @__PURE__ */ new Set(["user.message", "goal.defined", "goal.iteration"]);
3591
+ var TURN_TYPES = [...TURN_STARTS, "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
3438
3592
  function latestTurn(events) {
3439
3593
  let start = 0;
3440
3594
  for (let index = events.length - 1; index >= 0; index -= 1) {
3441
- if (events[index].type === "user.message") {
3595
+ if (TURN_STARTS.has(events[index].type)) {
3442
3596
  start = index + 1;
3443
3597
  break;
3444
3598
  }
@@ -3485,7 +3639,7 @@ import { createInterface as createInterface3 } from "readline";
3485
3639
 
3486
3640
  // src/setup-local.ts
3487
3641
  import { isUtf8 as isUtf83 } from "buffer";
3488
- import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
3642
+ import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
3489
3643
  import { homedir as homedir8 } from "os";
3490
3644
  import { dirname as dirname6, isAbsolute as isAbsolute2, join as join13, resolve as resolve6 } from "path";
3491
3645
  import { parse as parseToml4 } from "smol-toml";
@@ -3526,7 +3680,7 @@ function readLocalSkills(agents) {
3526
3680
  const skills2 = [];
3527
3681
  const ignored = [];
3528
3682
  for (const agent2 of agents) {
3529
- if (!existsSync9(agent2.skillsDir)) continue;
3683
+ if (!existsSync10(agent2.skillsDir)) continue;
3530
3684
  for (const entry2 of readdirSync6(agent2.skillsDir, { withFileTypes: true })) {
3531
3685
  const dir = join13(agent2.skillsDir, entry2.name);
3532
3686
  let isDir2 = entry2.isDirectory();
@@ -3537,7 +3691,7 @@ function readLocalSkills(agents) {
3537
3691
  isDir2 = false;
3538
3692
  }
3539
3693
  }
3540
- if (!isDir2 || !existsSync9(join13(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
3694
+ if (!isDir2 || !existsSync10(join13(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
3541
3695
  const name = frontmatterName(readFileSync12(join13(dir, "SKILL.md"), "utf8"));
3542
3696
  if (!name) {
3543
3697
  ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
@@ -3644,7 +3798,7 @@ function findLocalSecrets(agents, home = homedir8()) {
3644
3798
 
3645
3799
  // src/subscriptions.ts
3646
3800
  import { spawn as spawn3 } from "child_process";
3647
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
3801
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
3648
3802
  import { homedir as homedir9 } from "os";
3649
3803
  import { join as join14 } from "path";
3650
3804
  import { createInterface as createInterface2 } from "readline";
@@ -3743,7 +3897,7 @@ async function connectClaude() {
3743
3897
  }
3744
3898
  function codexSignIn(env = process.env, home = homedir9()) {
3745
3899
  const path = join14(env.CODEX_HOME || join14(home, ".codex"), "auth.json");
3746
- if (!existsSync10(path)) {
3900
+ if (!existsSync11(path)) {
3747
3901
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
3748
3902
  }
3749
3903
  const raw = readFileSync13(path, "utf8");
@@ -3756,7 +3910,7 @@ function codexSignIn(env = process.env, home = homedir9()) {
3756
3910
  }
3757
3911
  function cursorSignIn(env = process.env, home = homedir9()) {
3758
3912
  const path = join14(env.XDG_CONFIG_HOME || join14(home, ".config"), "cursor", "auth.json");
3759
- if (!existsSync10(path)) {
3913
+ if (!existsSync11(path)) {
3760
3914
  throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
3761
3915
  }
3762
3916
  const raw = readFileSync13(path, "utf8");
@@ -3878,7 +4032,9 @@ async function subscriptionUsage(name, account) {
3878
4032
  return usage;
3879
4033
  }
3880
4034
  function renderSubscriptionUsage(name, usage) {
3881
- const label = MODEL_SUBSCRIPTION_PROVIDER_NAME[managedProviderFor(name)];
4035
+ const provider = managedProviderFor(name);
4036
+ const label = MODEL_SUBSCRIPTION_PROVIDER_NAME[provider];
4037
+ if (usage.status === "unavailable" && (usage.reason === "unsupported" || provider === "anthropic" && usage.reason === "permission")) return [`${label}: usage isn't available for this account type`];
3882
4038
  if (usage.status === "unavailable") return [`${label}: usage unavailable (${usage.reason ?? "unknown"})`];
3883
4039
  const head = [`${label}${usage.plan ? ` ${usage.plan}` : ""}`, usage.allowed === false ? "limit reached" : null].filter(Boolean).join(" \xB7 ");
3884
4040
  const rows = usage.windows.map(
@@ -4353,12 +4509,25 @@ async function getSkillUsage(since) {
4353
4509
  const qs = since ? `?since=${encodeURIComponent(since)}` : "";
4354
4510
  return api("GET", `/skills/usage${qs}`);
4355
4511
  }
4512
+ function nextSkills(current, skillId, verb) {
4513
+ const has = current.some((ref) => ref.type === "skill" && ref.skillId === skillId);
4514
+ if (verb === "attach") return has ? null : [...current, { type: "skill", skillId, version: "latest" }];
4515
+ return has ? current.filter((ref) => !(ref.type === "skill" && ref.skillId === skillId)) : null;
4516
+ }
4517
+ async function setAgentSkill(agentId, skillId, verb) {
4518
+ const path = `/agents/${encodeURIComponent(agentId)}`;
4519
+ const { agent: agent2 } = await api("GET", path);
4520
+ const skills2 = nextSkills(agent2.skills ?? [], skillId, verb);
4521
+ if (!skills2) return "unchanged";
4522
+ await api("PATCH", path, { skills: skills2, expectedVersion: agent2.version });
4523
+ return "changed";
4524
+ }
4356
4525
 
4357
4526
  // src/task-export.ts
4358
4527
  import { createHash as createHash6 } from "crypto";
4359
4528
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
4360
4529
  import { dirname as dirname8, join as join17 } from "path";
4361
- import { stringify as stringifyToml2 } from "smol-toml";
4530
+ import { stringify as stringifyToml } from "smol-toml";
4362
4531
  var TEXT_MAX = 2e4;
4363
4532
  function scrub(text, names2) {
4364
4533
  let out = redactPlainText(text).text;
@@ -4536,7 +4705,7 @@ Write your deliverables under /workspace/outputs.
4536
4705
  `);
4537
4706
  out.set(
4538
4707
  "task.toml",
4539
- stringifyToml2({
4708
+ stringifyToml({
4540
4709
  metadata: {
4541
4710
  name: `agentsky-${idHash}`,
4542
4711
  tier: opts.tier,
@@ -4961,6 +5130,44 @@ function renderBrowserProfile(profile) {
4961
5130
  return lines.join("\n");
4962
5131
  }
4963
5132
 
5133
+ // src/mac.ts
5134
+ var REACH_KINDS = ["folder", "browser", "screen", "host"];
5135
+ var OPEN = /* @__PURE__ */ new Set(["pending", "asking"]);
5136
+ var MAX_WAIT_SECONDS = 25;
5137
+ async function listMacs() {
5138
+ return (await api("GET", "/desktop/machines")).machines;
5139
+ }
5140
+ async function askMac(machine, kind, reason) {
5141
+ if (!REACH_KINDS.includes(kind)) throw new Error(`kind must be one of ${REACH_KINDS.join(", ")}`);
5142
+ return api("POST", `/desktop/machines/${encodeURIComponent(machine)}/reach-requests`, { kind, reason });
5143
+ }
5144
+ async function readReachRequest(id, waitSeconds = 0) {
5145
+ const wait = Math.min(Math.max(Math.floor(waitSeconds), 0), MAX_WAIT_SECONDS);
5146
+ return (await api("GET", `/desktop/reach-requests/${encodeURIComponent(id)}${wait ? `?wait=${wait}` : ""}`)).reach_request;
5147
+ }
5148
+ async function waitForAnswer(id, budgetSeconds) {
5149
+ const until = Date.now() + budgetSeconds * 1e3;
5150
+ for (; ; ) {
5151
+ const left = Math.ceil((until - Date.now()) / 1e3);
5152
+ const request = await readReachRequest(id, Math.max(left, 0));
5153
+ if (!OPEN.has(request.status) || left <= 0) return request;
5154
+ }
5155
+ }
5156
+ async function takeBack(mount) {
5157
+ await api("DELETE", `/desktop/mounts/${encodeURIComponent(mount)}`);
5158
+ }
5159
+ function renderMac(mac2) {
5160
+ const lends = mac2.reach ? mac2.reach.kinds.join(", ") || "nothing" : "unknown (update the Mac app)";
5161
+ const screen = mac2.reach && !mac2.reach.kinds.includes("screen") ? ` screen: ${mac2.reach.screen}` : "";
5162
+ const lines = [`${mac2.id} ${mac2.name} ${mac2.online ? "online" : "offline"} can lend: ${lends}${screen}`];
5163
+ for (const mount of mac2.mounts ?? []) lines.push(` ${mount.id} ${mount.kind.padEnd(7)} ${mount.root ?? mount.label}`);
5164
+ return lines.join("\n");
5165
+ }
5166
+ function renderReachRequest(request) {
5167
+ if (request.status === "granted" && request.mount) return `${request.id} granted ${request.mount.id} (${request.mount.kind}${request.mount.root ? ` ${request.mount.root}` : ""})`;
5168
+ return `${request.id} ${request.status}${request.error ? ` ${request.error.code}: ${request.error.message}` : ""}`;
5169
+ }
5170
+
4964
5171
  // src/compare.ts
4965
5172
  async function listComparisons(opts = {}) {
4966
5173
  const qs = opts.archived ? "?archived=true" : "";
@@ -4981,6 +5188,53 @@ async function listThreads2(opts = {}) {
4981
5188
  const { threads } = await api("GET", `/threads${qs}`);
4982
5189
  return threads;
4983
5190
  }
5191
+ async function startThread(body) {
5192
+ return api("POST", "/threads", body);
5193
+ }
5194
+ var threadPath = (id) => `/threads/${encodeURIComponent(id)}`;
5195
+ async function getThread(id) {
5196
+ return api("GET", threadPath(id));
5197
+ }
5198
+ async function organizeThread(id, change) {
5199
+ return api("PATCH", threadPath(id), change);
5200
+ }
5201
+ async function deleteThread(id) {
5202
+ return api("DELETE", threadPath(id));
5203
+ }
5204
+ async function sendThreadMessage(id, body) {
5205
+ return api("POST", `${threadPath(id)}/messages`, body);
5206
+ }
5207
+ async function addThreadAgent(id, body) {
5208
+ return api("POST", `${threadPath(id)}/agents`, body);
5209
+ }
5210
+ async function removeThreadAgent(id, columnId) {
5211
+ return api("DELETE", `${threadPath(id)}/agents/${encodeURIComponent(columnId)}`);
5212
+ }
5213
+ async function interruptThread(id, columnId) {
5214
+ return api("POST", `${threadPath(id)}/interrupt`, columnId ? { columnId } : {});
5215
+ }
5216
+ async function forkThread(id, body) {
5217
+ return api("POST", `${threadPath(id)}/fork`, body);
5218
+ }
5219
+ async function changeThreadModel(id, change, columnId) {
5220
+ return api("PATCH", `${threadPath(id)}/engine${columnId ? `?column=${encodeURIComponent(columnId)}` : ""}`, change);
5221
+ }
5222
+ async function searchThreads(q, opts = {}) {
5223
+ const params = new URLSearchParams({ q, ...opts.threads ? { threads: opts.threads } : {}, ...opts.cursor ? { cursor: opts.cursor } : {} });
5224
+ return api("GET", `/threads/search?${params}`);
5225
+ }
5226
+ async function threadOptions() {
5227
+ return api("GET", "/threads/options");
5228
+ }
5229
+ async function joinThread(sessionId) {
5230
+ return api("POST", "/threads/joinable", { sessionId });
5231
+ }
5232
+ async function listProjects() {
5233
+ return api("GET", "/projects");
5234
+ }
5235
+ async function rememberProject(project2) {
5236
+ return api("PUT", "/projects", { project: project2 });
5237
+ }
4984
5238
  function renderComparisonSummary(c) {
4985
5239
  const agents = c.agents.map((a) => `${a.agentType}/${a.llm}`).join(" vs ");
4986
5240
  const status = c.statusLine ?? c.attention ?? (c.archived ? "archived" : "active");
@@ -4993,8 +5247,202 @@ function renderThreadSummary(t) {
4993
5247
  return `${t.id.slice(0, 12)} ${flag}${t.title.slice(0, 40).padEnd(44)} [${agents}] ${status}`;
4994
5248
  }
4995
5249
 
5250
+ // src/library.ts
5251
+ var artifactPath = (id) => `/library/artifacts/${encodeURIComponent(id)}`;
5252
+ async function listLibrary(query = {}) {
5253
+ const params = new URLSearchParams(Object.entries(query).filter((entry2) => Boolean(entry2[1])));
5254
+ const qs = params.size ? `?${params}` : "";
5255
+ return api("GET", `/library/artifacts${qs}`);
5256
+ }
5257
+ async function uploadToLibrary(file, requestId, workspace2) {
5258
+ const form = new FormData();
5259
+ form.append("file", new Blob([new Uint8Array(file.bytes)], { type: file.mediaType }), file.name);
5260
+ form.append("requestId", requestId);
5261
+ if (workspace2) form.append("workspace", workspace2);
5262
+ return apiForm("POST", "/library/artifacts", form);
5263
+ }
5264
+ async function getLibraryArtifact(id) {
5265
+ return api("GET", artifactPath(id));
5266
+ }
5267
+ async function deleteLibraryArtifact(id, revision) {
5268
+ return api("DELETE", artifactPath(id), { revision });
5269
+ }
5270
+ async function shareLibraryArtifact(id, enabled) {
5271
+ const { currentVersion } = await getLibraryArtifact(id);
5272
+ return api("PATCH", `${artifactPath(id)}/share`, { enabled, revision: currentVersion });
5273
+ }
5274
+ async function placeLibraryArtifact(id, workspace2) {
5275
+ return api("PATCH", `${artifactPath(id)}/workspace`, { workspace: workspace2 });
5276
+ }
5277
+ async function downloadLibraryArtifact(id, version) {
5278
+ return apiBytes("GET", `${artifactPath(id)}/download${version ? `?version=${version}` : ""}`);
5279
+ }
5280
+ function renderLibraryArtifact(artifact2) {
5281
+ const version = artifact2.currentVersion ? `v${artifact2.currentVersion}` : "";
5282
+ return `${artifact2.id} ${artifact2.name} ${version}${artifact2.shared ? " shared" : ""}`;
5283
+ }
5284
+
5285
+ // src/api-command.ts
5286
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
5287
+ var API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
5288
+ function readData(data, stdin = () => readFileSync16(0, "utf8")) {
5289
+ if (data === void 0) return void 0;
5290
+ const raw = data === "-" ? stdin() : data.startsWith("@") ? readFileSync16(data.slice(1), "utf8") : data;
5291
+ try {
5292
+ JSON.parse(raw);
5293
+ } catch {
5294
+ throw new Error("--data must be JSON (inline, @file or - for stdin)");
5295
+ }
5296
+ return raw;
5297
+ }
5298
+ function requestPath(path, query = []) {
5299
+ if (!path.startsWith("/") || path.startsWith("//")) throw new Error("path must start with /, e.g. /threads");
5300
+ const clean = path.replace(/^\/v1(?=\/)/, "");
5301
+ const params = new URLSearchParams();
5302
+ for (const pair of query) {
5303
+ const at = pair.indexOf("=");
5304
+ if (at <= 0) throw new Error(`--query needs key=value, got "${pair}"`);
5305
+ params.append(pair.slice(0, at), pair.slice(at + 1));
5306
+ }
5307
+ const sep3 = clean.includes("?") ? "&" : "?";
5308
+ const encoded = params.toString();
5309
+ return encoded ? `${clean}${sep3}${encoded}` : clean;
5310
+ }
5311
+ function matchOperations(doc, words) {
5312
+ const needles = words.map((w) => w.toLowerCase());
5313
+ const out = [];
5314
+ for (const [path, item] of Object.entries(doc.paths ?? {})) {
5315
+ for (const [method, op] of Object.entries(item)) {
5316
+ if (!API_METHODS.includes(method.toUpperCase())) continue;
5317
+ const hay = `${method} ${path} ${op.summary ?? ""} ${op.operationId ?? ""}`.toLowerCase();
5318
+ if (needles.every((n) => hay.includes(n))) out.push({ method: method.toUpperCase(), path, summary: op.summary, operationId: op.operationId });
5319
+ }
5320
+ }
5321
+ return out.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
5322
+ }
5323
+ async function searchApi(words) {
5324
+ const res = await apiRaw("GET", "/openapi.json", { auth: false });
5325
+ if (!res.ok) throw new Error(`could not read the API reference (${res.status})`);
5326
+ const ops = matchOperations(await res.json(), words);
5327
+ if (!ops.length) {
5328
+ console.log("no operations match");
5329
+ return;
5330
+ }
5331
+ for (const op of ops) console.log(`${op.method.padEnd(6)} ${op.path}${op.summary ? ` \u2014 ${op.summary}` : ""}`);
5332
+ }
5333
+ async function callApi(method, path, opts) {
5334
+ const verb = method.toUpperCase();
5335
+ if (!API_METHODS.includes(verb)) throw new Error(`method must be one of ${API_METHODS.join(", ")} or "search"`);
5336
+ const body = readData(opts.data);
5337
+ if (body !== void 0 && verb === "GET") throw new Error("a GET takes no --data; use --query");
5338
+ const res = await apiRaw(verb, requestPath(path, opts.query), { body, idempotencyKey: opts.idempotencyKey, universe: opts.workspace });
5339
+ const type = res.headers.get("content-type") ?? "";
5340
+ if (opts.output) {
5341
+ writeFileSync9(opts.output, new Uint8Array(await res.arrayBuffer()));
5342
+ console.error(`${res.status} \u2192 ${opts.output}`);
5343
+ } else if (/json/.test(type)) {
5344
+ const text = await res.text();
5345
+ let printed = text;
5346
+ try {
5347
+ printed = JSON.stringify(JSON.parse(text), null, 2);
5348
+ } catch {
5349
+ }
5350
+ (res.ok ? console.log : console.error)(printed);
5351
+ } else if (/^text\/|xml|csv|markdown/.test(type) || !type) {
5352
+ (res.ok ? console.log : console.error)(await res.text());
5353
+ } else {
5354
+ console.error(`${res.status}: ${type} body (${res.headers.get("content-length") ?? "?"} bytes); pass --output <file> to save it`);
5355
+ }
5356
+ if (!res.ok && res.status !== 304) process.exitCode = 1;
5357
+ }
5358
+
5359
+ // src/billing-plans.ts
5360
+ var readCodingPlan = () => api("GET", "/billing/coding-plan");
5361
+ function joinCodingPlan(dollars2, requestId) {
5362
+ const amount = Number(dollars2.trim().replace(/^\$/, ""));
5363
+ if (!Number.isInteger(amount) || amount <= 0) throw new Error(`<dollars> must be whole dollars, got ${JSON.stringify(dollars2)}`);
5364
+ return api("POST", "/billing/coding-plan", { amount_usd: amount, request_id: requestId });
5365
+ }
5366
+ var cancelCodingPlan = () => api("DELETE", "/billing/coding-plan");
5367
+ function renderCodingPlan(plan) {
5368
+ if (!plan.active) {
5369
+ const lines2 = [`coding plan: not a member (enrollment ${plan.enrollment}; monthly amounts $${plan.amounts_usd.join(", $")})`];
5370
+ if (plan.charge_failure) lines2.push(` last charge failed: ${plan.charge_failure}`);
5371
+ return lines2.join("\n");
5372
+ }
5373
+ const until = plan.current_period_end?.slice(0, 10) ?? "?";
5374
+ const lines = [`coding plan: $${plan.monthly_credit_usd}/month, paid until ${until}, ${plan.renews ? "renews" : "does not renew"}`];
5375
+ if (plan.charge_failure) lines.push(` last charge failed: ${plan.charge_failure}`);
5376
+ return lines.join("\n");
5377
+ }
5378
+ function readRequestUsage(opts) {
5379
+ const query = usageQuery(opts);
5380
+ const cursor = opts.cursor ? `${query ? "&" : "?"}cursor=${encodeURIComponent(opts.cursor)}` : "";
5381
+ return api("GET", `/billing/usage/requests${query}${cursor}`);
5382
+ }
5383
+ function renderRequestUsage(page) {
5384
+ 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)}`);
5385
+ if (!lines.length) lines.push("no API requests in this range");
5386
+ if (page.nextCursor) lines.push(`more: --cursor ${page.nextCursor}`);
5387
+ return lines.join("\n");
5388
+ }
5389
+ var readReferralLinks = () => api("GET", "/billing/referral-links");
5390
+ function referralLinkBody(opts) {
5391
+ 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");
5392
+ const body = {};
5393
+ if (opts.price !== void 0) body.customer_multiplier = multiplier("--price", opts.price);
5394
+ if (opts.sku?.length) {
5395
+ body.sku_prices = opts.sku.map((pair) => {
5396
+ const at = pair.lastIndexOf("=");
5397
+ if (at <= 0) throw new Error(`--sku must be id=multiplier, got ${JSON.stringify(pair)}`);
5398
+ return { sku_id: pair.slice(0, at), multiplier: multiplier("--sku", pair.slice(at + 1)) };
5399
+ });
5400
+ }
5401
+ if (opts.email) body.audience_email = opts.email;
5402
+ if (opts.uses) body.max_redemptions = wholeNumber2("--uses", opts.uses);
5403
+ if (opts.days) body.expires_in_days = wholeNumber2("--days", opts.days);
5404
+ return body;
5405
+ }
5406
+ var createReferralLink = (opts) => api("POST", "/billing/referral-links", referralLinkBody(opts));
5407
+ var disableReferralLink = (id) => api("POST", `/billing/referral-links/${encodeURIComponent(id)}/disable`);
5408
+ function renderReferralLinks(dashboard) {
5409
+ const lines = [`priced from ${dashboard.source_plan.name}: customers pay at least ${Math.round(dashboard.source_plan.multiplier * 100)}% of public price`];
5410
+ for (const link2 of dashboard.links) {
5411
+ const state = link2.disabled_at ? "disabled" : `expires ${link2.expires_at.slice(0, 10)}`;
5412
+ const audience = link2.audience_email ? ` for ${link2.audience_email} (${link2.email_status})` : "";
5413
+ lines.push(` ${link2.id} ${Math.round(link2.customer_multiplier * 100)}% ${link2.redeemed_count}/${link2.max_redemptions} used ${state}${audience}`);
5414
+ lines.push(` ${link2.url}`);
5415
+ }
5416
+ if (!dashboard.links.length) lines.push(" no links yet");
5417
+ const earned = dashboard.earnings.reduce((sum, earning) => sum + earning.amount_usd, 0);
5418
+ lines.push(`earned $${earned.toFixed(2)} from ${dashboard.earnings.length} customer${dashboard.earnings.length === 1 ? "" : "s"}`);
5419
+ return lines.join("\n");
5420
+ }
5421
+ function multiplier(flag, value) {
5422
+ const trimmed = value.trim();
5423
+ const percent = trimmed.endsWith("%");
5424
+ const number = Number(percent ? trimmed.slice(0, -1) : trimmed);
5425
+ 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)}`);
5426
+ return percent ? number / 100 : number;
5427
+ }
5428
+ function wholeNumber2(flag, value) {
5429
+ if (!/^\d+$/.test(value.trim())) throw new Error(`${flag} must be a whole number, got ${JSON.stringify(value)}`);
5430
+ return Number(value.trim());
5431
+ }
5432
+
5433
+ // src/workspace-flag.ts
5434
+ function workspaceFlagIsCommands(argv, commandPath) {
5435
+ let at = 0;
5436
+ let matched = 0;
5437
+ for (; at < argv.length && matched < commandPath.length; at += 1) {
5438
+ if (argv[at] === commandPath[matched]) matched += 1;
5439
+ }
5440
+ if (matched < commandPath.length) return false;
5441
+ return argv.slice(at).some((arg) => arg === "--workspace" || arg.startsWith("--workspace="));
5442
+ }
5443
+
4996
5444
  // src/harness.ts
4997
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
5445
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
4998
5446
  import { dirname as dirname9, join as join18, resolve as resolve8 } from "path";
4999
5447
  var HARNESS_FILE = "harness.json";
5000
5448
  function harnessFilePath(dir) {
@@ -5002,11 +5450,11 @@ function harnessFilePath(dir) {
5002
5450
  }
5003
5451
  function readHarnessFile(dir) {
5004
5452
  const path = harnessFilePath(dir);
5005
- if (!existsSync11(path)) {
5453
+ if (!existsSync12(path)) {
5006
5454
  throw new Error(`no ${HARNESS_FILE} found in ${resolve8(dir)} \u2014 run \`sky harness init\` first`);
5007
5455
  }
5008
5456
  try {
5009
- return JSON.parse(readFileSync16(path, "utf8"));
5457
+ return JSON.parse(readFileSync17(path, "utf8"));
5010
5458
  } catch (err) {
5011
5459
  throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
5012
5460
  }
@@ -5033,11 +5481,11 @@ var INIT_COMMENT = `# harness.json \u2014 workspace harness profile skeleton
5033
5481
  `;
5034
5482
  function harnessInit(dir) {
5035
5483
  const path = harnessFilePath(dir);
5036
- if (existsSync11(path)) {
5484
+ if (existsSync12(path)) {
5037
5485
  throw new Error(`${path} already exists \u2014 delete it to re-init`);
5038
5486
  }
5039
5487
  mkdirSync6(dirname9(path), { recursive: true });
5040
- writeFileSync9(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
5488
+ writeFileSync10(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
5041
5489
  `);
5042
5490
  console.log(`Created ${path}`);
5043
5491
  console.log(INIT_COMMENT);
@@ -5170,9 +5618,16 @@ function printModelBilling(session2) {
5170
5618
  }
5171
5619
  var program = new Command();
5172
5620
  program.name("sky").description("Launch and talk to long-lived agents \u2014 agentsky.dev").version(CLI_VERSION).option("--workspace <slug>", "workspace to act in (overrides sky universe switch and SKY_UNIVERSE; alias of --universe on sub-commands)");
5173
- program.hook("preAction", () => {
5621
+ program.hook("preAction", (_root, action) => {
5174
5622
  const workspace2 = program.opts().workspace;
5175
- if (workspace2) process.env.SKY_WORKSPACE = workspace2;
5623
+ if (!workspace2) return;
5624
+ const path = [];
5625
+ for (let command = action; command && command !== program; command = command.parent) path.unshift(command.name());
5626
+ if (action.options.some((option) => option.long === "--workspace") && workspaceFlagIsCommands(process.argv.slice(2), path)) {
5627
+ action.setOptionValue("workspace", workspace2);
5628
+ return;
5629
+ }
5630
+ process.env.SKY_WORKSPACE = workspace2;
5176
5631
  });
5177
5632
  var lastProgressDecile = -1;
5178
5633
  function uploadProgress(sent, total) {
@@ -5207,6 +5662,9 @@ auth.command("whoami").description("who the current token acts as").action(
5207
5662
  console.log(
5208
5663
  `${who.user.email ?? "unknown"} \xB7 universe ${who.universe.slug}${who.universe.isPersonal ? " (default)" : ""} \xB7 scopes ${who.scopes.join(",")}`
5209
5664
  );
5665
+ if (who.credential_universe) {
5666
+ console.log(` this key is fixed to workspace ${who.credential_universe}: it cannot act in other workspaces or create new ones (make an account-wide key with \`sky keys create\`, or \`sky auth login\`)`);
5667
+ }
5210
5668
  })
5211
5669
  );
5212
5670
  auth.command("connect <provider>").description("connect a model subscription: claude (setup-token) or chatgpt (Codex sign-in); --browser signs any plan in on its own page").option("-y, --yes", "skip the consent prompt").option("--browser", "sign in on the provider's page instead of using this machine's sign-in (claude, chatgpt, cursor, grok, muse)").option("--account <id>", "with --browser: re-sign an additional account, or `new` to add one").option("--label <label>", "with --browser: name the account").option("--no-open", "with --browser: print the sign-in link without opening it").action(
@@ -5231,6 +5689,19 @@ auth.command("usage <provider>").description("plan allowance left on a connected
5231
5689
  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)));
5232
5690
  auth.command("accounts").description("list connected model subscriptions and the agents they power").action(run(async () => listSubscriptions()));
5233
5691
  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)));
5692
+ auth.command("apps").description("the registered apps you let into your account, with the scopes you allowed").action(
5693
+ run(async () => {
5694
+ const grants = await listAppGrants();
5695
+ if (!grants.length) console.log("No apps have access to your account.");
5696
+ for (const grant of grants) console.log(renderAppGrant(grant));
5697
+ })
5698
+ );
5699
+ auth.command("revoke-app <clientId>").description("remove an app's access to your account; its tokens stop working at once (admin scope)").action(
5700
+ run(async (clientId) => {
5701
+ await revokeAppGrant(clientId);
5702
+ console.log(`Removed ${clientId}'s access to your account`);
5703
+ })
5704
+ );
5234
5705
  var universe = program.command("universe").description("grouping boundary for agents");
5235
5706
  universe.command("list").action(
5236
5707
  run(async () => {
@@ -5277,7 +5748,7 @@ universe.command("invite <emails...>").description("invite people by email; prin
5277
5748
  run(async (emails, opts) => {
5278
5749
  const result = await invite(await workspaceSlug(opts.universe), emails, opts);
5279
5750
  for (const i of result.invites) console.log(`invited ${i.email} as ${i.role} ${i.id}`);
5280
- for (const link of result.links) console.log(` ${link.email} joins with: sky universe join ${link.token}`);
5751
+ for (const link2 of result.links) console.log(` ${link2.email} joins with: sky universe join ${link2.token}`);
5281
5752
  if (!result.invites.length) console.log("nobody new to invite \u2014 everyone listed is already a member");
5282
5753
  })
5283
5754
  );
@@ -5330,6 +5801,12 @@ workspace.command("list").action(
5330
5801
  })
5331
5802
  );
5332
5803
  workspace.command("delete <slug>").description("permanently delete a workspace you own and everything in it (asks you to type the slug)").option("-y, --yes", "skip typing the slug").action(run(deleteWorkspaceCommand));
5804
+ async function renameWorkspaceCommand(slug, name) {
5805
+ const { universe: renamed } = await api("PATCH", `/universes/${encodeURIComponent(slug)}`, { name: name.join(" ") });
5806
+ console.log(`${renamed.slug} is now "${renamed.name}"`);
5807
+ }
5808
+ universe.command("rename <slug> <name...>").description("rename a workspace you own (its slug stays)").action(run(renameWorkspaceCommand));
5809
+ workspace.command("rename <slug> <name...>").description("rename a workspace you own (alias of universe rename)").action(run(renameWorkspaceCommand));
5333
5810
  var policy = program.command("policy").description("policy rules: tool calls, spend caps, call counts, capabilities");
5334
5811
  async function policyScope(opts) {
5335
5812
  return opts.session ? { session: await resolveSessionRef(opts.session) } : { slug: await workspaceSlug(opts.universe) };
@@ -5440,7 +5917,7 @@ agent.command("destroy <id>").description("delete the spec; requires all session
5440
5917
  console.log(`Destroyed ${id}`);
5441
5918
  })
5442
5919
  );
5443
- agent.command("message <id> [text]").description("send one message to the default session and stream the turn until idle").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
5920
+ agent.command("message <id> [text]").description("send one message to the default session and stream the turn until idle").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks); a file_\u2026 id attaches that stored file").option("--file-id <id...>", "attach files already uploaded (`sky files upload`), by id, without uploading again").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
5444
5921
  run(
5445
5922
  async (id, text, opts) => sendAndFollow(await resolveSessionRef(id), text ?? "", opts)
5446
5923
  )
@@ -5554,7 +6031,7 @@ session.command("delete <id>").description("permanently delete the session, its
5554
6031
  console.log(`Deleted ${id}`);
5555
6032
  })
5556
6033
  );
5557
- session.command("message <ref> [text]").description("send one message (session id, or agent id = its default session)").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
6034
+ session.command("message <ref> [text]").description("send one message (session id, or agent id = its default session)").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks); a file_\u2026 id attaches that stored file").option("--file-id <id...>", "attach files already uploaded (`sky files upload`), by id, without uploading again").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
5558
6035
  run(
5559
6036
  async (ref, text, opts) => sendAndFollow(await resolveSessionRef(ref), text ?? "", opts)
5560
6037
  )
@@ -5594,12 +6071,12 @@ session.command("share <ref>").description("print the session's share link, turn
5594
6071
  console.log(`Sharing is off for ${id}`);
5595
6072
  return;
5596
6073
  }
5597
- const link = await api("POST", path, {
6074
+ const link2 = await api("POST", path, {
5598
6075
  access: opts.view ? "view" : "prompt",
5599
6076
  ...opts.rotate ? { rotate: true } : {}
5600
6077
  });
5601
- if (link.shareUrl) console.log(link.shareUrl);
5602
- else console.log(`view-only token: ${link.token} \u2014 read with GET /links/${link.token}/events`);
6078
+ if (link2.shareUrl) console.log(link2.shareUrl);
6079
+ else console.log(`view-only token: ${link2.token} \u2014 read with GET /links/${link2.token}/events`);
5603
6080
  })
5604
6081
  );
5605
6082
  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(
@@ -5747,7 +6224,7 @@ sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1
5747
6224
  run(async (ref, path, opts) => {
5748
6225
  const bytes = await readFile(await resolveSessionRef(ref), path);
5749
6226
  if (opts.out) {
5750
- writeFileSync10(opts.out, bytes);
6227
+ writeFileSync11(opts.out, bytes);
5751
6228
  console.error(`saved ${bytes.length} bytes to ${opts.out}`);
5752
6229
  } else process.stdout.write(bytes);
5753
6230
  })
@@ -5764,7 +6241,7 @@ sessionWorkspace.command("patch <ref> [repo]").description("the work in a reposi
5764
6241
  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)})`);
5765
6242
  if (!patch.length) console.error("nothing changed");
5766
6243
  else if (opts.out) {
5767
- writeFileSync10(opts.out, patch);
6244
+ writeFileSync11(opts.out, patch);
5768
6245
  console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
5769
6246
  } else process.stdout.write(patch);
5770
6247
  })
@@ -5818,9 +6295,10 @@ function interactionMode(opts) {
5818
6295
  }
5819
6296
  async function uploadAttachments(opts) {
5820
6297
  const files2 = [];
5821
- for (const path of [...opts.image ?? [], ...opts.file ?? []]) {
5822
- const file = await uploadLocalFile(path);
5823
- console.error(`uploaded ${file.filename} \u2192 ${file.id}`);
6298
+ const refs = [...opts.image ?? [], ...opts.file ?? []].map((ref) => ({ ref, byId: false }));
6299
+ for (const { ref, byId } of [...refs, ...(opts.fileId ?? []).map((ref2) => ({ ref: ref2, byId: true }))]) {
6300
+ const { file, uploaded } = await attachment(ref, { byId });
6301
+ console.error(uploaded ? `uploaded ${file.filename} \u2192 ${file.id}` : `attaching ${file.id} (${file.filename})`);
5824
6302
  files2.push(file);
5825
6303
  }
5826
6304
  return files2;
@@ -5837,9 +6315,10 @@ async function sendAndFollow(sessionId, text, opts) {
5837
6315
  const controller = new AbortController();
5838
6316
  let opened = null;
5839
6317
  if (!opts.detach) {
6318
+ const render = createEventRenderer();
5840
6319
  opened = (async () => {
5841
6320
  for await (const frame of streamEvents(sessionId, controller.signal)) {
5842
- renderEvent(frame.event, frame.data);
6321
+ render(frame.event, frame.data);
5843
6322
  if (turnOver(frame)) {
5844
6323
  controller.abort();
5845
6324
  break;
@@ -5860,10 +6339,11 @@ async function chatLoop(sessionId, opts = {}) {
5860
6339
  const controller = new AbortController();
5861
6340
  const rl = createInterface6({ input: process.stdin, output: process.stdout, prompt: "> " });
5862
6341
  let system = opts.system;
6342
+ const render = createEventRenderer();
5863
6343
  void (async () => {
5864
6344
  try {
5865
6345
  for await (const frame of streamEvents(sessionId, controller.signal)) {
5866
- renderEvent(frame.event, frame.data);
6346
+ render(frame.event, frame.data);
5867
6347
  if (frame.event === "session.status_terminated") rl.close();
5868
6348
  else if (frame.event === "session.status_idle") rl.prompt();
5869
6349
  }
@@ -5970,10 +6450,32 @@ secrets.command("unset <id> <key>").action(
5970
6450
  console.log(`unset ${key}`);
5971
6451
  })
5972
6452
  );
5973
- program.command("launch").description("scaffold ./agent.toml if absent, create the agent, print its address").option("-f, --file <path>", "agent file", "agent.toml").option("--type <type>", "override the file's harness: hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode").option("--llm <llm>", "override the file's model (must suit the harness)").action(
6453
+ async function launchFromFile(path, spec, opts) {
6454
+ const { vcpus, memoryMb, ...specBody } = spec;
6455
+ const body = {
6456
+ ...specBody,
6457
+ ...opts.type ? { agentType: opts.type } : {},
6458
+ ...opts.llm ? { llm: opts.llm } : {}
6459
+ };
6460
+ const { agent: created } = await api("POST", "/agents", body);
6461
+ const { session: session2 } = await api("POST", "/sessions", {
6462
+ agent: created.id,
6463
+ ...opts.start ? {} : { provision: "deferred" },
6464
+ ...vcpus !== void 0 ? { vcpus } : {},
6465
+ ...memoryMb !== void 0 ? { memoryMb } : {}
6466
+ });
6467
+ stampId(path, created.id);
6468
+ console.log(`Launched ${created.id} (${created.agentType} \xB7 ${created.llm})`);
6469
+ printModelBilling(session2);
6470
+ console.log(opts.start ? " its first session is starting now" : " its first session starts on the first message");
6471
+ if (!spec.llm && !opts.llm) console.log(` model: ${created.llm}, the ${created.agentType} default \u2014 set llm = "\u2026" in ${basename9(path)} and run \`sky deploy\` to change it`);
6472
+ console.log(` sky agent message ${created.id} "hello"`);
6473
+ console.log(` sky agent chat ${created.id}`);
6474
+ }
6475
+ program.command("launch").description("scaffold ./agent.toml if absent, create the agent and its first session (idle until the first message), 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)").option("--start", "boot the first session now instead of on the first message").action(
5974
6476
  run(async (opts) => {
5975
6477
  const path = resolve9(opts.file);
5976
- if (!existsSync12(path)) {
6478
+ if (!existsSync13(path)) {
5977
6479
  const name = basename9(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
5978
6480
  scaffoldAgentFile(path, name);
5979
6481
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
@@ -5983,55 +6485,27 @@ program.command("launch").description("scaffold ./agent.toml if absent, create t
5983
6485
  console.log(`${file.id} already launched \u2014 use \`sky deploy\` to re-apply.`);
5984
6486
  return;
5985
6487
  }
5986
- const { vcpus, memoryMb, ...specBody } = spec;
5987
- const body = {
5988
- ...specBody,
5989
- ...opts.type ? { agentType: opts.type } : {},
5990
- ...opts.llm ? { llm: opts.llm } : {}
5991
- };
5992
- const { agent: created } = await api("POST", "/agents", body);
5993
- console.log("Starting its runtime\u2026");
5994
- const { session: session2 } = await api("POST", "/sessions", {
5995
- agent: created.id,
5996
- ...vcpus !== void 0 ? { vcpus } : {},
5997
- ...memoryMb !== void 0 ? { memoryMb } : {}
5998
- });
5999
- stampId(path, created.id);
6000
- console.log(`Launched ${created.id} (${created.agentType} \xB7 ${created.llm})`);
6001
- printModelBilling(session2);
6002
- console.log(` sky agent message ${created.id} "hello"`);
6003
- console.log(` sky agent chat ${created.id}`);
6488
+ await launchFromFile(path, spec, opts);
6004
6489
  })
6005
6490
  );
6006
- program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
6491
+ program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent: an unchanged file changes nothing)").option("-f, --file <path>", "agent file", "agent.toml").action(
6007
6492
  run(async (opts) => {
6008
6493
  const path = resolve9(opts.file);
6009
6494
  const { file, spec } = readAgentFile(path);
6010
6495
  if (!file.id) {
6011
6496
  console.log("No agent id in the file yet \u2014 running launch instead.");
6012
- const { vcpus, memoryMb, ...specBody } = spec;
6013
- const { agent: created } = await api("POST", "/agents", specBody);
6014
- await api("POST", "/sessions", {
6015
- agent: created.id,
6016
- ...vcpus !== void 0 ? { vcpus } : {},
6017
- ...memoryMb !== void 0 ? { memoryMb } : {}
6018
- });
6019
- stampId(path, created.id);
6020
- console.log(`Launched ${created.id}`);
6497
+ await launchFromFile(path, spec, {});
6021
6498
  return;
6022
6499
  }
6500
+ const ref = encodeURIComponent(file.id);
6501
+ const { agent: before } = await api("GET", `/agents/${ref}`);
6502
+ let promptChanged = false;
6023
6503
  if (spec.prompt !== void 0) {
6024
- await api("PUT", `/agents/${encodeURIComponent(file.id)}/prompt`, {
6025
- prompt: spec.prompt
6026
- });
6504
+ const saved = await api("PUT", `/agents/${ref}/prompt`, { prompt: spec.prompt });
6505
+ promptChanged = saved.unchanged === false;
6027
6506
  }
6028
- await api("PATCH", `/agents/${encodeURIComponent(file.id)}`, {
6029
- displayName: spec.displayName,
6030
- ...spec.capabilities ? { capabilities: spec.capabilities } : {},
6031
- ...spec.reasoningEffort ? { reasoningEffort: spec.reasoningEffort } : {},
6032
- ...spec.skills ? { skills: spec.skills } : {}
6033
- });
6034
- console.log(`Deployed ${file.id} (prompt + settings re-applied)`);
6507
+ const { agent: after } = await api("PATCH", `/agents/${ref}`, deployPatch(spec));
6508
+ for (const line of deployReport(file.id, spec, before, after, promptChanged)) console.log(line);
6035
6509
  })
6036
6510
  );
6037
6511
  program.command("clone").description("clone your local agent (Claude Code, \u2026) into the cloud; re-run to sync its prompt").option("--harness <type>", "claude_code | codex | openclaw (default: detected)").option("--new", "always launch a fresh agent, even if this machine is linked").option("--dry-run", "print the plan and send nothing").option("-y, --yes", "skip the confirmation").option("--open", "open the agent page when done").action(
@@ -6272,14 +6746,14 @@ task.command("get <task> <path>").description("read one task file; prints it, or
6272
6746
  run(async (taskId, path, opts) => {
6273
6747
  const bytes = await readTaskFile(taskId, path);
6274
6748
  if (opts.out) {
6275
- writeFileSync10(opts.out, bytes);
6749
+ writeFileSync11(opts.out, bytes);
6276
6750
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6277
6751
  } else process.stdout.write(bytes);
6278
6752
  })
6279
6753
  );
6280
6754
  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(
6281
6755
  run(async (taskId, path, local) => {
6282
- const { file, conflict } = await writeTaskFile(taskId, path, readFileSync17(local), mediaTypeFor(local));
6756
+ const { file, conflict } = await writeTaskFile(taskId, path, readFileSync18(local), mediaTypeFor(local));
6283
6757
  console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
6284
6758
  })
6285
6759
  );
@@ -6352,7 +6826,7 @@ task.command("verdict <task> <did-the-job>").description("record whether the tas
6352
6826
  console.log(`Recorded verdict on task ${updated.id}: did_the_job=${value === "yes"}`);
6353
6827
  })
6354
6828
  );
6355
- var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent by id");
6829
+ var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent (sky skills attach, or [[skills]] in agent.toml)");
6356
6830
  skills.command("upload <dir>").description("upload a skill directory (SKILL.md at its root) as a new skill").option("--name <name>", "display name (default: the directory's name)").option("--json", "print the stored skill as JSON").action(
6357
6831
  run(async (dir, opts) => {
6358
6832
  const { skill } = await uploadSkill(dir, opts.name);
@@ -6360,7 +6834,7 @@ skills.command("upload <dir>").description("upload a skill directory (SKILL.md a
6360
6834
  console.log(JSON.stringify(skill));
6361
6835
  return;
6362
6836
  }
6363
- console.log(`Uploaded ${skill.id} (${skill.displayName ?? dir}). Attach it in agent.toml:`);
6837
+ console.log(`Uploaded ${skill.id} (${skill.displayName ?? dir}). Attach it: sky skills attach ${skill.id} --agent <id>, or in agent.toml:`);
6364
6838
  console.log(` [[skills]]
6365
6839
  type = "skill"
6366
6840
  skillId = "${skill.id}"`);
@@ -6373,6 +6847,18 @@ skills.command("list").description("the workspace's stored skills").action(
6373
6847
  for (const skill of list) console.log(`${skill.id} ${skill.displayName ?? ""}`);
6374
6848
  })
6375
6849
  );
6850
+ for (const [verb, blurb] of [
6851
+ ["attach", "add a stored skill to an agent; its new sessions load it"],
6852
+ ["detach", "remove a skill from an agent; its new sessions no longer load it"]
6853
+ ]) {
6854
+ skills.command(`${verb} <skill>`).description(blurb).requiredOption("--agent <id>", "the agent").action(
6855
+ run(async (skillId, opts) => {
6856
+ const outcome = await setAgentSkill(opts.agent, skillId, verb);
6857
+ if (outcome === "unchanged") console.log(`${skillId} is ${verb === "attach" ? "already" : "not"} attached to ${opts.agent}`);
6858
+ else console.log(`${verb === "attach" ? "Attached" : "Detached"} ${skillId} ${verb === "attach" ? "to" : "from"} ${opts.agent} \u2014 new sessions ${verb === "attach" ? "load it" : "no longer load it"}; running sessions keep the skills they started with`);
6859
+ })
6860
+ );
6861
+ }
6376
6862
  skills.command("usage").description("view and miss counts per skill name in this workspace").option("--since <iso>", `start date (ISO 8601); default is the last 30 days`).option("--json", "print JSON").action(
6377
6863
  run(async (opts) => {
6378
6864
  const summary = await getSkillUsage(opts.since);
@@ -6416,6 +6902,56 @@ reflections.command("list").description("the workspace's proposals, newest first
6416
6902
  );
6417
6903
  reflections.command("approve <id>").description("write the proposal through the skill or memory store").action(run(async (id) => decideReflection(id, "approve")));
6418
6904
  reflections.command("reject <id>").description("decline the proposal; nothing is written").action(run(async (id) => decideReflection(id, "reject")));
6905
+ var library = program.command("library").description("your artifact Library on agentsky.dev: files and sites your agents saved or you uploaded");
6906
+ 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(
6907
+ run(async (opts) => {
6908
+ const page = await listLibrary({ q: opts.search, cursor: opts.cursor });
6909
+ if (opts.json) {
6910
+ console.log(JSON.stringify(page, null, 2));
6911
+ return;
6912
+ }
6913
+ if (!page.artifacts.length) console.log("no artifacts");
6914
+ for (const a of page.artifacts) console.log(renderLibraryArtifact(a));
6915
+ if (page.nextCursor) console.log(`more: --cursor ${page.nextCursor}`);
6916
+ })
6917
+ );
6918
+ library.command("upload <path>").description("save a file from this computer to your Library").option("--workspace <key>", "place it in this workspace").action(
6919
+ run(async (path, opts) => {
6920
+ const bytes = readFileSync18(path);
6921
+ const { artifact: artifact2 } = await uploadToLibrary({ bytes, name: basename9(path), mediaType: "application/octet-stream" }, `sky-upload-${crypto.randomUUID()}`, opts.workspace);
6922
+ console.log(renderLibraryArtifact(artifact2));
6923
+ })
6924
+ );
6925
+ library.command("show <id>").description("one artifact and its saved versions").action(run(async (id) => {
6926
+ console.log(JSON.stringify(await getLibraryArtifact(id), null, 2));
6927
+ }));
6928
+ library.command("delete <id>").description("delete your artifact with all its versions").action(
6929
+ run(async (id) => {
6930
+ const { currentVersion } = await getLibraryArtifact(id);
6931
+ await deleteLibraryArtifact(id, currentVersion ?? 0);
6932
+ console.log(`deleted ${id}`);
6933
+ })
6934
+ );
6935
+ for (const [verb, enabled, description] of [
6936
+ ["share", true, "turn on the artifact's public link"],
6937
+ ["unshare", false, "turn off the artifact's public link"]
6938
+ ]) {
6939
+ library.command(`${verb} <id>`).description(description).action(run(async (id) => {
6940
+ const state = await shareLibraryArtifact(id, enabled);
6941
+ console.log(state.url ?? `${verb}: ${id}`);
6942
+ }));
6943
+ }
6944
+ 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) => {
6945
+ await placeLibraryArtifact(id, workspace2 === "me" ? null : workspace2);
6946
+ console.log(`placed ${id}`);
6947
+ }));
6948
+ 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(
6949
+ run(async (id, out, opts) => {
6950
+ const { bytes } = await downloadLibraryArtifact(id, opts.version ? Number(opts.version) : void 0);
6951
+ writeFileSync11(out, bytes);
6952
+ console.log(`wrote ${bytes.length} bytes to ${out}`);
6953
+ })
6954
+ );
6419
6955
  var artifact = program.command("artifact").description("artifacts: publish a task's files as a named, shareable result");
6420
6956
  artifact.command("list").description("the workspace's artifacts, or one task's with --task").option("--task <task>", "only this task's artifacts").action(
6421
6957
  run(async (opts) => {
@@ -6433,7 +6969,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
6433
6969
  run(async (id, path, opts) => {
6434
6970
  const bytes = await readArtifactFile(id, path);
6435
6971
  if (opts.out) {
6436
- writeFileSync10(opts.out, bytes);
6972
+ writeFileSync11(opts.out, bytes);
6437
6973
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6438
6974
  } else process.stdout.write(bytes);
6439
6975
  })
@@ -6532,6 +7068,12 @@ for (const [verb, blurb] of [
6532
7068
  })
6533
7069
  );
6534
7070
  }
7071
+ routine.command("rm <id>").description("delete a routine: it never runs again; the sessions it started are kept").action(
7072
+ run(async (id) => {
7073
+ await api("DELETE", `/routines/${encodeURIComponent(id)}`);
7074
+ console.log(`Deleted routine ${id}. It will not run again; sessions it started are kept.`);
7075
+ })
7076
+ );
6535
7077
  routine.command("runs [id]").description("every fire attempt, newest first \u2014 of one routine, or all of them").option("--failed", "only runs that failed to start").option("--limit <n>", "how many (default 20)").action(
6536
7078
  run(async (id, opts) => {
6537
7079
  const params = new URLSearchParams({ limit: opts.limit ?? "20" });
@@ -6683,6 +7225,19 @@ program.command("config").description("show the CLI config location and current
6683
7225
  console.log(` token: ${config.token ? `${config.token.slice(0, 10)}\u2026` : "(none)"}`);
6684
7226
  console.log(` universe: ${config.currentUniverse ?? "(default)"}`);
6685
7227
  });
7228
+ var link = program.command("link").description("a session someone shared with you by a guest link: send to it and stop it");
7229
+ 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(
7230
+ run(async (ref, words, opts) => {
7231
+ await sendViaLink(ref, words.join(" "), opts.idempotencyKey ?? `sky-${crypto.randomUUID()}`);
7232
+ console.log("Sent. Read the reply with GET /links/<token>/events.");
7233
+ })
7234
+ );
7235
+ link.command("stop <link>").description("stop the turn running on the session a guest link opens").action(
7236
+ run(async (ref) => {
7237
+ await stopViaLink(ref);
7238
+ console.log("Stopped.");
7239
+ })
7240
+ );
6686
7241
  var billing = program.command("billing").description("your balance, auto top-up, usage across sessions and API keys, and buying credit");
6687
7242
  billing.command("show").description("balance, whether a card is saved, and auto top-up").action(
6688
7243
  run(async () => {
@@ -6709,6 +7264,11 @@ billing.command("topup <dollars>").description("buy credit with your saved card:
6709
7264
  console.log(renderTopup(result));
6710
7265
  })
6711
7266
  );
7267
+ billing.command("claim <link>").description("claim a credit invite link (agentsky.dev/i/<code>, or just the code) for your own balance").action(
7268
+ run(async (link2) => {
7269
+ console.log(renderCreditInviteClaim(await claimCreditInvite(link2)));
7270
+ })
7271
+ );
6712
7272
  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(
6713
7273
  run(async (opts) => {
6714
7274
  const body = autoTopupBody(opts);
@@ -6731,13 +7291,42 @@ usageOptions(
6731
7291
  run(async (opts) => {
6732
7292
  const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
6733
7293
  if (opts.out) {
6734
- writeFileSync10(opts.out, bytes);
7294
+ writeFileSync11(opts.out, bytes);
6735
7295
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6736
7296
  } else {
6737
7297
  process.stdout.write(bytes);
6738
7298
  }
6739
7299
  })
6740
7300
  );
7301
+ 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(
7302
+ run(async (opts) => {
7303
+ const page = await readRequestUsage(opts);
7304
+ console.log(opts.json ? JSON.stringify(page, null, 2) : renderRequestUsage(page));
7305
+ })
7306
+ );
7307
+ var codingPlan = billing.command("coding-plan").description("the coding plan: monthly credit on your saved card, API gateway calls billed at cost");
7308
+ codingPlan.command("show").description("whether you are a member, the monthly amount, and whether it renews").action(run(async () => console.log(renderCodingPlan(await readCodingPlan()))));
7309
+ 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(
7310
+ run(async (dollars2, opts) => {
7311
+ console.log(renderCodingPlan(await joinCodingPlan(dollars2, opts.requestId ?? `sky-${crypto.randomUUID()}`)));
7312
+ })
7313
+ );
7314
+ codingPlan.command("cancel").description("stop the plan renewing; the paid month stays").action(run(async () => console.log(renderCodingPlan(await cancelCodingPlan()))));
7315
+ var referralLinks = billing.command("referral-links").description("customer discount links for the API gateway (accounts on a discount plan)");
7316
+ referralLinks.command("list").description("your links, their use, and what referred customers earned you").action(run(async () => console.log(renderReferralLinks(await readReferralLinks()))));
7317
+ 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(
7318
+ run(async (opts) => {
7319
+ const { url, email_status: emailStatus } = await createReferralLink(opts);
7320
+ console.log(emailStatus ? `${url}
7321
+ email: ${emailStatus}` : url);
7322
+ })
7323
+ );
7324
+ referralLinks.command("disable <id>").description("stop a link being redeemed; customers already enrolled keep their price").action(
7325
+ run(async (id) => {
7326
+ await disableReferralLink(id);
7327
+ console.log(`Disabled ${id}`);
7328
+ })
7329
+ );
6741
7330
  var keys = program.command("keys").description("your own API keys: list, create, revoke, restore (admin scope)");
6742
7331
  keys.command("list").action(
6743
7332
  run(async () => {
@@ -7058,6 +7647,48 @@ vendor.command("usage <id>").description("the agents and tasks that used this ve
7058
7647
  for (const t of tasks) console.log(` task ${t.session_id} ${t.title ?? "(untitled)"} ${t.last_used_at.slice(0, 10)}`);
7059
7648
  })
7060
7649
  );
7650
+ 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");
7651
+ 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(
7652
+ run(async (opts) => {
7653
+ const macs = await listMacs();
7654
+ if (opts.json) {
7655
+ console.log(JSON.stringify(macs, null, 2));
7656
+ return;
7657
+ }
7658
+ if (!macs.length) console.log("no Macs connected; sign in to the AgentSky Mac app");
7659
+ for (const entry2 of macs) console.log(renderMac(entry2));
7660
+ })
7661
+ );
7662
+ 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(
7663
+ run(async (machine, kind, opts) => {
7664
+ const asked = await askMac(machine, kind, opts.reason);
7665
+ const answer = opts.wait ? await waitForAnswer(asked.reach_request.id, 240) : asked.reach_request;
7666
+ if (opts.json) {
7667
+ console.log(JSON.stringify({ reach_request: answer, mcp_url: asked.mcp_url }, null, 2));
7668
+ return;
7669
+ }
7670
+ console.log(renderReachRequest(answer));
7671
+ console.log(`mcp_url: ${asked.mcp_url}`);
7672
+ 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}");
7673
+ else if (!["pending", "asking"].includes(answer.status)) process.exitCode = 1;
7674
+ })
7675
+ );
7676
+ 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(
7677
+ run(async (id, opts) => {
7678
+ const answer = await readReachRequest(id, Number(opts.wait) || 0);
7679
+ if (opts.json) {
7680
+ console.log(JSON.stringify(answer, null, 2));
7681
+ return;
7682
+ }
7683
+ console.log(renderReachRequest(answer));
7684
+ })
7685
+ );
7686
+ 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(
7687
+ run(async (mount) => {
7688
+ await takeBack(mount);
7689
+ console.log(`took back ${mount}`);
7690
+ })
7691
+ );
7061
7692
  var browserProfile = program.command("browser-profile").description("saved-login profiles: the sites your agents stay signed in to");
7062
7693
  browserProfile.command("list").description("your workspace's browser profiles: the sites your agents stay signed in to").option("--json", "print as JSON").action(
7063
7694
  run(async (opts) => {
@@ -7121,10 +7752,16 @@ notifications.command("get").description("show the current notification preferen
7121
7752
  console.log(JSON.stringify(prefs, null, 2));
7122
7753
  return;
7123
7754
  }
7124
- console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done}`);
7755
+ console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done} marketing=${prefs.email.marketing}`);
7125
7756
  console.log(`push: needs_you=${prefs.push.needs_you} done=${prefs.push.done}`);
7126
7757
  })
7127
7758
  );
7759
+ notifications.command("unsubscribe-marketing").description("stop AgentSky marketing email to your address; login and account email continue").action(
7760
+ run(async () => {
7761
+ const { marketing } = await unsubscribeMarketing();
7762
+ console.log(marketing ? "Marketing email is still on." : "Unsubscribed from AgentSky marketing email.");
7763
+ })
7764
+ );
7128
7765
  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(
7129
7766
  run(async (opts) => {
7130
7767
  const parseBool = (v) => v === void 0 ? void 0 : v === "true" ? true : v === "false" ? false : (() => {
@@ -7193,6 +7830,85 @@ help.command("get <slug>").description("read a help article by its slug").option
7193
7830
  console.log(article.body);
7194
7831
  })
7195
7832
  );
7833
+ program.command("api <method> [path...]").description("call any AgentSky API operation (GET|POST|PUT|PATCH|DELETE <path>), or `sky api search <words\u2026>` to find one").option("-d, --data <json>", "JSON body: inline, @file, or - for stdin").option("-q, --query <key=value>", "query parameter (repeatable)", (v, acc) => [...acc, v], []).option("--workspace <slug>", "send the request in this workspace (X-Universe)").option("--idempotency-key <key>", "retry-safe key for a POST").option("-o, --output <file>", "save the body to a file (for downloads)").action(
7834
+ run(async (method, rest, opts) => {
7835
+ if (method.toLowerCase() === "search") return searchApi(rest);
7836
+ if (rest.length !== 1) throw new Error("usage: sky api <METHOD> <path> (one path), or sky api search <words\u2026>");
7837
+ return callApi(method, rest[0], opts);
7838
+ })
7839
+ );
7840
+ agent.command("edit <id>").description("change an agent's name, engine, model or thinking level (as its Library folder does)").option("--name <name>", "new name").option("--engine <agentType>", "engine, e.g. CLAUDE_CODE, CODEX, HERMES").option("--model <llm>", "model id, e.g. anthropic/claude-sonnet-4-5").option("--thinking <effort>", 'reasoning effort, or "default" to clear the pin').action(
7841
+ run(async (id, opts) => {
7842
+ const body = {
7843
+ ...opts.name ? { displayName: opts.name } : {},
7844
+ ...opts.engine ? { agentType: opts.engine } : {},
7845
+ ...opts.model ? { llm: opts.model } : {},
7846
+ ...opts.thinking ? { reasoningEffort: opts.thinking === "default" ? null : opts.thinking } : {}
7847
+ };
7848
+ if (!Object.keys(body).length) throw new Error("pass at least one of --name, --engine, --model, --thinking");
7849
+ const { agent: a } = await api("PATCH", `/agents/${encodeURIComponent(id)}`, body);
7850
+ console.log(`${a.id} \u2014 ${a.displayName} \xB7 ${a.agentType} \xB7 ${a.llm}${a.reasoningEffort ? ` \xB7 ${a.reasoningEffort}` : ""}`);
7851
+ })
7852
+ );
7853
+ skills.command("revisions <skillId>").description("a skill's saved versions, newest first").option("--json", "print as JSON").action(
7854
+ run(async (skillId, opts) => {
7855
+ const page = await api("GET", `/skills/${encodeURIComponent(skillId)}/revisions`);
7856
+ const rows = page.revisions ?? [];
7857
+ if (opts.json) return void console.log(JSON.stringify(rows, null, 2));
7858
+ if (!rows.length) return void console.log("no versions");
7859
+ for (const r of rows) console.log(`${r.id} ${r.createdAt ?? r.created_at ?? ""}`);
7860
+ })
7861
+ );
7862
+ skills.command("cat <skillId> <revisionId> <path>").description("print one file (e.g. SKILL.md) of a skill version").action(
7863
+ run(async (skillId, revisionId, path) => {
7864
+ const res = await apiRaw("GET", `/skills/${encodeURIComponent(skillId)}/revisions/${encodeURIComponent(revisionId)}/content?path=${encodeURIComponent(path)}`);
7865
+ if (!res.ok) throw new ApiError(res.status, "http_error", await res.text() || `${res.status}`);
7866
+ process.stdout.write(await res.text());
7867
+ })
7868
+ );
7869
+ skills.command("rm <skillId>").description("delete a skill from the workspace").action(
7870
+ run(async (skillId) => {
7871
+ await api("DELETE", `/skills/${encodeURIComponent(skillId)}`);
7872
+ console.log(`Deleted ${skillId}`);
7873
+ })
7874
+ );
7875
+ var notes = program.command("notes").description("memory notes in the Library's memory stores: list, read, write (every save keeps a version)");
7876
+ notes.command("stores").description("the workspace's memory stores").action(
7877
+ run(async () => {
7878
+ const { memory_stores: stores } = await api("GET", "/memory_stores?limit=100");
7879
+ if (!stores.length) return void console.log("no memory stores");
7880
+ for (const st of stores) console.log(`${st.id} ${st.name}`);
7881
+ })
7882
+ );
7883
+ notes.command("list <storeId> [folder]").description("the notes in a store, optionally under a folder ending in /").action(
7884
+ run(async (storeId, folder) => {
7885
+ const q = folder ? `&path_prefix=${encodeURIComponent(folder)}` : "";
7886
+ const { memories } = await api("GET", `/memory_stores/${encodeURIComponent(storeId)}/memories?limit=100${q}`);
7887
+ if (!memories.length) return void console.log("no notes");
7888
+ for (const n of memories) console.log(`${n.id} ${n.path}`);
7889
+ })
7890
+ );
7891
+ notes.command("read <storeId> <noteId>").description("print one note").action(
7892
+ run(async (storeId, noteId) => {
7893
+ const { memory: memory2 } = await api("GET", `/memory_stores/${encodeURIComponent(storeId)}/memories/${encodeURIComponent(noteId)}`);
7894
+ process.stdout.write(memory2.content.endsWith("\n") ? memory2.content : `${memory2.content}
7895
+ `);
7896
+ })
7897
+ );
7898
+ notes.command("save <storeId> <file>").description("write a note from a local file: --path creates one, --note saves a new version of one").option("--path <path>", "path of a new note, e.g. /projects/launch.md").option("--note <noteId>", "the note to update").action(
7899
+ run(async (storeId, file, opts) => {
7900
+ if (!opts.path === !opts.note) throw new Error("pass exactly one of --path (new note) or --note (update)");
7901
+ const content = readFileSync18(file, "utf8");
7902
+ const target = `/memory_stores/${encodeURIComponent(storeId)}/memories${opts.note ? `/${encodeURIComponent(opts.note)}` : ""}`;
7903
+ const { memory: memory2 } = await api("POST", target, { content, ...opts.path ? { path: opts.path } : {} });
7904
+ console.log(`Saved ${memory2.path} (${memory2.id})`);
7905
+ })
7906
+ );
7907
+ workspace.command("context").description("what every session in the workspace loads at start: AGENTS.md, MEMORY.md, indexes, your member page").action(
7908
+ run(async () => {
7909
+ console.log(JSON.stringify(await api("GET", "/workspace-context"), null, 2));
7910
+ })
7911
+ );
7196
7912
  var vault = program.command("vault").description("vaults: credentials a session is granted when it starts (values write-only)");
7197
7913
  vault.command("list").description("the workspace's vaults").action(
7198
7914
  run(async () => {
@@ -7282,7 +7998,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
7282
7998
  }));
7283
7999
  var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
7284
8000
  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) => {
7285
- const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-PDIOL2TC.js");
8001
+ const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-CFHCOGKB.js");
7286
8002
  if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
7287
8003
  const scopes = parseContentScopes(opts.capture);
7288
8004
  const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
@@ -7317,7 +8033,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
7317
8033
  }));
7318
8034
  traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
7319
8035
  try {
7320
- const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-PDIOL2TC.js");
8036
+ const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-CFHCOGKB.js");
7321
8037
  const chunks = [];
7322
8038
  for await (const chunk of process.stdin) chunks.push(chunk);
7323
8039
  const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
@@ -7330,14 +8046,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
7330
8046
  }
7331
8047
  });
7332
8048
  traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
7333
- const { readState, flush, grantSender } = await import("./trace-PDIOL2TC.js");
8049
+ const { readState, flush, grantSender } = await import("./trace-CFHCOGKB.js");
7334
8050
  const state = readState();
7335
8051
  if (!state) throw new Error("no trace is running here");
7336
8052
  const outcome = await flush(grantSender(state), { attempts: 4 });
7337
8053
  console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
7338
8054
  }));
7339
8055
  traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
7340
- const { readState, spoolStatus } = await import("./trace-PDIOL2TC.js");
8056
+ const { readState, spoolStatus } = await import("./trace-CFHCOGKB.js");
7341
8057
  const state = readState();
7342
8058
  if (!state) {
7343
8059
  console.log("No trace is running here.");
@@ -7347,7 +8063,7 @@ traceCmd.command("status").description("the running trace and its spool").action
7347
8063
  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`);
7348
8064
  }));
7349
8065
  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) => {
7350
- const { readState } = await import("./trace-PDIOL2TC.js");
8066
+ const { readState } = await import("./trace-CFHCOGKB.js");
7351
8067
  const ref = id ?? readState()?.trace_id;
7352
8068
  if (!ref) throw new Error("name a trace, or start one here");
7353
8069
  const bundle = await api("GET", `/traces/${ref}`);
@@ -7374,7 +8090,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
7374
8090
  for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
7375
8091
  }));
7376
8092
  traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
7377
- const { readState, record, flush, grantSender, clearState } = await import("./trace-PDIOL2TC.js");
8093
+ const { readState, record, flush, grantSender, clearState } = await import("./trace-CFHCOGKB.js");
7378
8094
  const state = readState();
7379
8095
  if (!state) throw new Error("no trace is running here");
7380
8096
  if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
@@ -7384,7 +8100,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
7384
8100
  console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
7385
8101
  }));
7386
8102
  traceCmd.command("sync [payload]").description("upload this machine's agent sessions (Claude Code, Codex, Cursor, Gemini CLI and every other agent `--agents` lists) to the matching workspace's trace library, redacted here first").option("--enable", "turn sync on for this machine").option("--disable", "turn sync off; nothing more is uploaded").option("--route <match=workspace>", "send sessions whose folder path contains <match> to <workspace> (repeatable; replaces the saved rules)", collect).option("--default <workspace>", "where a session no rule matches goes").option("--keep-local", "keep sessions no rule matches on this machine (the default)").option("--session <ref>", "only this session: its id or transcript path (repeatable)", collect).option("--import <export>", "add a downloaded cloud export (claude.ai, ChatGPT, Gemini, Grok, DeepSeek \u2026: its .zip, folder or file) so sync uploads it (repeatable)", collect).option("--agents", "list the agents sync reads and which are on this machine").option("--full", "re-read every session, not only files that changed").option("--limit <n>", "at most this many changed sessions this run", (v) => Number.parseInt(v, 10)).option("--dry-run", "show what would be uploaded without uploading").option("--hook", "run from a Claude Code SessionEnd hook or Codex notify: upload that session in the background and return at once").option("--install-schedule", "also sweep every 15 minutes (macOS launchd)").option("--status", "what sync has done on this machine").option("--quiet", "print only failures").action(async (payload, opts) => {
7387
- const lib = await import("./trace-library-TA3QUBHJ.js");
8103
+ const lib = await import("./trace-library-2VHTJCOY.js");
7388
8104
  if (opts.hook) {
7389
8105
  try {
7390
8106
  let raw = payload ?? "";
@@ -7456,11 +8172,11 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
7456
8172
  console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
7457
8173
  }
7458
8174
  if (opts.status) {
7459
- const { readFileSync: readFileSync18 } = await import("fs");
8175
+ const { readFileSync: readFileSync19 } = await import("fs");
7460
8176
  const { join: join19 } = await import("path");
7461
8177
  let state = { sessions: {} };
7462
8178
  try {
7463
- state = JSON.parse(readFileSync18(join19(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
8179
+ state = JSON.parse(readFileSync19(join19(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
7464
8180
  } catch {
7465
8181
  }
7466
8182
  const counts = /* @__PURE__ */ new Map();
@@ -7493,7 +8209,7 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
7493
8209
  })();
7494
8210
  });
7495
8211
  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) => {
7496
- const { libraryRequest } = await import("./trace-library-TA3QUBHJ.js");
8212
+ const { libraryRequest } = await import("./trace-library-2VHTJCOY.js");
7497
8213
  const workspace2 = opts.workspace ?? currentUniverse();
7498
8214
  if (!workspace2) throw new Error("name a workspace with --workspace");
7499
8215
  const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
@@ -7510,7 +8226,7 @@ traceCmd.command("search <query...>").description("search every session in a wor
7510
8226
  if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
7511
8227
  }));
7512
8228
  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) => {
7513
- const { libraryRequest } = await import("./trace-library-TA3QUBHJ.js");
8229
+ const { libraryRequest } = await import("./trace-library-2VHTJCOY.js");
7514
8230
  const workspace2 = opts.workspace ?? currentUniverse();
7515
8231
  if (!workspace2) throw new Error("name a workspace with --workspace");
7516
8232
  const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
@@ -7626,6 +8342,140 @@ thread.command("list").description("your workspace threads, most recently update
7626
8342
  for (const t of threads) console.log(renderThreadSummary(t));
7627
8343
  })
7628
8344
  );
8345
+ 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(
8346
+ run(async (text, opts) => {
8347
+ const compared = (opts.vs ?? []).map((v) => {
8348
+ const [agentType, llm] = v.split(":");
8349
+ if (!agentType) throw new ApiError(400, "invalid_request", `--vs must be type[:model], got "${v}"`);
8350
+ return { agentType, ...llm ? { llm } : {} };
8351
+ });
8352
+ const result = await startThread({
8353
+ requestId: `sky-thread-${crypto.randomUUID()}`,
8354
+ agentType: opts.agent,
8355
+ text,
8356
+ ...opts.llm ? { llm: opts.llm } : {},
8357
+ ...opts.effort ? { reasoningEffort: opts.effort } : {},
8358
+ ...compared.length ? { compare: compared } : {},
8359
+ ...opts.savedAgent ? { agentId: opts.savedAgent } : {},
8360
+ ...opts.workspace ? { workspace: opts.workspace } : {},
8361
+ ...opts.plan ? { interactionMode: "plan" } : {}
8362
+ });
8363
+ if (opts.json) {
8364
+ console.log(JSON.stringify(result, null, 2));
8365
+ return;
8366
+ }
8367
+ console.log(`started ${result.playgroundId}`);
8368
+ for (const col of result.columns) console.log(` agent ${col.columnId} session ${col.sessionId}`);
8369
+ })
8370
+ );
8371
+ thread.command("show <id>").description("one thread with every agent and the session each one runs in").option("--json", "print as JSON").action(
8372
+ run(async (id, opts) => {
8373
+ const detail = await getThread(id);
8374
+ if (opts.json) {
8375
+ console.log(JSON.stringify(detail, null, 2));
8376
+ return;
8377
+ }
8378
+ console.log(`thread ${detail.id} ${detail.title}`);
8379
+ 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 ?? ""}`);
8380
+ })
8381
+ );
8382
+ thread.command("rename <id> <title>").description("rename a thread").action(run(async (id, title) => {
8383
+ await organizeThread(id, { title });
8384
+ console.log(`renamed ${id}`);
8385
+ }));
8386
+ for (const [verb, change, description] of [
8387
+ ["pin", { pinned: true }, "pin a thread to the top of the rail"],
8388
+ ["unpin", { pinned: false }, "unpin a thread"],
8389
+ ["archive", { archived: true }, "put a thread away"],
8390
+ ["unarchive", { archived: false }, "bring an archived thread back"],
8391
+ ["mark-read", { seen: true }, "mark a thread read"],
8392
+ ["mark-unread", { seen: false }, "mark a thread unread"]
8393
+ ]) {
8394
+ thread.command(`${verb} <id>`).description(description).action(run(async (id) => {
8395
+ await organizeThread(id, change);
8396
+ console.log(`${verb}: ${id}`);
8397
+ }));
8398
+ }
8399
+ thread.command("delete <id>").description("delete a thread with its sessions").action(run(async (id) => {
8400
+ await deleteThread(id);
8401
+ console.log(`deleted ${id}`);
8402
+ }));
8403
+ 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(
8404
+ run(async (id, text, opts) => {
8405
+ const { results } = await sendThreadMessage(id, { text, deliveryId: crypto.randomUUID(), ...opts.agent?.length ? { columnIds: opts.agent } : {} });
8406
+ if (opts.json) {
8407
+ console.log(JSON.stringify(results, null, 2));
8408
+ return;
8409
+ }
8410
+ for (const [column, outcome] of Object.entries(results)) console.log(` ${column} ${outcome.ok ? "sent" : `not sent: ${outcome.error ?? "refused"}`}`);
8411
+ })
8412
+ );
8413
+ 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(
8414
+ run(async (id, opts) => {
8415
+ const { columns } = await addThreadAgent(id, { requestId: `sky-agent-${crypto.randomUUID()}`, agentType: opts.agent, ...opts.llm ? { llm: opts.llm } : {}, ...opts.effort ? { reasoningEffort: opts.effort } : {} });
8416
+ for (const col of columns) console.log(`added agent ${col.columnId} session ${col.sessionId}`);
8417
+ })
8418
+ );
8419
+ 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) => {
8420
+ await removeThreadAgent(id, columnId);
8421
+ console.log(`removed ${columnId}`);
8422
+ }));
8423
+ 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) => {
8424
+ await interruptThread(id, opts.agent);
8425
+ console.log(`stopped ${id}`);
8426
+ }));
8427
+ 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(
8428
+ run(async (id, opts) => {
8429
+ const [agentType, llm] = (opts.to ?? "").split(":");
8430
+ const { playgroundId } = await forkThread(id, {
8431
+ requestId: `sky-fork-${crypto.randomUUID()}`,
8432
+ ...opts.agent ? { columnId: opts.agent } : {},
8433
+ ...opts.at ? { messageId: opts.at } : {},
8434
+ ...agentType ? { target: { agentType, ...llm ? { llm } : {} } } : {}
8435
+ });
8436
+ console.log(`forked into ${playgroundId}`);
8437
+ })
8438
+ );
8439
+ 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(
8440
+ run(async (id, opts) => {
8441
+ if (!opts.llm && !opts.effort) throw new ApiError(400, "invalid_request", "Provide --llm or --effort.");
8442
+ const result = await changeThreadModel(id, { ...opts.llm ? { llm: opts.llm } : {}, ...opts.effort ? { reasoningEffort: opts.effort === "default" ? null : opts.effort } : {} }, opts.agent);
8443
+ console.log(`now ${result.llm}${result.reasoningEffort ? ` (${result.reasoningEffort})` : ""}`);
8444
+ })
8445
+ );
8446
+ 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(
8447
+ run(async (query, opts) => {
8448
+ const threads = opts.all ? "all" : opts.archived ? "archived" : void 0;
8449
+ console.log(JSON.stringify(await searchThreads(query, { ...threads ? { threads } : {}, ...opts.cursor ? { cursor: opts.cursor } : {} }), null, 2));
8450
+ })
8451
+ );
8452
+ thread.command("options").description("what a new thread can start on: agents, models, thinking levels and grantable tools").action(run(async () => {
8453
+ console.log(JSON.stringify(await threadOptions(), null, 2));
8454
+ }));
8455
+ thread.command("join <sessionId>").description("open a teammate's thread, or one shared with you, as the session link does").action(
8456
+ run(async (sessionId) => {
8457
+ const opened = await joinThread(sessionId);
8458
+ console.log(opened.readOnly ? `read-only: session ${opened.sessionId}` : `opened ${opened.playgroundId}`);
8459
+ })
8460
+ );
8461
+ var project = program.command("project").description("the GitHub repositories a new thread can start on");
8462
+ project.command("list").description("repositories a new thread can start on, and the one you chose last").option("--json", "print as JSON").action(
8463
+ run(async (opts) => {
8464
+ const listed = await listProjects();
8465
+ if (opts.json) {
8466
+ console.log(JSON.stringify(listed, null, 2));
8467
+ return;
8468
+ }
8469
+ if (listed.unavailable) console.log(listed.unavailable.message);
8470
+ for (const repo of listed.repos) console.log(`${repo.fullName === listed.last?.repoFullName ? "* " : " "}${repo.fullName}`);
8471
+ })
8472
+ );
8473
+ 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(
8474
+ run(async (repo, opts) => {
8475
+ const { project: remembered } = await rememberProject(repo ? { repoFullName: repo, branch: opts.branch } : null);
8476
+ console.log(remembered ? `next thread starts on ${remembered.repoFullName}@${remembered.branch}` : "next thread starts on no repository");
8477
+ })
8478
+ );
7629
8479
  session.command("feedback <id>").description("read the thumbs-up/down feedback on this session's messages").option("--json", "print as JSON").action(
7630
8480
  run(async (id, opts) => {
7631
8481
  const result = await api(
@@ -7682,7 +8532,7 @@ session.command("code-review <id>").description("run the code-review assistant o
7682
8532
  }
7683
8533
  })
7684
8534
  );
7685
- session.command("snapshot <id>").description("read or revoke the public share link for a session").requiredOption("--thread <threadId>", "the thread/comparison that owns this session").option("--revoke", "revoke the snapshot link instead of reading it").option("--json", "print as JSON").action(
8535
+ session.command("snapshot <id>").usage("<id> --thread <threadId> [--revoke] [--json]").description("read or revoke the public share link for a session; --thread is required").requiredOption("--thread <threadId>", "(required) the thread/comparison that owns this session").option("--revoke", "revoke the snapshot link instead of reading it").option("--json", "print as JSON").action(
7686
8536
  run(async (id, opts) => {
7687
8537
  const path = `/sessions/${encodeURIComponent(id)}/snapshot?thread=${encodeURIComponent(opts.thread)}`;
7688
8538
  const result = await api(opts.revoke ? "DELETE" : "GET", path);