@agentsky/sky 2.15.0 → 2.16.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,12 @@ import {
4
4
  api,
5
5
  apiBytes,
6
6
  apiForm,
7
- downloadFile,
7
+ downloadFileTo,
8
8
  postEvents,
9
9
  resolveSessionRef,
10
10
  streamEvents,
11
11
  uploadFile
12
- } from "./chunk-Y3NTKJLA.js";
12
+ } from "./chunk-MQECNHD2.js";
13
13
  import {
14
14
  CLI_VERSION,
15
15
  USER_AGENT,
@@ -20,7 +20,7 @@ import {
20
20
  currentUniverse,
21
21
  loadConfig,
22
22
  saveConfig
23
- } from "./chunk-R5FKQQMG.js";
23
+ } from "./chunk-5JHQH7WH.js";
24
24
  import {
25
25
  AGENT_TYPES,
26
26
  MODEL_SUBSCRIPTION_PLAN_PHRASE,
@@ -44,14 +44,118 @@ import {
44
44
  import "./chunk-MLKGABMK.js";
45
45
 
46
46
  // src/index.ts
47
- import { existsSync as existsSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
48
- import { basename as basename6, resolve as resolve7 } from "path";
49
- import { createInterface as createInterface5 } from "readline";
47
+ import { existsSync as existsSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
48
+ import { basename as basename8, resolve as resolve9 } from "path";
49
+ import { createInterface as createInterface6 } from "readline";
50
50
  import { Command } from "commander";
51
51
 
52
52
  // src/files.ts
53
- import { readFileSync } from "fs";
54
- import { basename, extname } from "path";
53
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
54
+ import { basename as basename2, extname } from "path";
55
+
56
+ // src/resumable-upload.ts
57
+ import { createHash } from "crypto";
58
+ import { mkdirSync, openSync, readFileSync, readSync, closeSync, rmSync, statSync, writeFileSync } from "fs";
59
+ import { basename, dirname, join, resolve } from "path";
60
+ var SINGLE_REQUEST_MAX_BYTES = 32 * 1024 * 1024;
61
+ var MAX_ATTEMPTS = 8;
62
+ function statePath(path, size, mtimeMs) {
63
+ const key = createHash("sha256").update(`${resolve(path)}\0${size}\0${mtimeMs}`).digest("hex").slice(0, 32);
64
+ return join(dirname(configPath()), "uploads", `${key}.json`);
65
+ }
66
+ function loadSession(file, expiresInSeconds) {
67
+ try {
68
+ const saved = JSON.parse(readFileSync(file, "utf8"));
69
+ if (saved.expiresInSeconds !== expiresInSeconds) return null;
70
+ if (Date.parse(saved.expires_at) - Date.now() < 3600 * 1e3) return null;
71
+ return saved;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ function acknowledged(res) {
77
+ const range = res.headers.get("range")?.match(/^bytes=0-(\d+)$/);
78
+ return range ? Number(range[1]) + 1 : 0;
79
+ }
80
+ async function probe(session2) {
81
+ const res = await fetch(session2.upload_url, {
82
+ method: "PUT",
83
+ headers: { "user-agent": USER_AGENT, "content-range": `bytes */${session2.size_bytes}` }
84
+ });
85
+ if (res.status === 200 || res.status === 201) return "done";
86
+ if (res.status === 308) return acknowledged(res);
87
+ if (res.status === 404 || res.status === 410) throw new ApiError(res.status, "upload_expired", "the upload session is gone");
88
+ throw new ApiError(res.status, "upload_failed", `upload status check failed: ${res.status} ${res.statusText}`);
89
+ }
90
+ async function sendChunk(session2, fd, offset) {
91
+ const length = Math.min(session2.chunk_size, session2.size_bytes - offset);
92
+ const chunk = Buffer.alloc(length);
93
+ let read = 0;
94
+ while (read < length) read += readSync(fd, chunk, read, length - read, offset + read);
95
+ const res = await fetch(session2.upload_url, {
96
+ method: "PUT",
97
+ headers: {
98
+ "user-agent": USER_AGENT,
99
+ "content-range": `bytes ${offset}-${offset + length - 1}/${session2.size_bytes}`
100
+ },
101
+ body: chunk
102
+ });
103
+ if (res.status === 200 || res.status === 201) return "done";
104
+ if (res.status === 308) return acknowledged(res);
105
+ if (res.status >= 500 || res.status === 429) throw new Error(`transient ${res.status}`);
106
+ throw new ApiError(res.status, "upload_failed", `chunk upload failed: ${res.status} ${(await res.text()).slice(0, 200)}`);
107
+ }
108
+ async function transfer(session2, path, onProgress) {
109
+ const fd = openSync(path, "r");
110
+ try {
111
+ let failures = 0;
112
+ let state = await probe(session2);
113
+ while (state !== "done") {
114
+ onProgress(state, session2.size_bytes);
115
+ try {
116
+ state = await sendChunk(session2, fd, state);
117
+ failures = 0;
118
+ } catch (error) {
119
+ if (error instanceof ApiError || ++failures >= MAX_ATTEMPTS) throw error;
120
+ await new Promise((done) => setTimeout(done, Math.min(3e4, 500 * 2 ** failures)));
121
+ state = await probe(session2);
122
+ }
123
+ }
124
+ onProgress(session2.size_bytes, session2.size_bytes);
125
+ } finally {
126
+ closeSync(fd);
127
+ }
128
+ }
129
+ async function uploadLargeFile(path, mediaType, expiresInSeconds, onProgress) {
130
+ const { size, mtimeMs } = statSync(path);
131
+ const stateFile = statePath(path, size, mtimeMs);
132
+ let session2 = loadSession(stateFile, expiresInSeconds);
133
+ if (session2) {
134
+ try {
135
+ await probe(session2);
136
+ } catch (error) {
137
+ if (!(error instanceof ApiError) || error.status !== 404 && error.status !== 410) throw error;
138
+ session2 = null;
139
+ }
140
+ }
141
+ if (!session2) {
142
+ session2 = await api("POST", "/files/uploads", {
143
+ filename: basename(path),
144
+ size_bytes: size,
145
+ mime_type: mediaType,
146
+ ...expiresInSeconds ? { expires_in_seconds: expiresInSeconds } : {}
147
+ });
148
+ mkdirSync(dirname(stateFile), { recursive: true });
149
+ const saved = { ...session2, path: resolve(path), mtimeMs, expiresInSeconds };
150
+ writeFileSync(stateFile, JSON.stringify(saved), { mode: 384 });
151
+ }
152
+ await transfer(session2, path, onProgress);
153
+ const file = await api("POST", `/files/uploads/${encodeURIComponent(session2.id)}/complete`);
154
+ rmSync(stateFile, { force: true });
155
+ return file;
156
+ }
157
+
158
+ // src/files.ts
55
159
  var MEDIA_TYPES = {
56
160
  ".png": "image/png",
57
161
  ".jpg": "image/jpeg",
@@ -71,20 +175,24 @@ var MEDIA_TYPES = {
71
175
  function mediaTypeFor(path) {
72
176
  return MEDIA_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
73
177
  }
74
- async function uploadLocalFile(path, expiresInSeconds) {
75
- const bytes = readFileSync(path);
76
- return uploadFile(new Uint8Array(bytes), basename(path), mediaTypeFor(path), expiresInSeconds);
178
+ async function uploadLocalFile(path, expiresInSeconds, onProgress = () => {
179
+ }) {
180
+ if (statSync2(path).size > SINGLE_REQUEST_MAX_BYTES) {
181
+ return uploadLargeFile(path, mediaTypeFor(path), expiresInSeconds, onProgress);
182
+ }
183
+ const bytes = readFileSync2(path);
184
+ return uploadFile(new Uint8Array(bytes), basename2(path), mediaTypeFor(path), expiresInSeconds);
77
185
  }
78
186
  function fileBlock(file) {
79
187
  const source = { type: "file", file_id: file.id };
80
188
  return file.mime_type.startsWith("image/") ? { type: "image", source } : { type: "document", source, title: file.filename };
81
189
  }
82
- function eventsFor(text, files2, system) {
190
+ function eventsFor(text, files2, system, interactionMode2) {
83
191
  const content = [];
84
192
  if (text.trim()) content.push({ type: "text", text });
85
193
  for (const file of files2) content.push(fileBlock(file));
86
194
  if (content.length === 0) throw new Error("nothing to send: pass text, --file or --image");
87
- const events = [{ type: "user.message", content }];
195
+ const events = [{ type: "user.message", content, ...interactionMode2 ? { interaction_mode: interactionMode2 } : {} }];
88
196
  if (system?.trim()) events.push({ type: "system.message", content: [{ type: "text", text: system }] });
89
197
  return events;
90
198
  }
@@ -286,8 +394,8 @@ function renderContextPage(page) {
286
394
  }
287
395
 
288
396
  // src/agent-file.ts
289
- import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
290
- import { dirname, resolve } from "path";
397
+ import { existsSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
398
+ import { dirname as dirname2, resolve as resolve2 } from "path";
291
399
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
292
400
  var fileInstructionSchema = external_exports.object({
293
401
  name: external_exports.string(),
@@ -319,7 +427,7 @@ var agentFileSchema = external_exports.object({
319
427
  memory_mb: external_exports.number().int().optional()
320
428
  });
321
429
  function readAgentFile(path) {
322
- const raw = readFileSync2(path, "utf8");
430
+ const raw = readFileSync3(path, "utf8");
323
431
  const parsed = agentFileSchema.safeParse(parseToml(raw));
324
432
  if (!parsed.success) {
325
433
  const issue = parsed.error.issues[0];
@@ -331,11 +439,11 @@ function readAgentFile(path) {
331
439
  `${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
332
440
  );
333
441
  }
334
- const base = dirname(resolve(path));
442
+ const base = dirname2(resolve2(path));
335
443
  const inline = (ref, inlineContent) => {
336
444
  if (inlineContent !== void 0) return inlineContent;
337
445
  if (!ref) return void 0;
338
- return readFileSync2(resolve(base, ref), "utf8");
446
+ return readFileSync3(resolve2(base, ref), "utf8");
339
447
  };
340
448
  const spec = {
341
449
  name: file.name,
@@ -361,14 +469,14 @@ function readAgentFile(path) {
361
469
  return { file, spec };
362
470
  }
363
471
  function stampId(path, id) {
364
- const doc = parseToml(readFileSync2(path, "utf8"));
472
+ const doc = parseToml(readFileSync3(path, "utf8"));
365
473
  doc.id = id;
366
474
  delete doc.slug;
367
- writeFileSync(path, stringifyToml(doc));
475
+ writeFileSync2(path, stringifyToml(doc));
368
476
  }
369
477
  function scaffoldAgentFile(path, name) {
370
478
  if (existsSync(path)) throw new Error(`${path} already exists`);
371
- writeFileSync(
479
+ writeFileSync2(
372
480
  path,
373
481
  `# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
374
482
  name = "${name}"
@@ -454,18 +562,18 @@ function renderCatalogAgent(agent2) {
454
562
 
455
563
  // src/clone.ts
456
564
  import { spawn } from "child_process";
457
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
565
+ import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
458
566
  import { homedir } from "os";
459
- import { basename as basename2, join } from "path";
567
+ import { basename as basename3, join as join2 } from "path";
460
568
  import { createInterface } from "readline";
461
569
  var PROMPT_CAP = 1e5;
462
570
  var HARNESSES = ["claude_code", "codex", "openclaw"];
463
571
  function claudeConfigRoot() {
464
- return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
572
+ return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir(), ".claude");
465
573
  }
466
574
  function readIfExists(path) {
467
575
  try {
468
- return readFileSync3(path, "utf8");
576
+ return readFileSync4(path, "utf8");
469
577
  } catch {
470
578
  return void 0;
471
579
  }
@@ -481,9 +589,9 @@ function readJson(path) {
481
589
  }
482
590
  function claudeModel(configRoot) {
483
591
  const candidates = [
484
- join(process.cwd(), ".claude", "settings.local.json"),
485
- join(process.cwd(), ".claude", "settings.json"),
486
- join(configRoot, "settings.json")
592
+ join2(process.cwd(), ".claude", "settings.local.json"),
593
+ join2(process.cwd(), ".claude", "settings.json"),
594
+ join2(configRoot, "settings.json")
487
595
  ];
488
596
  for (const path of candidates) {
489
597
  const model = readJson(path)?.model;
@@ -493,11 +601,11 @@ function claudeModel(configRoot) {
493
601
  }
494
602
  function claudeMcpServers() {
495
603
  const merged = {};
496
- const userConfig = readJson(join(homedir(), ".claude.json"));
604
+ const userConfig = readJson(join2(homedir(), ".claude.json"));
497
605
  Object.assign(merged, userConfig?.mcpServers ?? {});
498
606
  Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
499
607
  const projectConfig = readJson(
500
- join(process.cwd(), ".mcp.json")
608
+ join2(process.cwd(), ".mcp.json")
501
609
  );
502
610
  Object.assign(merged, projectConfig?.mcpServers ?? {});
503
611
  return merged;
@@ -514,13 +622,13 @@ function readClaudeCode() {
514
622
  const configRoot = claudeConfigRoot();
515
623
  const parts = [];
516
624
  const sources = [];
517
- const global = readIfExists(join(configRoot, "CLAUDE.md"));
625
+ const global = readIfExists(join2(configRoot, "CLAUDE.md"));
518
626
  if (global?.trim()) {
519
627
  parts.push({ label: "Global CLAUDE.md", content: global });
520
628
  sources.push("global CLAUDE.md");
521
629
  }
522
630
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
523
- const project = readIfExists(join(process.cwd(), name));
631
+ const project = readIfExists(join2(process.cwd(), name));
524
632
  if (project?.trim()) {
525
633
  parts.push({ label: `Project ${name}`, content: project });
526
634
  sources.push(`project ${name}`);
@@ -561,8 +669,8 @@ function detectHarness(explicit) {
561
669
  return explicit;
562
670
  }
563
671
  if (existsSync2(claudeConfigRoot())) return "claude_code";
564
- if (existsSync2(join(homedir(), ".codex"))) return "codex";
565
- if (existsSync2(join(homedir(), ".openclaw"))) return "openclaw";
672
+ if (existsSync2(join2(homedir(), ".codex"))) return "codex";
673
+ if (existsSync2(join2(homedir(), ".openclaw"))) return "openclaw";
566
674
  throw new Error(
567
675
  "no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
568
676
  );
@@ -574,14 +682,14 @@ function readLocal(harness2) {
574
682
  );
575
683
  }
576
684
  function linkPath(configRoot) {
577
- return join(configRoot, "agentsky-link.json");
685
+ return join2(configRoot, "agentsky-link.json");
578
686
  }
579
687
  function readLink(configRoot) {
580
688
  const link = readJson(linkPath(configRoot));
581
689
  return link?.agentId ? link : void 0;
582
690
  }
583
691
  function writeLink(configRoot, link) {
584
- writeFileSync2(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
692
+ writeFileSync3(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
585
693
  `);
586
694
  }
587
695
  var DEFAULT_VAULT_NAME = "default";
@@ -596,10 +704,10 @@ async function ensureDefaultVaultId() {
596
704
  const existing = await fetchDefaultVaultId();
597
705
  if (existing) return existing;
598
706
  try {
599
- const { vault } = await api("POST", "/vaults", {
707
+ const { vault: vault2 } = await api("POST", "/vaults", {
600
708
  display_name: DEFAULT_VAULT_NAME
601
709
  });
602
- return vault.id;
710
+ return vault2.id;
603
711
  } catch (error) {
604
712
  if (error instanceof ApiError && error.status === 409) {
605
713
  const raced = await fetchDefaultVaultId();
@@ -683,7 +791,7 @@ async function createClone(local, opts) {
683
791
  `model ${local.model ?? "(harness default)"}`,
684
792
  `${keptNames.length} MCP server${keptNames.length === 1 ? "" : "s"}${keptNames.length ? ` (${keptNames.join(", ")})` : ""}`
685
793
  ].join(" \xB7 ");
686
- console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename2(process.cwd())})`);
794
+ console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename3(process.cwd())})`);
687
795
  console.log(`Will clone: ${summary}`);
688
796
  if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
689
797
  if (local.droppedServers.length) {
@@ -820,22 +928,22 @@ async function clone(opts) {
820
928
  }
821
929
 
822
930
  // src/import-local.ts
823
- import { closeSync, existsSync as existsSync5, openSync, readdirSync as readdirSync2, readFileSync as readFileSync6, readSync, statSync as statSync3 } from "fs";
931
+ import { closeSync as closeSync2, existsSync as existsSync5, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, readSync as readSync2, statSync as statSync5 } from "fs";
824
932
  import { homedir as homedir4 } from "os";
825
- import { join as join4 } from "path";
826
- import { createHash } from "crypto";
933
+ import { join as join5 } from "path";
934
+ import { createHash as createHash2 } from "crypto";
827
935
 
828
936
  // src/import-files.ts
829
937
  import { isUtf8 } from "buffer";
830
- import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
938
+ import { existsSync as existsSync4, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
831
939
  import { homedir as homedir3 } from "os";
832
- import { dirname as dirname3, extname as extname2, isAbsolute, join as join3, relative as relative2, resolve as resolve3, sep as sep2, basename as basename4 } from "path";
940
+ import { dirname as dirname4, extname as extname2, isAbsolute, join as join4, relative as relative2, resolve as resolve4, sep as sep2, basename as basename5 } from "path";
833
941
 
834
942
  // src/import-workspace.ts
835
943
  import { spawnSync } from "child_process";
836
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, realpathSync, statSync } from "fs";
944
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
837
945
  import { homedir as homedir2 } from "os";
838
- import { basename as basename3, dirname as dirname2, join as join2, relative, sep } from "path";
946
+ import { basename as basename4, dirname as dirname3, join as join3, relative, sep } from "path";
839
947
  import { parse as parseToml2 } from "smol-toml";
840
948
  var MAX_TREE = 5e3;
841
949
  var SKILL_LINE = /Base directory for this skill: (\/[^\s"'\\<>]+)/g;
@@ -845,26 +953,26 @@ function git(cwd, ...args) {
845
953
  }
846
954
  var isFile = (path) => {
847
955
  try {
848
- return statSync(path).isFile();
956
+ return statSync3(path).isFile();
849
957
  } catch {
850
958
  return false;
851
959
  }
852
960
  };
853
961
  var isDir = (path) => {
854
962
  try {
855
- return statSync(path).isDirectory();
963
+ return statSync3(path).isDirectory();
856
964
  } catch {
857
965
  return false;
858
966
  }
859
967
  };
860
968
  function sharedInstructionPaths(home = homedir2()) {
861
- const rules = join2(home, ".agents", "rules");
969
+ const rules = join3(home, ".agents", "rules");
862
970
  let ruleFiles = [];
863
971
  try {
864
- ruleFiles = readdirSync(rules).filter((name) => name.endsWith(".md")).sort().map((name) => join2(rules, name));
972
+ ruleFiles = readdirSync(rules).filter((name) => name.endsWith(".md")).sort().map((name) => join3(rules, name));
865
973
  } catch {
866
974
  }
867
- return [join2(home, "AGENTS.md"), join2(home, "MEMORY.md"), ...ruleFiles].filter(isFile);
975
+ return [join3(home, "AGENTS.md"), join3(home, "MEMORY.md"), ...ruleFiles].filter(isFile);
868
976
  }
869
977
  function strings(value, out, depth = 0) {
870
978
  if (depth > 12) return;
@@ -873,18 +981,18 @@ function strings(value, out, depth = 0) {
873
981
  else if (value && typeof value === "object") for (const item of Object.values(value)) strings(item, out, depth + 1);
874
982
  }
875
983
  function skillRoots(cwd, home) {
876
- const claude = process.env.CLAUDE_CONFIG_DIR ?? join2(home, ".claude");
877
- const codex = process.env.CODEX_HOME ?? join2(home, ".codex");
984
+ const claude = process.env.CLAUDE_CONFIG_DIR ?? join3(home, ".claude");
985
+ const codex = process.env.CODEX_HOME ?? join3(home, ".codex");
878
986
  return [
879
- ...cwd ? [join2(cwd, ".claude", "skills"), join2(cwd, ".agents", "skills")] : [],
880
- join2(home, ".agents", "skills"),
881
- join2(claude, "skills"),
882
- join2(codex, "skills"),
883
- join2(home, ".hermes", "skills")
987
+ ...cwd ? [join3(cwd, ".claude", "skills"), join3(cwd, ".agents", "skills")] : [],
988
+ join3(home, ".agents", "skills"),
989
+ join3(claude, "skills"),
990
+ join3(codex, "skills"),
991
+ join3(home, ".hermes", "skills")
884
992
  ];
885
993
  }
886
994
  function pluginSkill(plugin, skill, home) {
887
- const base = join2(process.env.CLAUDE_CONFIG_DIR ?? join2(home, ".claude"), "plugins");
995
+ const base = join3(process.env.CLAUDE_CONFIG_DIR ?? join3(home, ".claude"), "plugins");
888
996
  const walk = (dir, depth) => {
889
997
  if (depth > 7) return null;
890
998
  let entries;
@@ -895,8 +1003,8 @@ function pluginSkill(plugin, skill, home) {
895
1003
  }
896
1004
  for (const entry2 of entries) {
897
1005
  if (!entry2.isDirectory() || entry2.name === "node_modules" || entry2.name === ".git") continue;
898
- const path = join2(dir, entry2.name);
899
- if (entry2.name === skill && basename3(dir) === "skills" && path.includes(`${sep}${plugin}${sep}`) && isFile(join2(path, "SKILL.md"))) return path;
1006
+ const path = join3(dir, entry2.name);
1007
+ if (entry2.name === skill && basename4(dir) === "skills" && path.includes(`${sep}${plugin}${sep}`) && isFile(join3(path, "SKILL.md"))) return path;
900
1008
  const found = walk(path, depth + 1);
901
1009
  if (found) return found;
902
1010
  }
@@ -908,16 +1016,16 @@ function findSkill(name, cwd, home) {
908
1016
  const [scope, skill] = name.includes(":") ? name.split(":", 2) : [null, name];
909
1017
  if (scope && skill) return pluginSkill(scope, skill, home);
910
1018
  for (const root of skillRoots(cwd, home)) {
911
- const direct = join2(root, name);
912
- if (isFile(join2(direct, "SKILL.md"))) return realpathSync(direct);
1019
+ const direct = join3(root, name);
1020
+ if (isFile(join3(direct, "SKILL.md"))) return realpathSync(direct);
913
1021
  }
914
1022
  return null;
915
1023
  }
916
1024
  function loadedSkills(records, touched, cwd, home = homedir2()) {
917
1025
  const dirs = /* @__PURE__ */ new Map();
918
1026
  const missing = /* @__PURE__ */ new Set();
919
- const add = (dir, name = basename3(dir)) => {
920
- if (!isFile(join2(dir, "SKILL.md"))) return false;
1027
+ const add = (dir, name = basename4(dir)) => {
1028
+ if (!isFile(join3(dir, "SKILL.md"))) return false;
921
1029
  const real = realpathSync(dir);
922
1030
  if (![...dirs.values()].includes(real)) dirs.set(name, real);
923
1031
  return true;
@@ -937,13 +1045,13 @@ function loadedSkills(records, touched, cwd, home = homedir2()) {
937
1045
  if (!dir || !add(dir, name)) missing.add(name);
938
1046
  }
939
1047
  }
940
- for (const path of touched) if (basename3(path) === "SKILL.md") add(dirname2(path));
1048
+ for (const path of touched) if (basename4(path) === "SKILL.md") add(dirname3(path));
941
1049
  for (const name of dirs.keys()) missing.delete(name);
942
1050
  return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
943
1051
  }
944
1052
  function readJson2(path) {
945
1053
  try {
946
- const value = JSON.parse(readFileSync4(path, "utf8"));
1054
+ const value = JSON.parse(readFileSync5(path, "utf8"));
947
1055
  return value && typeof value === "object" ? value : null;
948
1056
  } catch {
949
1057
  return null;
@@ -988,16 +1096,16 @@ function mcpServers(harness2, cwd, home = homedir2()) {
988
1096
  }
989
1097
  };
990
1098
  if (harness2 === "claude_code") {
991
- const config = readJson2(join2(process.env.CLAUDE_CONFIG_DIR ? dirname2(process.env.CLAUDE_CONFIG_DIR) : home, ".claude.json"));
1099
+ const config = readJson2(join3(process.env.CLAUDE_CONFIG_DIR ? dirname3(process.env.CLAUDE_CONFIG_DIR) : home, ".claude.json"));
992
1100
  const projects = config?.projects && typeof config.projects === "object" ? config.projects : {};
993
1101
  const root = cwd ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || cwd : "";
994
1102
  if (cwd) push(projects[cwd]?.mcpServers ?? projects[root]?.mcpServers, "local");
995
- if (root) push(readJson2(join2(root, ".mcp.json"))?.mcpServers, "project");
1103
+ if (root) push(readJson2(join3(root, ".mcp.json"))?.mcpServers, "project");
996
1104
  push(config?.mcpServers, "user");
997
1105
  }
998
1106
  if (harness2 === "codex") {
999
1107
  try {
1000
- const config = parseToml2(readFileSync4(join2(process.env.CODEX_HOME ?? join2(home, ".codex"), "config.toml"), "utf8"));
1108
+ const config = parseToml2(readFileSync5(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "config.toml"), "utf8"));
1001
1109
  push(config.mcp_servers, "user");
1002
1110
  } catch {
1003
1111
  }
@@ -1035,7 +1143,7 @@ function workspaceState(cwd, harness2, skills2, home = homedir2(), title = null)
1035
1143
  harness: harness2,
1036
1144
  title: title?.trim().slice(0, 120) || null,
1037
1145
  cwd: cwd ? cwd.replace(home, "~") : null,
1038
- project: cwd ? basename3(root ?? cwd) : null,
1146
+ project: cwd ? basename4(root ?? cwd) : null,
1039
1147
  git: gitState,
1040
1148
  tree: tree.slice(0, MAX_TREE),
1041
1149
  tree_truncated: tree.length > MAX_TREE,
@@ -1057,7 +1165,7 @@ function skillFiles(skill) {
1057
1165
  }
1058
1166
  for (const entry2 of entries) {
1059
1167
  if (entry2.name === "node_modules" || entry2.name === ".git" || entry2.name === "__pycache__" || entry2.name === ".venv") continue;
1060
- const path = join2(dir, entry2.name);
1168
+ const path = join3(dir, entry2.name);
1061
1169
  if (entry2.isDirectory()) walk(path, depth + 1);
1062
1170
  else if (entry2.isFile()) out.push({ path, name: `skills/${skill.name}/${relative(skill.dir, path).split(sep).join("/")}` });
1063
1171
  }
@@ -1068,7 +1176,7 @@ function skillFiles(skill) {
1068
1176
  function codexThreadName(sessionId, home = homedir2()) {
1069
1177
  let name = null;
1070
1178
  try {
1071
- for (const line of readFileSync4(join2(process.env.CODEX_HOME ?? join2(home, ".codex"), "session_index.jsonl"), "utf8").split("\n")) {
1179
+ for (const line of readFileSync5(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "session_index.jsonl"), "utf8").split("\n")) {
1072
1180
  if (!line.includes(sessionId)) continue;
1073
1181
  try {
1074
1182
  const entry2 = JSON.parse(line);
@@ -1130,7 +1238,7 @@ function classify(path) {
1130
1238
  if (DOCUMENTS[ext]) return { kind: "document", mediaType: DOCUMENTS[ext] };
1131
1239
  if (IMAGES[ext]) return { kind: "image", mediaType: IMAGES[ext] };
1132
1240
  if (TEXT[ext]) return { kind: "text", mediaType: TEXT[ext] };
1133
- if (PLAIN_TEXT.has(ext) || PLAIN_NAMES.has(basename4(path))) return { kind: "text", mediaType: "text/plain" };
1241
+ if (PLAIN_TEXT.has(ext) || PLAIN_NAMES.has(basename5(path))) return { kind: "text", mediaType: "text/plain" };
1134
1242
  return null;
1135
1243
  }
1136
1244
  var SHELL_READ = /(?:^|[\s;&|(`"'])(?:cat|head|tail|nl|less|more|wc|bat|sed\s+-n\s+(?:'[^']*'|"[^"]*"|\S+))(?:\s+-[\w-]+(?:\s+\d+)?)*\s+((?:~|\.{0,2}\/)?[\w@%+./~-]*[\w-]\.[\w]{1,10}|\/[\w@%+./-]+)/g;
@@ -1149,8 +1257,8 @@ function touchedPaths(records, cwd, home = homedir3()) {
1149
1257
  const add = (raw, base) => {
1150
1258
  if (!raw || raw.length > 1e3) return;
1151
1259
  const path = raw.replace(/^~(?=\/)/, home);
1152
- if (isAbsolute(path)) out.add(resolve3(path));
1153
- else if (base) out.add(resolve3(base, path));
1260
+ if (isAbsolute(path)) out.add(resolve4(path));
1261
+ else if (base) out.add(resolve4(base, path));
1154
1262
  };
1155
1263
  const call = (name, input) => {
1156
1264
  const i = obj(input);
@@ -1187,33 +1295,33 @@ function touchedPaths(records, cwd, home = homedir3()) {
1187
1295
  return [...out];
1188
1296
  }
1189
1297
  function gitRoot(dir) {
1190
- for (let d = dir; ; d = dirname3(d)) {
1191
- if (existsSync4(join3(d, ".git"))) return d;
1192
- if (d === dirname3(d)) return null;
1298
+ for (let d = dir; ; d = dirname4(d)) {
1299
+ if (existsSync4(join4(d, ".git"))) return d;
1300
+ if (d === dirname4(d)) return null;
1193
1301
  }
1194
1302
  }
1195
1303
  function instructionPaths(cwd, harness2, home = homedir3()) {
1196
1304
  const out = [];
1197
1305
  if (cwd && existsSync4(cwd)) {
1198
1306
  const root = gitRoot(cwd) ?? cwd;
1199
- for (let d = resolve3(cwd); ; d = dirname3(d)) {
1200
- for (const name of INSTRUCTION_FILES) out.push(join3(d, name));
1201
- if (d === root || d === dirname3(d)) break;
1307
+ for (let d = resolve4(cwd); ; d = dirname4(d)) {
1308
+ for (const name of INSTRUCTION_FILES) out.push(join4(d, name));
1309
+ if (d === root || d === dirname4(d)) break;
1202
1310
  }
1203
1311
  }
1204
- if (harness2 === "claude_code") out.push(join3(process.env.CLAUDE_CONFIG_DIR ?? join3(home, ".claude"), "CLAUDE.md"));
1205
- if (harness2 === "codex") out.push(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "AGENTS.md"));
1312
+ if (harness2 === "claude_code") out.push(join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "CLAUDE.md"));
1313
+ if (harness2 === "codex") out.push(join4(process.env.CODEX_HOME ?? join4(home, ".codex"), "AGENTS.md"));
1206
1314
  out.push(...sharedInstructionPaths(home));
1207
1315
  return out.filter((path) => {
1208
1316
  try {
1209
- return statSync2(path).isFile();
1317
+ return statSync4(path).isFile();
1210
1318
  } catch {
1211
1319
  return false;
1212
1320
  }
1213
1321
  });
1214
1322
  }
1215
1323
  function resourceName(path, cwd, home = homedir3()) {
1216
- const inside = cwd ? relative2(resolve3(cwd), path) : "";
1324
+ const inside = cwd ? relative2(resolve4(cwd), path) : "";
1217
1325
  if (cwd && inside && !inside.startsWith("..") && !isAbsolute(inside)) return `workspace/${inside.split(sep2).join("/")}`;
1218
1326
  const underHome = relative2(home, path);
1219
1327
  const shown = !underHome.startsWith("..") && !isAbsolute(underHome) ? `~/${underHome}` : path.replace(/^\/+/, "");
@@ -1225,7 +1333,7 @@ function resourceLine(name, mediaType, data) {
1225
1333
  }
1226
1334
  function collectSessionFiles(records, opts) {
1227
1335
  const home = opts.home ?? homedir3();
1228
- const exclude = new Set((opts.exclude ?? []).map((path) => resolve3(path)));
1336
+ const exclude = new Set((opts.exclude ?? []).map((path) => resolve4(path)));
1229
1337
  const leftOut = [];
1230
1338
  const candidates = [];
1231
1339
  const seen = /* @__PURE__ */ new Set();
@@ -1240,7 +1348,7 @@ function collectSessionFiles(records, opts) {
1240
1348
  }
1241
1349
  let size;
1242
1350
  try {
1243
- const stat = statSync2(path);
1351
+ const stat = statSync4(path);
1244
1352
  if (!stat.isFile()) return;
1245
1353
  size = stat.size;
1246
1354
  } catch {
@@ -1290,7 +1398,7 @@ function collectSessionFiles(records, opts) {
1290
1398
  for (const file of candidates) {
1291
1399
  let data;
1292
1400
  try {
1293
- data = readFileSync5(file.path);
1401
+ data = readFileSync6(file.path);
1294
1402
  } catch {
1295
1403
  leftOut.push({ path: file.path, reason: "missing" });
1296
1404
  continue;
@@ -1336,12 +1444,12 @@ function decodeWorkspace(encoded) {
1336
1444
  }
1337
1445
  }
1338
1446
  function readPrefix(filePath) {
1339
- const fd = openSync(filePath, "r");
1447
+ const fd = openSync2(filePath, "r");
1340
1448
  try {
1341
1449
  const bytes = Buffer.alloc(256 * 1024);
1342
- return bytes.subarray(0, readSync(fd, bytes, 0, bytes.length, 0)).toString("utf8");
1450
+ return bytes.subarray(0, readSync2(fd, bytes, 0, bytes.length, 0)).toString("utf8");
1343
1451
  } finally {
1344
- closeSync(fd);
1452
+ closeSync2(fd);
1345
1453
  }
1346
1454
  }
1347
1455
  function sniffTitle(filePath) {
@@ -1396,13 +1504,13 @@ function sniffTitle(filePath) {
1396
1504
  }
1397
1505
  }
1398
1506
  function listClaudeCodeSessions(limit) {
1399
- const root = process.env.CLAUDE_CONFIG_DIR ?? join4(homedir4(), ".claude");
1400
- const projectsDir = join4(root, "projects");
1507
+ const root = process.env.CLAUDE_CONFIG_DIR ?? join5(homedir4(), ".claude");
1508
+ const projectsDir = join5(root, "projects");
1401
1509
  if (!existsSync5(projectsDir)) return [];
1402
1510
  const sessions = [];
1403
1511
  try {
1404
1512
  for (const encoded of readdirSync2(projectsDir)) {
1405
- const projectDir = join4(projectsDir, encoded);
1513
+ const projectDir = join5(projectsDir, encoded);
1406
1514
  let entries;
1407
1515
  try {
1408
1516
  entries = readdirSync2(projectDir);
@@ -1413,22 +1521,22 @@ function listClaudeCodeSessions(limit) {
1413
1521
  for (const entry2 of entries) {
1414
1522
  if (!entry2.endsWith(".jsonl")) continue;
1415
1523
  const sessionId = entry2.slice(0, -6);
1416
- const mainFile = join4(projectDir, entry2);
1524
+ const mainFile = join5(projectDir, entry2);
1417
1525
  let stat;
1418
1526
  try {
1419
- stat = statSync3(mainFile);
1527
+ stat = statSync5(mainFile);
1420
1528
  } catch {
1421
1529
  continue;
1422
1530
  }
1423
1531
  if (!stat.isFile()) continue;
1424
1532
  let discoveryError;
1425
1533
  const subagentFiles = [];
1426
- const subagentDir = join4(projectDir, sessionId, "subagents");
1534
+ const subagentDir = join5(projectDir, sessionId, "subagents");
1427
1535
  if (existsSync5(subagentDir)) {
1428
1536
  try {
1429
1537
  const subFiles = readdirSync2(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
1430
1538
  for (const f of subFiles) {
1431
- subagentFiles.push(join4(subagentDir, f));
1539
+ subagentFiles.push(join5(subagentDir, f));
1432
1540
  }
1433
1541
  } catch {
1434
1542
  discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
@@ -1450,7 +1558,7 @@ function listClaudeCodeSessions(limit) {
1450
1558
  return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
1451
1559
  }
1452
1560
  function listCodexSessions(limit) {
1453
- const sessionsDir = join4(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join4(homedir4(), ".codex"), "sessions");
1561
+ const sessionsDir = join5(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join5(homedir4(), ".codex"), "sessions");
1454
1562
  const records = [];
1455
1563
  function walk(dir) {
1456
1564
  let entries;
@@ -1460,7 +1568,7 @@ function listCodexSessions(limit) {
1460
1568
  return;
1461
1569
  }
1462
1570
  for (const entry2 of entries) {
1463
- const full = join4(dir, entry2.name);
1571
+ const full = join5(dir, entry2.name);
1464
1572
  if (entry2.isDirectory()) {
1465
1573
  walk(full);
1466
1574
  continue;
@@ -1478,7 +1586,7 @@ function listCodexSessions(limit) {
1478
1586
  sessionId: data.id,
1479
1587
  workspace: typeof data.cwd === "string" ? data.cwd : dir,
1480
1588
  title: sniffTitle(full),
1481
- mtimeMs: statSync3(full).mtimeMs,
1589
+ mtimeMs: statSync5(full).mtimeMs,
1482
1590
  files: [full]
1483
1591
  },
1484
1592
  ...typeof parent === "string" ? { parent } : {}
@@ -1547,7 +1655,7 @@ async function mintTrace(base, harness2) {
1547
1655
  function loadSummaryFile(summaryPath) {
1548
1656
  let raw;
1549
1657
  try {
1550
- raw = readFileSync6(summaryPath, "utf8");
1658
+ raw = readFileSync7(summaryPath, "utf8");
1551
1659
  } catch {
1552
1660
  throw new Error(`Cannot read summary file: ${summaryPath}`);
1553
1661
  }
@@ -1579,7 +1687,7 @@ function prepareImport(session2, summaryPath, options = {}) {
1579
1687
  let inputBytes = 0;
1580
1688
  let outputBytes = 0;
1581
1689
  const lines = [];
1582
- const digest = createHash("sha256");
1690
+ const digest = createHash2("sha256");
1583
1691
  let nativeId;
1584
1692
  let redactionCount = 0;
1585
1693
  let encryptedReasoningCount = 0;
@@ -1588,9 +1696,9 @@ function prepareImport(session2, summaryPath, options = {}) {
1588
1696
  const source = new SessionSourceCollector();
1589
1697
  const records = [];
1590
1698
  for (const path of session2.files) {
1591
- const stat = statSync3(path);
1699
+ const stat = statSync5(path);
1592
1700
  if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
1593
- const text = readFileSync6(path, "utf8");
1701
+ const text = readFileSync7(path, "utf8");
1594
1702
  for (const raw of text.split("\n")) {
1595
1703
  if (!raw.trim()) continue;
1596
1704
  let record;
@@ -1672,8 +1780,8 @@ function prepareImport(session2, summaryPath, options = {}) {
1672
1780
  }
1673
1781
  async function importSession(opts) {
1674
1782
  const base = (opts.agentUseUrl ?? agentUseBase()).replace(/\/$/, "");
1675
- const destination = new URL(base);
1676
- if (destination.username || destination.password || destination.protocol !== "https:" && !(destination.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(destination.hostname))) {
1783
+ const destination2 = new URL(base);
1784
+ if (destination2.username || destination2.password || destination2.protocol !== "https:" && !(destination2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(destination2.hostname))) {
1677
1785
  throw new Error("Import requires HTTPS, or a loopback development server.");
1678
1786
  }
1679
1787
  opts.onProgress?.("Reading transcript and redacting secrets locally\u2026");
@@ -1716,8 +1824,59 @@ async function importSession(opts) {
1716
1824
  };
1717
1825
  }
1718
1826
 
1827
+ // src/channels.ts
1828
+ function connectionLine(c) {
1829
+ const route = c.bound_session ? `\u2192 ${c.bound_session.id}${c.bound_session.label ? ` (${c.bound_session.label})` : ""}` : "unrouted";
1830
+ return `${c.id} ${c.platform.padEnd(9)} ${c.status.padEnd(12)} ${c.label ?? ""} ${route}`;
1831
+ }
1832
+ function connectSteps(c) {
1833
+ const steps = [];
1834
+ if (c.connect?.url) steps.push(`Open: ${c.connect.url}`);
1835
+ if (c.connect?.phone && c.connect.code) steps.push(`Text ${c.connect.code} to ${c.connect.phone}`);
1836
+ else if (c.connect?.app_id && c.connect.code) steps.push(`Send ${c.connect.code} to the bot (app ${c.connect.app_id})`);
1837
+ else if (c.connect?.code) steps.push(`Send ${c.connect.code} to the bot`);
1838
+ if (c.connect?.expires_at) steps.push(`Expires ${c.connect.expires_at.replace("T", " ").slice(0, 16)}`);
1839
+ return steps;
1840
+ }
1841
+ function destination(opts) {
1842
+ if (Boolean(opts.session) === Boolean(opts.webhook)) {
1843
+ throw new ApiError(400, "invalid_destination", "pass exactly one of --session or --webhook");
1844
+ }
1845
+ return opts.session ? { session: opts.session } : { webhook: opts.webhook };
1846
+ }
1847
+ async function listConnections() {
1848
+ return (await api("GET", "/channels/connections")).connections;
1849
+ }
1850
+ async function connect(platform, opts) {
1851
+ return api("POST", "/channels/connections", {
1852
+ platform,
1853
+ ...opts.label ? { label: opts.label } : {},
1854
+ ...opts.app ? { app: opts.app } : {},
1855
+ ...opts.session || opts.webhook ? { destination: destination(opts) } : {}
1856
+ });
1857
+ }
1858
+ async function waitConnected(id, opts = {}) {
1859
+ const deadline = Date.now() + (opts.timeoutMs ?? 10 * 6e4);
1860
+ for (; ; ) {
1861
+ const found = (await listConnections()).find((c) => c.id === id);
1862
+ if (!found) throw new ApiError(404, "not_found", `connection ${id} is gone`);
1863
+ if (found.status !== "PENDING") return found;
1864
+ if (Date.now() > deadline) throw new ApiError(408, "connect_timeout", "the connection was not claimed within 10 minutes");
1865
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 3e3));
1866
+ }
1867
+ }
1868
+ async function bind(connectionId, opts) {
1869
+ await api("PUT", `/channels/connections/${encodeURIComponent(connectionId)}/binding`, { destination: destination(opts) });
1870
+ }
1871
+ async function send(threadId, text) {
1872
+ await api("POST", `/channels/threads/${encodeURIComponent(threadId)}/messages`, { parts: [{ type: "markdown", text }] });
1873
+ }
1874
+
1875
+ // src/connectors.ts
1876
+ import { createServer as createServer2 } from "http";
1877
+
1719
1878
  // src/login.ts
1720
- import { createHash as createHash2, randomBytes } from "crypto";
1879
+ import { createHash as createHash3, randomBytes } from "crypto";
1721
1880
  import { createServer } from "http";
1722
1881
  import { hostname } from "os";
1723
1882
  import { spawn as spawn2 } from "child_process";
@@ -1733,7 +1892,7 @@ function openBrowser2(url) {
1733
1892
  }
1734
1893
  async function login(opts) {
1735
1894
  const verifier = b64url(randomBytes(32));
1736
- const challenge = b64url(createHash2("sha256").update(verifier).digest());
1895
+ const challenge = b64url(createHash3("sha256").update(verifier).digest());
1737
1896
  const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
1738
1897
  saveConfig({
1739
1898
  ...loadConfig(),
@@ -1749,7 +1908,7 @@ async function login(opts) {
1749
1908
  }
1750
1909
  async function browserLogin(verifier, challenge) {
1751
1910
  const state = b64url(randomBytes(16));
1752
- const code = await new Promise((resolve8, reject) => {
1911
+ const code = await new Promise((resolve10, reject) => {
1753
1912
  const server = createServer((req, res) => {
1754
1913
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
1755
1914
  if (url.pathname !== "/callback") {
@@ -1762,7 +1921,7 @@ async function browserLogin(verifier, challenge) {
1762
1921
  if (url.searchParams.get("state") !== state || !got) {
1763
1922
  reject(new Error("login callback state mismatch"));
1764
1923
  } else {
1765
- resolve8(got);
1924
+ resolve10(got);
1766
1925
  }
1767
1926
  server.close();
1768
1927
  });
@@ -1815,18 +1974,244 @@ async function headlessLogin(verifier, challenge) {
1815
1974
  throw new Error("login timed out after 10 minutes");
1816
1975
  }
1817
1976
 
1977
+ // src/connectors.ts
1978
+ async function listApps(query, limit = 50) {
1979
+ const apps = [];
1980
+ let cursor = null;
1981
+ do {
1982
+ const params = new URLSearchParams();
1983
+ if (query) params.set("q", query);
1984
+ if (cursor) params.set("cursor", cursor);
1985
+ const qs = params.toString();
1986
+ const page = await api("GET", `/connectors/apps${qs ? `?${qs}` : ""}`);
1987
+ apps.push(...page.apps);
1988
+ cursor = page.next_cursor;
1989
+ } while (cursor && apps.length < limit);
1990
+ return apps.slice(0, limit);
1991
+ }
1992
+ function connectionLine2(c) {
1993
+ return `${c.id} ${c.app_name} (${c.app}) ${c.label} since ${c.created_at.slice(0, 10)}`;
1994
+ }
1995
+ function workspaceConnectionLine(c) {
1996
+ const owner = c.workspace_owned ? "the workspace" : c.mine ? "you" : c.shared_by?.name ?? c.shared_by?.user_id ?? "someone";
1997
+ return `${c.connection_id} ${c.app_name} (${c.app}) ${c.label} shared by ${owner}`;
1998
+ }
1999
+ function browserReturn(start, opts = {}) {
2000
+ return new Promise((resolve10, reject) => {
2001
+ const server = createServer2((req, res) => {
2002
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
2003
+ if (url.pathname !== "/done" && url.pathname !== "/failed") {
2004
+ res.writeHead(404).end();
2005
+ return;
2006
+ }
2007
+ const ok = url.pathname === "/done";
2008
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2009
+ res.end(ok ? "<p>Connected \u2014 you can close this tab and return to your terminal.</p>" : "<p>The app was not connected. Return to your terminal.</p>");
2010
+ server.close();
2011
+ if (ok) resolve10(url.searchParams.get("account_id") ?? void 0);
2012
+ else reject(new ApiError(400, "connect_failed", "the app was not connected"));
2013
+ });
2014
+ server.listen(0, "127.0.0.1", () => {
2015
+ const address = server.address();
2016
+ const port = typeof address === "object" && address ? address.port : 0;
2017
+ const base = `http://127.0.0.1:${port}`;
2018
+ start(`${base}/done`, `${base}/failed`).then(
2019
+ (authorizeUrl) => {
2020
+ console.log(`Authorize access in your browser:
2021
+ ${authorizeUrl}`);
2022
+ if (opts.open !== false) openBrowser2(authorizeUrl);
2023
+ },
2024
+ (err) => {
2025
+ server.close();
2026
+ reject(err);
2027
+ }
2028
+ );
2029
+ });
2030
+ setTimeout(() => {
2031
+ server.close();
2032
+ reject(new ApiError(408, "connect_timeout", "nobody finished the authorization within 10 minutes"));
2033
+ }, opts.timeoutMs ?? 10 * 6e4).unref();
2034
+ });
2035
+ }
2036
+ async function connectApp(app, opts = {}) {
2037
+ const base = opts.workspace ? `/universes/${encodeURIComponent(opts.workspace)}/connections` : "/connectors/connections";
2038
+ const accountId = await browserReturn(async (success_url, error_url) => {
2039
+ const { url } = await api("POST", base, { app, success_url, error_url });
2040
+ return url;
2041
+ }, { open: opts.open });
2042
+ const done = await api("POST", `${base}/complete`, {
2043
+ app,
2044
+ ...accountId ? { account_id: accountId } : {}
2045
+ });
2046
+ return done.connection;
2047
+ }
2048
+
2049
+ // src/routine-triggers.ts
2050
+ var triggerPath = (id, resource) => `/routines/${encodeURIComponent(id)}/${resource}`;
2051
+ function inboundLine(u) {
2052
+ if (!u) return "no inbound URL \u2014 make one with --create";
2053
+ return u.url ?? `inbound URL ${u.prefix}\u2026 made ${u.created_at.slice(0, 10)}${u.last_used_at ? `, last used ${u.last_used_at.slice(0, 16).replace("T", " ")}` : ", never used"}`;
2054
+ }
2055
+ function precheckLine(p) {
2056
+ if (!p) return "no precheck: every scheduled fire runs";
2057
+ const last = p.last_result ? `last check ${p.last_result}${p.last_error ? ` (${p.last_error.type}: ${p.last_error.message})` : ""}` : "not checked yet";
2058
+ return `fires only when ${p.url} changes \xB7 ${last}`;
2059
+ }
2060
+ function triggerLine(t, available = true) {
2061
+ if (!t) return "no chat trigger";
2062
+ const where = t.thread_id ? `thread ${t.thread_id} on ${t.connection_id}` : `any conversation on ${t.connection_id}`;
2063
+ const what = t.text_prefix ? `messages starting "${t.text_prefix}"` : "every message";
2064
+ return `${what} in ${where} start a run${available ? "" : " (chat messages are not offered to routines in this deployment)"}`;
2065
+ }
2066
+ function deliverLine(d) {
2067
+ return d ? `answers post to thread ${d.thread_id} on ${d.connection_id}` : "answers are posted nowhere";
2068
+ }
2069
+ function triggerBody(opts) {
2070
+ if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
2071
+ return { connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {}, ...opts.prefix ? { text_prefix: opts.prefix } : {} };
2072
+ }
2073
+ function deliverBody(opts) {
2074
+ if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
2075
+ return { type: "channel_thread", connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {} };
2076
+ }
2077
+ async function triggerRequest(id, resource, change) {
2078
+ const path = triggerPath(id, resource);
2079
+ if (change.remove) return api("DELETE", path);
2080
+ if (change.create) return api("POST", path);
2081
+ if (change.body) return api("PUT", path, change.body);
2082
+ return api("GET", path);
2083
+ }
2084
+
2085
+ // src/session-workspace.ts
2086
+ import { spawnSync as spawnSync2 } from "child_process";
2087
+ import { createHash as createHash4, randomUUID } from "crypto";
2088
+ import { readdirSync as readdirSync3, statSync as statSync6 } from "fs";
2089
+ import { basename as basename6, join as join6, relative as relative3, resolve as resolve5 } from "path";
2090
+ var SINGLE_IMPORT_MAX = 25 * 1024 * 1024;
2091
+ var PART_SIZE = 8 * 1024 * 1024;
2092
+ var SKIPPED = /* @__PURE__ */ new Set([".git", "node_modules"]);
2093
+ var workspacePath = (id) => `/sessions/${encodeURIComponent(id)}/workspace`;
2094
+ async function listFiles(id, path, hidden) {
2095
+ const query = new URLSearchParams();
2096
+ if (path) query.set("path", path);
2097
+ if (hidden) query.set("hidden", "true");
2098
+ const qs = query.toString();
2099
+ return api("GET", `${workspacePath(id)}/files${qs ? `?${qs}` : ""}`);
2100
+ }
2101
+ function entryLine(e) {
2102
+ const size = e.type === "directory" ? "-" : String(e.size ?? 0);
2103
+ return `${e.type === "directory" ? "d" : "-"} ${size.padStart(10)} ${(e.mtime ?? "").slice(0, 16).replace("T", " ")} ${e.name}${e.type === "directory" ? "/" : ""}`;
2104
+ }
2105
+ async function readFile(id, path) {
2106
+ return (await apiBytes("GET", `${workspacePath(id)}/files/content?path=${encodeURIComponent(path)}`)).bytes;
2107
+ }
2108
+ function renderChanges(changes) {
2109
+ if (!changes.repos.length) return [`no git repository under ${changes.root}`];
2110
+ return changes.repos.flatMap((r) => [
2111
+ `${r.path === "." ? changes.root : r.path}: ${r.status ? "changed" : "clean"}`,
2112
+ ...(r.stat ?? r.status ?? "").split("\n").filter(Boolean).map((line) => ` ${line}`)
2113
+ ]);
2114
+ }
2115
+ async function workspacePatch(id, repo) {
2116
+ const { patch, ...meta } = await api("POST", `${workspacePath(id)}/patch`, { repo });
2117
+ return { meta, patch: Buffer.from(patch, "base64") };
2118
+ }
2119
+ function folderFiles(dir) {
2120
+ const git2 = spawnSync2("git", ["-C", dir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8" });
2121
+ const top = spawnSync2("git", ["-C", dir, "rev-parse", "--show-prefix"], { encoding: "utf8" });
2122
+ if (git2.status === 0 && top.status === 0) {
2123
+ return git2.stdout.split("\0").filter((path) => path && statSync6(join6(dir, path), { throwIfNoEntry: false })?.isFile());
2124
+ }
2125
+ const files2 = [];
2126
+ const walk = (at) => {
2127
+ for (const entry2 of readdirSync3(at, { withFileTypes: true })) {
2128
+ if (SKIPPED.has(entry2.name)) continue;
2129
+ const full = join6(at, entry2.name);
2130
+ if (entry2.isDirectory()) walk(full);
2131
+ else if (entry2.isFile()) files2.push(relative3(dir, full));
2132
+ }
2133
+ };
2134
+ walk(dir);
2135
+ return files2;
2136
+ }
2137
+ function folderArchive(dir, files2) {
2138
+ const tar = spawnSync2("tar", ["--format=ustar", "-czf", "-", "-C", dir, "-T", "-"], {
2139
+ input: files2.join("\n"),
2140
+ env: { ...process.env, COPYFILE_DISABLE: "1" },
2141
+ maxBuffer: 2 * 1024 * 1024 * 1024
2142
+ });
2143
+ if (tar.status !== 0) throw new ApiError(400, "archive_failed", `tar failed: ${tar.stderr.toString().trim()}`);
2144
+ return tar.stdout;
2145
+ }
2146
+ async function importFolder(id, dir, opts = {}) {
2147
+ const root = resolve5(dir);
2148
+ const name = opts.name ?? basename6(root);
2149
+ const archive = opts.archive ?? folderArchive(root, folderFiles(root));
2150
+ if (archive.length <= SINGLE_IMPORT_MAX) {
2151
+ const res = await apiBytes("POST", `${workspacePath(id)}/import?name=${encodeURIComponent(name)}`, { bytes: archive, mediaType: "application/gzip" });
2152
+ return res.json();
2153
+ }
2154
+ const sha256 = createHash4("sha256").update(archive).digest("hex");
2155
+ let upload = await api("POST", `${workspacePath(id)}/imports`, { name, size: archive.length, sha256, part_size: PART_SIZE });
2156
+ const uploadPath = `${workspacePath(id)}/imports/${encodeURIComponent(upload.id)}`;
2157
+ for (let part = 0; part < upload.parts; part += 1) {
2158
+ if (upload.received.includes(part)) continue;
2159
+ const bytes = archive.subarray(part * upload.part_size, (part + 1) * upload.part_size);
2160
+ await apiBytes("PUT", `${uploadPath}/parts/${part}`, { bytes, mediaType: "application/octet-stream" });
2161
+ }
2162
+ upload = await api("POST", `${uploadPath}/complete`);
2163
+ while (upload.state !== "imported" && upload.state !== "failed") {
2164
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
2165
+ upload = await api("GET", uploadPath);
2166
+ }
2167
+ if (upload.state === "failed") throw new ApiError(422, upload.error?.code ?? "import_failed", upload.error?.message ?? "the import failed");
2168
+ return { path: upload.path ?? name, files: upload.files ?? 0 };
2169
+ }
2170
+ function renderAccess(a) {
2171
+ const lines = [
2172
+ `${a.session_id}${a.title ? ` \u2014 ${a.title}` : ""}`,
2173
+ ` ${a.visibility === "private" ? "private to its owner" : `visible to ${a.workspace.name} (${a.workspace.slug})`} \xB7 you are its ${a.access}`,
2174
+ ` owner: ${a.owner.name ?? a.owner.email ?? "unknown"}`
2175
+ ];
2176
+ if (a.viewers) lines.push(a.viewers.length ? ` viewers: ${a.viewers.map((v) => v.email).join(", ")}` : " viewers: none");
2177
+ return lines;
2178
+ }
2179
+ function accessPatch(opts) {
2180
+ if (opts.private && opts.members) throw new ApiError(400, "invalid_request", "pass --private or --members, not both");
2181
+ return {
2182
+ ...opts.private ? { visibility: "private" } : opts.members ? { visibility: "workspace" } : {},
2183
+ ...opts.addViewer?.length ? { add_viewers: opts.addViewer } : {},
2184
+ ...opts.removeViewer?.length ? { remove_viewers: opts.removeViewer } : {}
2185
+ };
2186
+ }
2187
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
2188
+ async function handoff(id, targetAgent, opts = {}) {
2189
+ let { handoff: h } = await api("POST", `/sessions/${encodeURIComponent(id)}/handoff`, {
2190
+ target_agent: targetAgent,
2191
+ ...opts.fromMessage ? { last_message_id: opts.fromMessage } : {}
2192
+ }, { idempotencyKey: opts.idempotencyKey ?? randomUUID() });
2193
+ opts.onStatus?.(h);
2194
+ while (opts.wait && !TERMINAL.has(h.status)) {
2195
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
2196
+ const next = (await api("GET", `/session-handoffs/${encodeURIComponent(h.id)}`)).handoff;
2197
+ if (next.status !== h.status) opts.onStatus?.(next);
2198
+ h = next;
2199
+ }
2200
+ return h;
2201
+ }
2202
+
1818
2203
  // src/local-agents.ts
1819
2204
  import { existsSync as existsSync6 } from "fs";
1820
2205
  import { homedir as homedir5 } from "os";
1821
- import { join as join5 } from "path";
2206
+ import { join as join7 } from "path";
1822
2207
  function localAgents(env = process.env, home = homedir5()) {
1823
- const claude = env.CLAUDE_CONFIG_DIR || join5(home, ".claude");
1824
- const codex = env.CODEX_HOME || join5(home, ".codex");
1825
- const cursor = join5(home, ".cursor");
2208
+ const claude = env.CLAUDE_CONFIG_DIR || join7(home, ".claude");
2209
+ const codex = env.CODEX_HOME || join7(home, ".codex");
2210
+ const cursor = join7(home, ".cursor");
1826
2211
  return [
1827
- { id: "claude_code", name: "Claude Code", root: claude, skillsDir: join5(claude, "skills") },
1828
- { id: "codex", name: "Codex", root: codex, skillsDir: join5(codex, "skills") },
1829
- { id: "cursor", name: "Cursor", root: cursor, skillsDir: join5(cursor, "skills") }
2212
+ { id: "claude_code", name: "Claude Code", root: claude, skillsDir: join7(claude, "skills") },
2213
+ { id: "codex", name: "Codex", root: codex, skillsDir: join7(codex, "skills") },
2214
+ { id: "cursor", name: "Cursor", root: cursor, skillsDir: join7(cursor, "skills") }
1830
2215
  ];
1831
2216
  }
1832
2217
  function detectLocalAgents(env = process.env, home = homedir5()) {
@@ -1926,21 +2311,21 @@ async function readTurn(sessionId, opts = {}) {
1926
2311
  for (; ; ) {
1927
2312
  const reading = latestTurn(await history(sessionId));
1928
2313
  if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
1929
- await new Promise((resolve8) => setTimeout(resolve8, opts.intervalMs ?? 3e3));
2314
+ await new Promise((resolve10) => setTimeout(resolve10, opts.intervalMs ?? 3e3));
1930
2315
  }
1931
2316
  }
1932
2317
 
1933
2318
  // src/setup.ts
1934
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
1935
- import { dirname as dirname5, join as join9 } from "path";
2319
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
2320
+ import { dirname as dirname6, join as join11 } from "path";
1936
2321
  import { createInterface as createInterface3 } from "readline";
1937
2322
 
1938
2323
  // src/mcp-register.ts
1939
- import { spawnSync as spawnSync2 } from "child_process";
2324
+ import { spawnSync as spawnSync3 } from "child_process";
1940
2325
 
1941
2326
  // src/skill.ts
1942
- import { mkdirSync, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
1943
- import { join as join6 } from "path";
2327
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
2328
+ import { join as join8 } from "path";
1944
2329
 
1945
2330
  // src/catalog-file.ts
1946
2331
  var CATALOG_FILE = "catalog.md";
@@ -2381,7 +2766,7 @@ To set a per-repo default, create \`.agentsky.json\` at the git root:
2381
2766
  }
2382
2767
  function installedVersion(dir) {
2383
2768
  try {
2384
- const marker = JSON.parse(readFileSync7(join6(dir, SKILL_MARKER), "utf8"));
2769
+ const marker = JSON.parse(readFileSync8(join8(dir, SKILL_MARKER), "utf8"));
2385
2770
  return typeof marker.version === "string" ? marker.version : null;
2386
2771
  } catch {
2387
2772
  return null;
@@ -2391,25 +2776,25 @@ function isInstalledSkill(dir) {
2391
2776
  return installedVersion(dir) !== null;
2392
2777
  }
2393
2778
  function installSkill(agent2, opts = {}) {
2394
- const dir = join6(agent2.skillsDir, SKILL_NAME);
2779
+ const dir = join8(agent2.skillsDir, SKILL_NAME);
2395
2780
  const previous = installedVersion(dir);
2396
2781
  const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
2397
2782
  if (opts.dryRun) return { agent: agent2.id, path: dir, outcome };
2398
2783
  if (outcome !== "current") {
2399
- mkdirSync(dir, { recursive: true });
2400
- writeFileSync3(join6(dir, "SKILL.md"), skillDocument());
2401
- writeFileSync3(join6(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
2784
+ mkdirSync2(dir, { recursive: true });
2785
+ writeFileSync4(join8(dir, "SKILL.md"), skillDocument());
2786
+ writeFileSync4(join8(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
2402
2787
  `);
2403
2788
  }
2404
2789
  if (opts.catalog) {
2405
- mkdirSync(dir, { recursive: true });
2406
- writeFileSync3(join6(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
2407
- }
2408
- const refsDir = join6(dir, REFERENCES_DIR);
2409
- mkdirSync(refsDir, { recursive: true });
2410
- writeFileSync3(join6(refsDir, "import.md"), importReferenceDoc());
2411
- writeFileSync3(join6(refsDir, "workspaces.md"), workspacesReferenceDoc());
2412
- writeFileSync3(join6(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
2790
+ mkdirSync2(dir, { recursive: true });
2791
+ writeFileSync4(join8(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
2792
+ }
2793
+ const refsDir = join8(dir, REFERENCES_DIR);
2794
+ mkdirSync2(refsDir, { recursive: true });
2795
+ writeFileSync4(join8(refsDir, "import.md"), importReferenceDoc());
2796
+ writeFileSync4(join8(refsDir, "workspaces.md"), workspacesReferenceDoc());
2797
+ writeFileSync4(join8(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
2413
2798
  return { agent: agent2.id, path: dir, outcome };
2414
2799
  }
2415
2800
 
@@ -2418,9 +2803,9 @@ var MCP_SERVER_NAME = SKILL_NAME;
2418
2803
  var CODEX_TOKEN_ENV = "AGENTSKY_API_KEY";
2419
2804
  function systemCommandRunner() {
2420
2805
  return {
2421
- has: (command) => spawnSync2(command, ["--version"], { stdio: "ignore" }).status === 0,
2806
+ has: (command) => spawnSync3(command, ["--version"], { stdio: "ignore" }).status === 0,
2422
2807
  run: (command, args) => {
2423
- const result = spawnSync2(command, args, { encoding: "utf8" });
2808
+ const result = spawnSync3(command, args, { encoding: "utf8" });
2424
2809
  return { status: result.status, stderr: result.stderr ?? "" };
2425
2810
  }
2426
2811
  };
@@ -2461,9 +2846,9 @@ function registerMcpServer(agent2, opts, runner = systemCommandRunner()) {
2461
2846
 
2462
2847
  // src/setup-local.ts
2463
2848
  import { isUtf8 as isUtf82 } from "buffer";
2464
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
2849
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync7 } from "fs";
2465
2850
  import { homedir as homedir6 } from "os";
2466
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
2851
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join9, resolve as resolve6 } from "path";
2467
2852
  import { parse as parseToml3 } from "smol-toml";
2468
2853
  var MAX_SKILL_FILE_BYTES = 1024 * 1024;
2469
2854
  var MAX_IMPORT_DEPTH = 5;
@@ -2487,12 +2872,12 @@ function redacted(data) {
2487
2872
  }
2488
2873
  function skillFiles2(root, prefix) {
2489
2874
  const files2 = [];
2490
- for (const entry2 of readdirSync3(root, { withFileTypes: true })) {
2491
- const path = join7(root, entry2.name);
2875
+ for (const entry2 of readdirSync4(root, { withFileTypes: true })) {
2876
+ const path = join9(root, entry2.name);
2492
2877
  if (entry2.isDirectory()) {
2493
2878
  if (!SKIPPED_DIRS.has(entry2.name)) files2.push(...skillFiles2(path, `${prefix}/${entry2.name}`));
2494
- } else if (entry2.isFile() && !sensitive(entry2.name) && statSync4(path).size <= MAX_SKILL_FILE_BYTES) {
2495
- const bytes = redacted(readFileSync8(path));
2879
+ } else if (entry2.isFile() && !sensitive(entry2.name) && statSync7(path).size <= MAX_SKILL_FILE_BYTES) {
2880
+ const bytes = redacted(readFileSync9(path));
2496
2881
  if (bytes) files2.push({ path: `${prefix}/${entry2.name}`, bytes });
2497
2882
  }
2498
2883
  }
@@ -2503,18 +2888,18 @@ function readLocalSkills(agents) {
2503
2888
  const ignored = [];
2504
2889
  for (const agent2 of agents) {
2505
2890
  if (!existsSync7(agent2.skillsDir)) continue;
2506
- for (const entry2 of readdirSync3(agent2.skillsDir, { withFileTypes: true })) {
2507
- const dir = join7(agent2.skillsDir, entry2.name);
2891
+ for (const entry2 of readdirSync4(agent2.skillsDir, { withFileTypes: true })) {
2892
+ const dir = join9(agent2.skillsDir, entry2.name);
2508
2893
  let isDir2 = entry2.isDirectory();
2509
2894
  if (entry2.isSymbolicLink()) {
2510
2895
  try {
2511
- isDir2 = statSync4(dir).isDirectory();
2896
+ isDir2 = statSync7(dir).isDirectory();
2512
2897
  } catch {
2513
2898
  isDir2 = false;
2514
2899
  }
2515
2900
  }
2516
- if (!isDir2 || !existsSync7(join7(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
2517
- const name = frontmatterName(readFileSync8(join7(dir, "SKILL.md"), "utf8"));
2901
+ if (!isDir2 || !existsSync7(join9(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
2902
+ const name = frontmatterName(readFileSync9(join9(dir, "SKILL.md"), "utf8"));
2518
2903
  if (!name) {
2519
2904
  ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
2520
2905
  continue;
@@ -2542,11 +2927,11 @@ function inlineImports(content, from, home, depth, seen) {
2542
2927
  }
2543
2928
  const target = fence ? void 0 : line.match(/^@(\S+\.(?:md|markdown|txt))$/i)?.[1];
2544
2929
  if (!target || depth >= MAX_IMPORT_DEPTH) return line;
2545
- const path = target.startsWith("~/") ? join7(home, target.slice(2)) : isAbsolute2(target) ? target : resolve4(dirname4(from), target);
2930
+ const path = target.startsWith("~/") ? join9(home, target.slice(2)) : isAbsolute2(target) ? target : resolve6(dirname5(from), target);
2546
2931
  if (seen.includes(path)) return "";
2547
2932
  let imported;
2548
2933
  try {
2549
- imported = readFileSync8(path, "utf8");
2934
+ imported = readFileSync9(path, "utf8");
2550
2935
  } catch {
2551
2936
  return line;
2552
2937
  }
@@ -2559,10 +2944,10 @@ function readGlobalInstructions(agents, home = homedir6()) {
2559
2944
  for (const agent2 of agents) {
2560
2945
  const file = INSTRUCTION_FILES2[agent2.id];
2561
2946
  if (!file) continue;
2562
- const path = join7(agent2.root, file);
2947
+ const path = join9(agent2.root, file);
2563
2948
  let raw;
2564
2949
  try {
2565
- raw = readFileSync8(path, "utf8");
2950
+ raw = readFileSync9(path, "utf8");
2566
2951
  } catch {
2567
2952
  continue;
2568
2953
  }
@@ -2581,7 +2966,7 @@ ${item.content.trim()}`).join("\n\n");
2581
2966
  }
2582
2967
  function readJson3(path) {
2583
2968
  try {
2584
- return JSON.parse(readFileSync8(path, "utf8"));
2969
+ return JSON.parse(readFileSync9(path, "utf8"));
2585
2970
  } catch {
2586
2971
  return void 0;
2587
2972
  }
@@ -2595,10 +2980,10 @@ function bearerOf(def) {
2595
2980
  return match?.[1] ?? null;
2596
2981
  }
2597
2982
  function serversOf(agent2, home) {
2598
- if (agent2.id === "claude_code") return readJson3(join7(home, ".claude.json"))?.mcpServers ?? {};
2599
- if (agent2.id === "cursor") return readJson3(join7(agent2.root, "mcp.json"))?.mcpServers ?? {};
2983
+ if (agent2.id === "claude_code") return readJson3(join9(home, ".claude.json"))?.mcpServers ?? {};
2984
+ if (agent2.id === "cursor") return readJson3(join9(agent2.root, "mcp.json"))?.mcpServers ?? {};
2600
2985
  try {
2601
- return parseToml3(readFileSync8(join7(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
2986
+ return parseToml3(readFileSync9(join9(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
2602
2987
  } catch {
2603
2988
  return {};
2604
2989
  }
@@ -2619,9 +3004,9 @@ function findLocalSecrets(agents, home = homedir6()) {
2619
3004
 
2620
3005
  // src/subscriptions.ts
2621
3006
  import { spawn as spawn3 } from "child_process";
2622
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
3007
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
2623
3008
  import { homedir as homedir7 } from "os";
2624
- import { join as join8 } from "path";
3009
+ import { join as join10 } from "path";
2625
3010
  import { createInterface as createInterface2 } from "readline";
2626
3011
  var ALIASES = {
2627
3012
  claude: "anthropic",
@@ -2637,14 +3022,24 @@ function providerFor(name) {
2637
3022
  }
2638
3023
  return provider;
2639
3024
  }
3025
+ var MANAGED_ALIASES = {
3026
+ ...ALIASES,
3027
+ cursor: "cursor",
3028
+ grok: "xai",
3029
+ xai: "xai",
3030
+ muse: "meta",
3031
+ meta: "meta"
3032
+ };
2640
3033
  function managedProviderFor(name) {
2641
- if (name.toLowerCase() === "cursor") return "cursor";
2642
- const provider = ALIASES[name.toLowerCase()];
3034
+ const provider = MANAGED_ALIASES[name.toLowerCase()];
2643
3035
  if (!provider) {
2644
- throw new ApiError(400, "unknown_provider", `unknown provider "${name}" \u2014 use claude, chatgpt or cursor`);
3036
+ throw new ApiError(400, "unknown_provider", `unknown provider "${name}" \u2014 use claude, chatgpt, cursor, grok or muse`);
2645
3037
  }
2646
3038
  return provider;
2647
3039
  }
3040
+ function accountQuery(account) {
3041
+ return account ? `?accountId=${encodeURIComponent(account)}` : "";
3042
+ }
2648
3043
  function ask(question) {
2649
3044
  return new Promise((done) => {
2650
3045
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
@@ -2707,11 +3102,11 @@ async function connectClaude() {
2707
3102
  console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
2708
3103
  }
2709
3104
  function codexSignIn(env = process.env, home = homedir7()) {
2710
- const path = join8(env.CODEX_HOME || join8(home, ".codex"), "auth.json");
3105
+ const path = join10(env.CODEX_HOME || join10(home, ".codex"), "auth.json");
2711
3106
  if (!existsSync8(path)) {
2712
3107
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
2713
3108
  }
2714
- const raw = readFileSync9(path, "utf8");
3109
+ const raw = readFileSync10(path, "utf8");
2715
3110
  try {
2716
3111
  const parsed = JSON.parse(raw);
2717
3112
  return { path, raw, account: parsed.tokens?.account_id };
@@ -2720,11 +3115,11 @@ function codexSignIn(env = process.env, home = homedir7()) {
2720
3115
  }
2721
3116
  }
2722
3117
  function cursorSignIn(env = process.env, home = homedir7()) {
2723
- const path = join8(env.XDG_CONFIG_HOME || join8(home, ".config"), "cursor", "auth.json");
3118
+ const path = join10(env.XDG_CONFIG_HOME || join10(home, ".config"), "cursor", "auth.json");
2724
3119
  if (!existsSync8(path)) {
2725
3120
  throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
2726
3121
  }
2727
- const raw = readFileSync9(path, "utf8");
3122
+ const raw = readFileSync10(path, "utf8");
2728
3123
  try {
2729
3124
  const parsed = JSON.parse(raw);
2730
3125
  if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") throw new Error("shape");
@@ -2787,7 +3182,87 @@ async function disconnect(name, opts) {
2787
3182
  console.log(
2788
3183
  `${affectedAgents.length} agent${affectedAgents.length === 1 ? "" : "s"} still point at it and will fail: ` + affectedAgents.map((a) => a.agentId ?? a.id).join(", ")
2789
3184
  );
2790
- console.log("Switch them to Platform in each agent's Configure tab (or reconnect).");
3185
+ console.log(`Move them to platform credits with \`sky auth switch-to-credits ${name}\`, or reconnect.`);
3186
+ }
3187
+ }
3188
+ async function browserSignIn(name, opts) {
3189
+ const provider = managedProviderFor(name);
3190
+ const base = `/model-subscriptions/${provider}/logins`;
3191
+ const readCode = opts.readCode ?? ask;
3192
+ let { login: login2 } = await api("POST", base, {
3193
+ ...opts.account ? { accountId: opts.account } : {},
3194
+ ...opts.label ? { label: opts.label } : {}
3195
+ });
3196
+ let shown = false;
3197
+ let codeSent = false;
3198
+ const cancel = () => {
3199
+ if ("loginId" in login2 && login2.loginId) {
3200
+ void api("DELETE", `${base}/${encodeURIComponent(login2.loginId)}`).catch(() => void 0);
3201
+ }
3202
+ };
3203
+ process.once("SIGINT", cancel);
3204
+ try {
3205
+ for (; ; ) {
3206
+ if (login2.state === "connected" || login2.state === "cleanup_pending") {
3207
+ if (login2.state === "cleanup_pending") cancel();
3208
+ return login2.subscription;
3209
+ }
3210
+ if (login2.state === "failed") {
3211
+ if (login2.loginId) cancel();
3212
+ throw new ApiError(422, "sign_in_failed", login2.reason);
3213
+ }
3214
+ if (login2.state === "waiting" && !shown) {
3215
+ shown = true;
3216
+ console.log(`Sign in to ${MODEL_SUBSCRIPTION_PROVIDER_NAME[provider]} at:
3217
+ ${login2.url}`);
3218
+ if (login2.userCode) console.log(`Confirm this code on that page: ${login2.userCode}`);
3219
+ if (opts.browser !== false) openBrowser2(login2.url);
3220
+ }
3221
+ if (login2.state === "waiting" && login2.acceptsCode && !codeSent) {
3222
+ const code = await readCode("Paste the code the sign-in page shows: ");
3223
+ if (!code) throw new ApiError(400, "no_code", "no code provided");
3224
+ await api("POST", `${base}/${encodeURIComponent(login2.loginId)}/code`, { code });
3225
+ codeSent = true;
3226
+ } else {
3227
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
3228
+ }
3229
+ ({ login: login2 } = await api("GET", `${base}/${encodeURIComponent(login2.loginId)}`));
3230
+ }
3231
+ } finally {
3232
+ process.removeListener("SIGINT", cancel);
3233
+ }
3234
+ }
3235
+ async function subscriptionUsage(name, account) {
3236
+ const provider = managedProviderFor(name);
3237
+ const { usage } = await api("GET", `/model-subscriptions/${provider}/usage${accountQuery(account)}`);
3238
+ return usage;
3239
+ }
3240
+ function renderSubscriptionUsage(name, usage) {
3241
+ const label = MODEL_SUBSCRIPTION_PROVIDER_NAME[managedProviderFor(name)];
3242
+ if (usage.status === "unavailable") return [`${label}: usage unavailable (${usage.reason ?? "unknown"})`];
3243
+ const head = [`${label}${usage.plan ? ` ${usage.plan}` : ""}`, usage.allowed === false ? "limit reached" : null].filter(Boolean).join(" \xB7 ");
3244
+ const rows = usage.windows.map(
3245
+ (w) => ` ${w.label}: ${Math.round(w.remainingPercent)}% left${w.resetsAt ? ` \xB7 resets ${w.resetsAt.slice(0, 16).replace("T", " ")}` : ""}`
3246
+ );
3247
+ return [head, ...rows.length ? rows : [" no usage windows reported"]];
3248
+ }
3249
+ async function switchToCredits(name, opts) {
3250
+ const provider = managedProviderFor(name);
3251
+ const label = MODEL_SUBSCRIPTION_PROVIDER_NAME[provider];
3252
+ if (!opts.yes) {
3253
+ const ok = await confirm2(`Move every agent on this ${label} account to platform credits? Their running sessions restart.`);
3254
+ if (!ok) {
3255
+ console.log("Aborted.");
3256
+ return;
3257
+ }
3258
+ }
3259
+ const result = await api(
3260
+ "POST",
3261
+ `/model-subscriptions/${provider}/switch-to-credits${accountQuery(opts.account)}`
3262
+ );
3263
+ console.log(`Moved ${result.switched} agent${result.switched === 1 ? "" : "s"} to platform credits.`);
3264
+ if (result.restartFailures.length) {
3265
+ console.log(`Couldn't restart: ${result.restartFailures.join(", ")} \u2014 they pick up credits on their next start.`);
2791
3266
  }
2792
3267
  }
2793
3268
 
@@ -2803,11 +3278,11 @@ function planProviders(names2) {
2803
3278
  }));
2804
3279
  }
2805
3280
  function reportPath() {
2806
- return join9(dirname5(configPath()), "setup.json");
3281
+ return join11(dirname6(configPath()), "setup.json");
2807
3282
  }
2808
3283
  function loadReport() {
2809
3284
  try {
2810
- const saved = JSON.parse(readFileSync10(reportPath(), "utf8"));
3285
+ const saved = JSON.parse(readFileSync11(reportPath(), "utf8"));
2811
3286
  return { agents: saved.agents ?? [], plans: saved.plans ?? [], ...saved.secrets ? { secrets: saved.secrets } : {} };
2812
3287
  } catch {
2813
3288
  return { agents: [], plans: [] };
@@ -2820,8 +3295,8 @@ function mergeReport(base, next) {
2820
3295
  return { agents, plans, ...secrets2 ? { secrets: secrets2 } : {} };
2821
3296
  }
2822
3297
  function saveReport(report) {
2823
- mkdirSync2(dirname5(reportPath()), { recursive: true });
2824
- writeFileSync4(reportPath(), `${JSON.stringify(report, null, 2)}
3298
+ mkdirSync3(dirname6(reportPath()), { recursive: true });
3299
+ writeFileSync5(reportPath(), `${JSON.stringify(report, null, 2)}
2825
3300
  `, { mode: 384 });
2826
3301
  }
2827
3302
  function confirm3(question) {
@@ -2908,7 +3383,7 @@ async function connectedProviders() {
2908
3383
  var WORKSPACE_VAULT_NAME = "default";
2909
3384
  async function findWorkspaceVault() {
2910
3385
  const { vaults } = await api("GET", "/vaults");
2911
- return vaults.find((vault) => vault.display_name === WORKSPACE_VAULT_NAME)?.id ?? null;
3386
+ return vaults.find((vault2) => vault2.display_name === WORKSPACE_VAULT_NAME)?.id ?? null;
2912
3387
  }
2913
3388
  async function workspaceVaultId() {
2914
3389
  const existing = await findWorkspaceVault();
@@ -3170,17 +3645,17 @@ function renderArtifact(artifact2) {
3170
3645
  }
3171
3646
 
3172
3647
  // src/skills-store.ts
3173
- import { readdirSync as readdirSync4, readFileSync as readFileSync11, statSync as statSync5 } from "fs";
3174
- import { basename as basename5, join as join10, relative as relative3, resolve as resolve5 } from "path";
3648
+ import { readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
3649
+ import { basename as basename7, join as join12, relative as relative4, resolve as resolve7 } from "path";
3175
3650
  function skillBundle(dir) {
3176
- const root = resolve5(dir);
3177
- const top = basename5(root);
3651
+ const root = resolve7(dir);
3652
+ const top = basename7(root);
3178
3653
  const files2 = [];
3179
3654
  const walk = (path) => {
3180
- for (const name of readdirSync4(path).sort()) {
3181
- const full = join10(path, name);
3182
- if (statSync5(full).isDirectory()) walk(full);
3183
- else files2.push({ path: `${top}/${relative3(root, full)}`, bytes: readFileSync11(full) });
3655
+ for (const name of readdirSync5(path).sort()) {
3656
+ const full = join12(path, name);
3657
+ if (statSync8(full).isDirectory()) walk(full);
3658
+ else files2.push({ path: `${top}/${relative4(root, full)}`, bytes: readFileSync12(full) });
3184
3659
  }
3185
3660
  };
3186
3661
  walk(root);
@@ -3190,7 +3665,7 @@ function skillBundle(dir) {
3190
3665
  async function uploadSkill(dir, displayName) {
3191
3666
  const form = new FormData();
3192
3667
  for (const file of skillBundle(dir)) form.append("files", new Blob([new Uint8Array(file.bytes)]), file.path);
3193
- form.append("displayName", displayName ?? basename5(resolve5(dir)));
3668
+ form.append("displayName", displayName ?? basename7(resolve7(dir)));
3194
3669
  return apiForm("POST", "/skills", form);
3195
3670
  }
3196
3671
  async function listSkills() {
@@ -3222,9 +3697,9 @@ async function workspaceSlug(explicit) {
3222
3697
  if (current) return current;
3223
3698
  return (await api("GET", "/whoami")).universe.slug;
3224
3699
  }
3225
- var workspacePath = (slug) => `/universes/${encodeURIComponent(slug)}`;
3700
+ var workspacePath2 = (slug) => `/universes/${encodeURIComponent(slug)}`;
3226
3701
  async function listMembers(slug) {
3227
- return api("GET", `${workspacePath(slug)}/members`);
3702
+ return api("GET", `${workspacePath2(slug)}/members`);
3228
3703
  }
3229
3704
  function findMember(members, ref) {
3230
3705
  const needle = ref.toLowerCase();
@@ -3243,20 +3718,20 @@ function inviteBody(emails, options) {
3243
3718
  };
3244
3719
  }
3245
3720
  async function invite(slug, emails, options) {
3246
- return api("POST", `${workspacePath(slug)}/invites`, inviteBody(emails, options));
3721
+ return api("POST", `${workspacePath2(slug)}/invites`, inviteBody(emails, options));
3247
3722
  }
3248
3723
  async function withdrawInvite(slug, inviteId) {
3249
- await api("DELETE", `${workspacePath(slug)}/invites/${encodeURIComponent(inviteId)}`);
3724
+ await api("DELETE", `${workspacePath2(slug)}/invites/${encodeURIComponent(inviteId)}`);
3250
3725
  }
3251
3726
  async function setRole(slug, memberRef, role) {
3252
3727
  if (role !== "admin" && role !== "member") throw new ApiError(400, "invalid_request", "a role is admin or member");
3253
3728
  const member = findMember((await listMembers(slug)).members, memberRef);
3254
- await api("PATCH", `${workspacePath(slug)}/members/${encodeURIComponent(member.user_id)}`, { role });
3729
+ await api("PATCH", `${workspacePath2(slug)}/members/${encodeURIComponent(member.user_id)}`, { role });
3255
3730
  return member;
3256
3731
  }
3257
3732
  async function removeMember(slug, memberRef) {
3258
3733
  const member = findMember((await listMembers(slug)).members, memberRef);
3259
- await api("DELETE", `${workspacePath(slug)}/members/${encodeURIComponent(member.user_id)}`);
3734
+ await api("DELETE", `${workspacePath2(slug)}/members/${encodeURIComponent(member.user_id)}`);
3260
3735
  return member;
3261
3736
  }
3262
3737
  async function joinWorkspace(token) {
@@ -3289,7 +3764,7 @@ function policyRuleBody(options) {
3289
3764
  ...options.disabled ? { enabled: false } : {}
3290
3765
  };
3291
3766
  }
3292
- var policiesPath = (scope) => "session" in scope ? `/sessions/${encodeURIComponent(scope.session)}/policies` : `${workspacePath(scope.slug)}/policies`;
3767
+ var policiesPath = (scope) => "session" in scope ? `/sessions/${encodeURIComponent(scope.session)}/policies` : `${workspacePath2(scope.slug)}/policies`;
3293
3768
  async function listPolicies(scope, effective = false) {
3294
3769
  if (effective && "session" in scope) return api("GET", `/sessions/${encodeURIComponent(scope.session)}/effective-policy`);
3295
3770
  return api("GET", policiesPath(scope));
@@ -3353,20 +3828,99 @@ function typedConfirmation(slug) {
3353
3828
  });
3354
3829
  }
3355
3830
 
3831
+ // src/vaults.ts
3832
+ import { createInterface as createInterface5 } from "readline";
3833
+ var vaultPath = (id) => `/vaults/${encodeURIComponent(id)}`;
3834
+ function secretLine(s) {
3835
+ const target = s.secret_name ? `$${s.secret_name}` : s.mcp_server_url ?? "";
3836
+ return `${s.id} ${s.auth_type.padEnd(20)} ${s.display_name} ${target} ${s.status}${s.expires_at ? ` expires ${s.expires_at.slice(0, 10)}` : ""}`;
3837
+ }
3838
+ async function readSecretValue(prompt, input = process.stdin) {
3839
+ if (!input.isTTY) {
3840
+ const chunks = [];
3841
+ for await (const chunk of input) chunks.push(chunk);
3842
+ return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
3843
+ }
3844
+ const rl = createInterface5({ input, output: process.stdout, terminal: true });
3845
+ const write = rl._writeToOutput.bind(rl);
3846
+ let prompted = false;
3847
+ rl._writeToOutput = (s) => {
3848
+ if (!prompted) {
3849
+ prompted = true;
3850
+ write(s);
3851
+ }
3852
+ };
3853
+ const value = await new Promise((done) => rl.question(prompt, done));
3854
+ rl.close();
3855
+ process.stdout.write("\n");
3856
+ return value;
3857
+ }
3858
+ function secretBody(input) {
3859
+ if (input.kind === "mcp") {
3860
+ return { display_name: input.displayName ?? new URL(input.url).host, auth: { type: "static_bearer", mcp_server_url: input.url, token: input.token } };
3861
+ }
3862
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(input.name)) {
3863
+ throw new ApiError(400, "invalid_name", `"${input.name}" is not an environment variable name`);
3864
+ }
3865
+ if (!input.hosts.length) throw new ApiError(400, "no_hosts", "name at least one --host the secret may be sent to");
3866
+ const injection = {
3867
+ header: true,
3868
+ body: false,
3869
+ ...input.header ? { header_name: input.header } : {},
3870
+ ...input.template ? { value_template: input.template } : {}
3871
+ };
3872
+ return {
3873
+ display_name: input.displayName ?? input.name,
3874
+ auth: { type: "environment_variable", secret_name: input.name, value: input.value, allowed_hosts: input.hosts, injection_location: injection }
3875
+ };
3876
+ }
3877
+ async function listVaults() {
3878
+ return (await api("GET", "/vaults")).vaults;
3879
+ }
3880
+ async function listSecrets(vaultId) {
3881
+ return (await api("GET", `${vaultPath(vaultId)}/secrets`)).secrets;
3882
+ }
3883
+ async function addSecret(vaultId, input) {
3884
+ return (await api("POST", `${vaultPath(vaultId)}/secrets`, secretBody(input))).secret;
3885
+ }
3886
+ async function updateSecret(vaultId, secretId, patch) {
3887
+ const body = {
3888
+ ...patch.value !== void 0 ? { value: patch.value } : {},
3889
+ ...patch.token !== void 0 ? { token: patch.token } : {},
3890
+ ...patch.hosts?.length ? { allowed_hosts: patch.hosts } : {},
3891
+ ...patch.displayName ? { display_name: patch.displayName } : {}
3892
+ };
3893
+ if (!Object.keys(body).length) throw new ApiError(400, "nothing_to_change", "pass a new value, --host or --name");
3894
+ return (await api("PATCH", `${vaultPath(vaultId)}/secrets/${encodeURIComponent(secretId)}`, body)).secret;
3895
+ }
3896
+ async function moveSecrets(intoVault, from, dryRun) {
3897
+ const result = await api("POST", `${vaultPath(intoVault)}/secrets/move`, { from, ...dryRun ? { dry_run: true } : {} });
3898
+ return result.secrets;
3899
+ }
3900
+ function confirmPrompt(question) {
3901
+ return new Promise((done) => {
3902
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
3903
+ rl.question(`${question} [y/N] `, (answer) => {
3904
+ rl.close();
3905
+ done(/^y(es)?$/i.test(answer.trim()));
3906
+ });
3907
+ });
3908
+ }
3909
+
3356
3910
  // src/harness.ts
3357
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
3358
- import { dirname as dirname6, join as join11, resolve as resolve6 } from "path";
3911
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
3912
+ import { dirname as dirname7, join as join13, resolve as resolve8 } from "path";
3359
3913
  var HARNESS_FILE = "harness.json";
3360
3914
  function harnessFilePath(dir) {
3361
- return join11(resolve6(dir), HARNESS_FILE);
3915
+ return join13(resolve8(dir), HARNESS_FILE);
3362
3916
  }
3363
3917
  function readHarnessFile(dir) {
3364
3918
  const path = harnessFilePath(dir);
3365
3919
  if (!existsSync9(path)) {
3366
- throw new Error(`no ${HARNESS_FILE} found in ${resolve6(dir)} \u2014 run \`sky harness init\` first`);
3920
+ throw new Error(`no ${HARNESS_FILE} found in ${resolve8(dir)} \u2014 run \`sky harness init\` first`);
3367
3921
  }
3368
3922
  try {
3369
- return JSON.parse(readFileSync12(path, "utf8"));
3923
+ return JSON.parse(readFileSync13(path, "utf8"));
3370
3924
  } catch (err) {
3371
3925
  throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
3372
3926
  }
@@ -3396,8 +3950,8 @@ function harnessInit(dir) {
3396
3950
  if (existsSync9(path)) {
3397
3951
  throw new Error(`${path} already exists \u2014 delete it to re-init`);
3398
3952
  }
3399
- mkdirSync3(dirname6(path), { recursive: true });
3400
- writeFileSync5(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
3953
+ mkdirSync4(dirname7(path), { recursive: true });
3954
+ writeFileSync6(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
3401
3955
  `);
3402
3956
  console.log(`Created ${path}`);
3403
3957
  console.log(INIT_COMMENT);
@@ -3534,6 +4088,18 @@ program.hook("preAction", () => {
3534
4088
  const workspace2 = program.opts().workspace;
3535
4089
  if (workspace2) process.env.SKY_WORKSPACE = workspace2;
3536
4090
  });
4091
+ var lastProgressDecile = -1;
4092
+ function uploadProgress(sent, total) {
4093
+ const percent = Math.floor(sent / total * 100);
4094
+ const line = `uploading ${(sent / 1024 / 1024).toFixed(0)} / ${(total / 1024 / 1024).toFixed(0)} MiB (${percent}%)`;
4095
+ if (process.stderr.isTTY) {
4096
+ process.stderr.write(`\r${line}${sent >= total ? "\n" : ""}`);
4097
+ } else if (Math.floor(percent / 10) !== lastProgressDecile) {
4098
+ lastProgressDecile = Math.floor(percent / 10);
4099
+ process.stderr.write(`${line}
4100
+ `);
4101
+ }
4102
+ }
3537
4103
  function fail(error) {
3538
4104
  if (error instanceof ApiError) console.error(`sky: ${error.message} (${error.code})`);
3539
4105
  else console.error(`sky: ${error instanceof Error ? error.message : String(error)}`);
@@ -3556,13 +4122,26 @@ auth.command("whoami").description("who the current token acts as").action(
3556
4122
  );
3557
4123
  })
3558
4124
  );
3559
- auth.command("connect <provider>").description("connect a model subscription: claude (setup-token) or chatgpt (Codex sign-in)").option("-y, --yes", "skip the consent prompt").action(
4125
+ 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(
3560
4126
  run(async (name, opts) => {
4127
+ if (opts.browser) {
4128
+ const sub = await browserSignIn(name, { account: opts.account, label: opts.label, browser: opts.open });
4129
+ console.log(`Connected ${MODEL_SUBSCRIPTION_PROVIDER_NAME[sub.provider]} (${sub.label}). Powers new eligible agents.`);
4130
+ return;
4131
+ }
3561
4132
  const provider = providerFor(name);
3562
4133
  if (provider === "anthropic") await connectClaude();
3563
4134
  else await connectChatgpt(opts);
3564
4135
  })
3565
4136
  );
4137
+ auth.command("usage <provider>").description("plan allowance left on a connected model subscription").option("--account <id>", "an additional account (default: the provider's first account)").option("--json", "print the raw usage").action(
4138
+ run(async (name, opts) => {
4139
+ const usage = await subscriptionUsage(name, opts.account);
4140
+ if (opts.json) console.log(JSON.stringify(usage, null, 2));
4141
+ else for (const line of renderSubscriptionUsage(name, usage)) console.log(line);
4142
+ })
4143
+ );
4144
+ 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)));
3566
4145
  auth.command("accounts").description("list connected model subscriptions and the agents they power").action(run(async () => listSubscriptions()));
3567
4146
  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)));
3568
4147
  var universe = program.command("universe").description("grouping boundary for agents");
@@ -3774,7 +4353,7 @@ agent.command("destroy <id>").description("delete the spec; requires all session
3774
4353
  console.log(`Destroyed ${id}`);
3775
4354
  })
3776
4355
  );
3777
- 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").action(
4356
+ 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(
3778
4357
  run(
3779
4358
  async (id, text, opts) => sendAndFollow(await resolveSessionRef(id), text ?? "", opts)
3780
4359
  )
@@ -3884,7 +4463,7 @@ session.command("delete <id>").description("permanently delete the session, its
3884
4463
  console.log(`Deleted ${id}`);
3885
4464
  })
3886
4465
  );
3887
- 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").action(
4466
+ 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(
3888
4467
  run(
3889
4468
  async (ref, text, opts) => sendAndFollow(await resolveSessionRef(ref), text ?? "", opts)
3890
4469
  )
@@ -4026,6 +4605,78 @@ sessionPorts.command("revoke <ref> <port>").description("stop exposing a port; i
4026
4605
  console.log(`Revoked port ${port}`);
4027
4606
  })
4028
4607
  );
4608
+ session.command("access <ref>").description("who may use the session; --private/--members and --add-viewer/--remove-viewer change it (owner)").option("--private", "only you can use it").option("--members", "every member of its workspace can read and prompt it").option("--add-viewer <email...>", "people who may read its messages from any workspace").option("--remove-viewer <email...>", "take viewers off").action(
4609
+ run(async (ref, opts) => {
4610
+ const id = await resolveSessionRef(ref);
4611
+ const patch = accessPatch(opts);
4612
+ const path = `/sessions/${encodeURIComponent(id)}/access`;
4613
+ const access = await api(Object.keys(patch).length ? "PATCH" : "GET", path, Object.keys(patch).length ? patch : void 0);
4614
+ for (const line of renderAccess(access)) console.log(line);
4615
+ })
4616
+ );
4617
+ session.command("shared").description("sessions other people shared with you to read, newest first").option("--limit <n>", "at most this many (1\u2013100, default 50)").action(
4618
+ run(async (opts) => {
4619
+ const { sessions } = await api("GET", `/shared-sessions${opts.limit ? `?limit=${encodeURIComponent(opts.limit)}` : ""}`);
4620
+ if (!sessions.length) console.log("nobody has shared a session with you");
4621
+ for (const s of sessions) console.log(`${s.id} ${s.agent_type.padEnd(12)} ${s.title ?? "(untitled)"} from ${s.owner.name ?? s.owner.email ?? "unknown"} ${s.shared_at.slice(0, 10)}`);
4622
+ })
4623
+ );
4624
+ session.command("handoff <ref> <targetAgent>").description("hand the session's context to another agent (agent id or catalog handle); prints the new session").option("--from-message <id>", "carry the conversation up to this message only").option("--no-wait", "return once the handoff is accepted").action(
4625
+ run(async (ref, targetAgent, opts) => {
4626
+ const id = await resolveSessionRef(ref);
4627
+ const done = await handoff(id, targetAgent, {
4628
+ fromMessage: opts.fromMessage,
4629
+ wait: opts.wait !== false,
4630
+ onStatus: (h) => console.error(`${h.id}: ${h.status}`)
4631
+ });
4632
+ if (done.status === "failed") throw new ApiError(422, done.error?.code ?? "handoff_failed", done.error?.message ?? "the handoff failed");
4633
+ if (done.target_session) console.log(`${done.target_session} (${done.resolved_mode === "native_fork" ? "forked" : "from a written summary"})`);
4634
+ })
4635
+ );
4636
+ var sessionWorkspace = session.command("workspace").description("the session's workspace: list and read files, see changes, take a patch, copy a folder in");
4637
+ sessionWorkspace.command("ls <ref> [path]").description("list a directory (default: the workspace root)").option("--hidden", "include dotfiles").action(
4638
+ run(async (ref, path, opts) => {
4639
+ const listing = await listFiles(await resolveSessionRef(ref), path, Boolean(opts.hidden));
4640
+ for (const e of listing.entries) console.log(entryLine(e));
4641
+ if (listing.truncated) console.error(`(only the first ${listing.entries.length} entries)`);
4642
+ })
4643
+ );
4644
+ sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1 MB), or save it with --out").option("--out <file>", "write it to this local file").action(
4645
+ run(async (ref, path, opts) => {
4646
+ const bytes = await readFile(await resolveSessionRef(ref), path);
4647
+ if (opts.out) {
4648
+ writeFileSync7(opts.out, bytes);
4649
+ console.error(`saved ${bytes.length} bytes to ${opts.out}`);
4650
+ } else process.stdout.write(bytes);
4651
+ })
4652
+ );
4653
+ sessionWorkspace.command("changes <ref>").description("git status and diff summary of each repository in the workspace").action(
4654
+ run(async (ref) => {
4655
+ const changes = await api("GET", `/sessions/${encodeURIComponent(await resolveSessionRef(ref))}/workspace/changes`);
4656
+ for (const line of renderChanges(changes)) console.log(line);
4657
+ })
4658
+ );
4659
+ sessionWorkspace.command("patch <ref> [repo]").description("the work in a repository since its base branch, as a git patch (default repo: the root)").option("--out <file>", "write the patch to this file instead of stdout").action(
4660
+ run(async (ref, repo, opts) => {
4661
+ const { meta, patch } = await workspacePatch(await resolveSessionRef(ref), repo ?? ".");
4662
+ 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)})`);
4663
+ if (!patch.length) console.error("nothing changed");
4664
+ else if (opts.out) {
4665
+ writeFileSync7(opts.out, patch);
4666
+ console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
4667
+ } else process.stdout.write(patch);
4668
+ })
4669
+ );
4670
+ sessionWorkspace.command("import <ref> <dir>").description("copy a local folder into the workspace as a git repository (files git ignores are left out)").option("--name <name>", "its folder name in the workspace (default: the local folder's name)").action(
4671
+ run(async (ref, dir, opts) => {
4672
+ const imported = await importFolder(await resolveSessionRef(ref), dir, { name: opts.name });
4673
+ console.log(`${imported.path} (${imported.files} files)`);
4674
+ })
4675
+ );
4676
+ function interactionMode(opts) {
4677
+ if (opts.plan && opts.build) throw new ApiError(400, "invalid_request", "pass --plan or --build, not both");
4678
+ return opts.plan ? "plan" : opts.build ? "build" : void 0;
4679
+ }
4029
4680
  async function uploadAttachments(opts) {
4030
4681
  const files2 = [];
4031
4682
  for (const path of [...opts.image ?? [], ...opts.file ?? []]) {
@@ -4042,7 +4693,8 @@ function turnOver(frame) {
4042
4693
  return stopReason?.type !== "requires_action";
4043
4694
  }
4044
4695
  async function sendAndFollow(sessionId, text, opts) {
4045
- const events = eventsFor(text, await uploadAttachments(opts), opts.system);
4696
+ const mode = interactionMode(opts);
4697
+ const events = eventsFor(text, await uploadAttachments(opts), opts.system, mode);
4046
4698
  const controller = new AbortController();
4047
4699
  let opened = null;
4048
4700
  if (!opts.detach) {
@@ -4067,7 +4719,7 @@ async function sendAndFollow(sessionId, text, opts) {
4067
4719
  async function chatLoop(sessionId, opts = {}) {
4068
4720
  console.log(`Chatting with ${sessionId} \u2014 Ctrl-C to leave.`);
4069
4721
  const controller = new AbortController();
4070
- const rl = createInterface5({ input: process.stdin, output: process.stdout, prompt: "> " });
4722
+ const rl = createInterface6({ input: process.stdin, output: process.stdout, prompt: "> " });
4071
4723
  let system = opts.system;
4072
4724
  void (async () => {
4073
4725
  try {
@@ -4181,9 +4833,9 @@ secrets.command("unset <id> <key>").action(
4181
4833
  );
4182
4834
  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(
4183
4835
  run(async (opts) => {
4184
- const path = resolve7(opts.file);
4836
+ const path = resolve9(opts.file);
4185
4837
  if (!existsSync10(path)) {
4186
- const name = basename6(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
4838
+ const name = basename8(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
4187
4839
  scaffoldAgentFile(path, name);
4188
4840
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
4189
4841
  }
@@ -4214,7 +4866,7 @@ program.command("launch").description("scaffold ./agent.toml if absent, create t
4214
4866
  );
4215
4867
  program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
4216
4868
  run(async (opts) => {
4217
- const path = resolve7(opts.file);
4869
+ const path = resolve9(opts.file);
4218
4870
  const { file, spec } = readAgentFile(path);
4219
4871
  if (!file.id) {
4220
4872
  console.log("No agent id in the file yet \u2014 running launch instead.");
@@ -4315,13 +4967,13 @@ program.command("import").description("import a local Claude Code or Codex sessi
4315
4967
  console.log(` ${ws}`);
4316
4968
  });
4317
4969
  console.log();
4318
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
4970
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
4319
4971
  const answer = await new Promise(
4320
- (resolve8) => rl.question(
4972
+ (resolve10) => rl.question(
4321
4973
  `Import which session? [1-${candidates.length}, or comma-separated, or "all"]: `,
4322
4974
  (a) => {
4323
4975
  rl.close();
4324
- resolve8(a.trim());
4976
+ resolve10(a.trim());
4325
4977
  }
4326
4978
  )
4327
4979
  );
@@ -4343,10 +4995,10 @@ program.command("import").description("import a local Claude Code or Codex sessi
4343
4995
  }
4344
4996
  if (!autoConfirm) {
4345
4997
  if (nonInteractive) throw new Error("Use --yes to confirm this transfer in a non-interactive terminal.");
4346
- const prompt = createInterface5({ input: process.stdin, output: process.stdout });
4347
- const answer = await new Promise((resolve8) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
4998
+ const prompt = createInterface6({ input: process.stdin, output: process.stdout });
4999
+ const answer = await new Promise((resolve10) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
4348
5000
  prompt.close();
4349
- resolve8(value);
5001
+ resolve10(value);
4350
5002
  }));
4351
5003
  if (!/^y(?:es)?$/i.test(answer.trim())) {
4352
5004
  console.log("Cancelled. Nothing uploaded.");
@@ -4400,7 +5052,7 @@ Importing ${label} \u2026
4400
5052
  var files = program.command("files").description("files: upload once, reference by id from any message");
4401
5053
  files.command("upload <path>").description("upload a local file; prints the file id to reference from a message").option("--expires <seconds>", "expire the upload after this many seconds (3600\u20137776000)").action(
4402
5054
  run(async (path, opts) => {
4403
- const file = await uploadLocalFile(path, opts.expires ? Number(opts.expires) : void 0);
5055
+ const file = await uploadLocalFile(path, opts.expires ? Number(opts.expires) : void 0, uploadProgress);
4404
5056
  console.log(`${file.id} ${file.filename} ${file.mime_type} ${file.size_bytes} bytes`);
4405
5057
  })
4406
5058
  );
@@ -4431,9 +5083,8 @@ files.command("rm <id>").description("delete a file (a session that already rece
4431
5083
  );
4432
5084
  files.command("download <id>").description("download a session file's bytes").requiredOption("-o, --out <path>", "where to write the bytes").action(
4433
5085
  run(async (id, opts) => {
4434
- const { bytes } = await downloadFile(id);
4435
- writeFileSync6(opts.out, bytes);
4436
- console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
5086
+ const written = await downloadFileTo(id, opts.out);
5087
+ console.log(`Wrote ${written} bytes to ${opts.out}`);
4437
5088
  })
4438
5089
  );
4439
5090
  var task = program.command("task").description("a task's shared files and open requests (the task is its first session's id)");
@@ -4448,14 +5099,14 @@ task.command("get <task> <path>").description("read one task file; prints it, or
4448
5099
  run(async (taskId, path, opts) => {
4449
5100
  const bytes = await readTaskFile(taskId, path);
4450
5101
  if (opts.out) {
4451
- writeFileSync6(opts.out, bytes);
5102
+ writeFileSync7(opts.out, bytes);
4452
5103
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
4453
5104
  } else process.stdout.write(bytes);
4454
5105
  })
4455
5106
  );
4456
5107
  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(
4457
5108
  run(async (taskId, path, local) => {
4458
- const { file, conflict } = await writeTaskFile(taskId, path, readFileSync13(local), mediaTypeFor(local));
5109
+ const { file, conflict } = await writeTaskFile(taskId, path, readFileSync14(local), mediaTypeFor(local));
4459
5110
  console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
4460
5111
  })
4461
5112
  );
@@ -4521,7 +5172,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
4521
5172
  run(async (id, path, opts) => {
4522
5173
  const bytes = await readArtifactFile(id, path);
4523
5174
  if (opts.out) {
4524
- writeFileSync6(opts.out, bytes);
5175
+ writeFileSync7(opts.out, bytes);
4525
5176
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
4526
5177
  } else process.stdout.write(bytes);
4527
5178
  })
@@ -4633,6 +5284,50 @@ routine.command("runs [id]").description("every fire attempt, newest first \u201
4633
5284
  }
4634
5285
  })
4635
5286
  );
5287
+ routine.command("inbound-url <id>").description("a URL that starts a run when posted to, for services that can't hold an API token").option("--create", "make one (replaces any earlier URL); the URL is printed once").option("--rm", "remove it; posting to it stops working").action(
5288
+ run(async (id, opts) => {
5289
+ const res = await triggerRequest(id, "inbound-url", { create: opts.create, remove: opts.rm });
5290
+ console.log(opts.rm ? res.removed ? "Removed the inbound URL" : "There was no inbound URL" : inboundLine(res.inbound_url ?? null));
5291
+ })
5292
+ );
5293
+ routine.command("precheck <id> [url]").description("run scheduled fires only when the content at an https URL changed; no url shows the precheck").option("--rm", "remove it: every scheduled fire runs again").action(
5294
+ run(async (id, url, opts) => {
5295
+ const res = await triggerRequest(id, "precheck", { body: url ? { url } : void 0, remove: opts.rm });
5296
+ console.log(opts.rm ? res.removed ? "Removed the precheck" : "There was no precheck" : precheckLine(res.precheck ?? null));
5297
+ })
5298
+ );
5299
+ routine.command("result-email <id> [state]").description("email you each run's result: on or off; no state shows the setting (new-session routines only)").action(
5300
+ run(async (id, state) => {
5301
+ if (state && state !== "on" && state !== "off") throw new ApiError(400, "invalid_request", "state is on or off");
5302
+ const res = await api(
5303
+ state === "on" ? "PUT" : state === "off" ? "DELETE" : "GET",
5304
+ triggerPath(id, "result-email")
5305
+ );
5306
+ console.log(`result email ${res.result_email.enabled ? "on" : "off"}`);
5307
+ })
5308
+ );
5309
+ routine.command("chat-trigger <id>").description("start a run from your own message in a connected chat; no flags shows the trigger").option("--connection <id>", "the chat connection (see `sky channel list`)").option("--thread <id>", "only this conversation (default: every conversation on the connection)").option("--prefix <text>", "only messages starting with this; the rest of the message is the run's input").option("--rm", "remove the trigger").action(
5310
+ run(async (id, opts) => {
5311
+ const setting = Boolean(opts.connection || opts.thread || opts.prefix);
5312
+ const res = await triggerRequest(
5313
+ id,
5314
+ "channel-trigger",
5315
+ { body: setting ? triggerBody(opts) : void 0, remove: opts.rm }
5316
+ );
5317
+ console.log(opts.rm ? res.removed ? "Removed the chat trigger" : "There was no chat trigger" : triggerLine(res.channel_trigger ?? null, res.available ?? true));
5318
+ })
5319
+ );
5320
+ routine.command("deliver-to <id>").description("post each run's answer to a conversation in a connected chat; no flags shows where").option("--connection <id>", "the chat connection (see `sky channel list`)").option("--thread <id>", "the conversation (default: the chat itself)").option("--rm", "stop posting answers").action(
5321
+ run(async (id, opts) => {
5322
+ const setting = Boolean(opts.connection || opts.thread);
5323
+ const res = await triggerRequest(
5324
+ id,
5325
+ "deliver-to",
5326
+ { body: setting ? deliverBody(opts) : void 0, remove: opts.rm }
5327
+ );
5328
+ console.log(opts.rm ? res.removed ? "Stopped posting answers" : "Answers were posted nowhere" : deliverLine(res.deliver_to ?? null));
5329
+ })
5330
+ );
4636
5331
  var collect = (value, previous = []) => [...previous, value];
4637
5332
  function goalPath(sessionId, suffix = "") {
4638
5333
  return `/sessions/${encodeURIComponent(sessionId)}/goal${suffix}`;
@@ -4775,7 +5470,7 @@ usageOptions(
4775
5470
  run(async (opts) => {
4776
5471
  const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
4777
5472
  if (opts.out) {
4778
- writeFileSync6(opts.out, bytes);
5473
+ writeFileSync7(opts.out, bytes);
4779
5474
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
4780
5475
  } else {
4781
5476
  process.stdout.write(bytes);
@@ -4868,6 +5563,189 @@ webhook.command("rm <id>").description("delete an endpoint").action(
4868
5563
  console.log(`Deleted ${id}`);
4869
5564
  })
4870
5565
  );
5566
+ var channel = program.command("channel").description("chat surfaces (Slack, Telegram, Discord, WhatsApp, iMessage, \u2026) routed to a session");
5567
+ channel.command("list").description("your connections and the session each routes to").action(
5568
+ run(async () => {
5569
+ const connections = await listConnections();
5570
+ if (!connections.length) console.log("no channels \u2014 connect one with `sky channel connect <platform> --session sess_\u2026`");
5571
+ for (const c of connections) console.log(connectionLine(c));
5572
+ })
5573
+ );
5574
+ channel.command("connect <platform>").description("connect a surface; prints the link or code that finishes it").option("--session <id>", "route it to this session (sess_\u2026) once claimed").option("--webhook <id>", "route it to this channel webhook endpoint instead").option("--app <id>", "run the connection on your own registered bot or number (see `sky channel apps`)").option("--label <label>", "a name for the connection").option("--wait", "wait until the connection is claimed").action(
5575
+ run(async (platform, opts) => {
5576
+ const created = await connect(platform, opts);
5577
+ console.log(connectionLine(created));
5578
+ for (const step of connectSteps(created)) console.log(` ${step}`);
5579
+ if (opts.wait && created.status === "PENDING") {
5580
+ const done = await waitConnected(created.id);
5581
+ console.log(connectionLine(done));
5582
+ }
5583
+ })
5584
+ );
5585
+ channel.command("bind <connectionId>").description("route a connection to a session or a channel webhook endpoint").option("--session <id>", "the session (sess_\u2026)").option("--webhook <id>", "the channel webhook endpoint").action(
5586
+ run(async (connectionId, opts) => {
5587
+ await bind(connectionId, opts);
5588
+ console.log(`${connectionId} \u2192 ${opts.session ?? opts.webhook}`);
5589
+ })
5590
+ );
5591
+ channel.command("send <threadId> <text...>").description("post a markdown message into a connected thread").action(
5592
+ run(async (threadId, text) => {
5593
+ await send(threadId, text.join(" "));
5594
+ console.log(`Sent to ${threadId}`);
5595
+ })
5596
+ );
5597
+ channel.command("disconnect <connectionId>").description("disconnect a surface").action(
5598
+ run(async (connectionId) => {
5599
+ await api("DELETE", `/channels/connections/${encodeURIComponent(connectionId)}`);
5600
+ console.log(`Disconnected ${connectionId}`);
5601
+ })
5602
+ );
5603
+ channel.command("apps").description("your own bots and numbers registered as channel apps").action(
5604
+ run(async () => {
5605
+ const { apps } = await api("GET", "/channels/apps");
5606
+ if (!apps.length) console.log("no channel apps \u2014 connections use the shared AgentSky bots");
5607
+ for (const a of apps) console.log(`${a.id} ${a.platform.padEnd(9)} ${a.label}${a.status ? ` ${a.status}` : ""}`);
5608
+ })
5609
+ );
5610
+ channel.command("identities").description("chat identities whose messages run as you").action(
5611
+ run(async () => {
5612
+ const { identities } = await api("GET", "/channels/identities");
5613
+ if (!identities.length) console.log("no linked chat identities");
5614
+ for (const i of identities) console.log(`${i.id} ${i.platform.padEnd(9)} ${i.display_name ?? i.external_user_id}${i.linked_at ? ` since ${i.linked_at.slice(0, 10)}` : ""}`);
5615
+ })
5616
+ );
5617
+ channel.command("unlink <identityId>").description("unlink a chat identity; that person's messages stop running as you").action(
5618
+ run(async (identityId) => {
5619
+ await api("DELETE", `/channels/identities/${encodeURIComponent(identityId)}`);
5620
+ console.log(`Unlinked ${identityId}`);
5621
+ })
5622
+ );
5623
+ var connector = program.command("connector").description("apps your agents can use: connect, list, share with a workspace");
5624
+ connector.command("apps [query]").description("apps you can connect, searched by name").option("--limit <n>", "at most this many", (v) => Number.parseInt(v, 10), 50).action(
5625
+ run(async (query, opts) => {
5626
+ for (const a of await listApps(query, opts.limit)) console.log(`${a.slug.padEnd(24)} ${a.name} \u2014 ${a.description}`);
5627
+ })
5628
+ );
5629
+ connector.command("list").description("the accounts you connected").action(
5630
+ run(async () => {
5631
+ const { connections, credit } = await api("GET", "/connectors/connections");
5632
+ if (!connections.length) console.log("no connections \u2014 connect one with `sky connector connect <app>`");
5633
+ for (const c of connections) console.log(connectionLine2(c));
5634
+ if (credit.cap_usd > 0) console.log(`credit earned by connecting: $${credit.earned_usd} of $${credit.cap_usd}`);
5635
+ })
5636
+ );
5637
+ connector.command("connect <app>").description("connect an app in the browser (the slug from `sky connector apps`)").option("--universe <slug>", "connect it for this workspace itself (owner or admin)").option("--no-open", "print the authorization link without opening it").action(
5638
+ run(async (app, opts) => {
5639
+ const connection = await connectApp(app, { workspace: opts.universe, open: opts.open });
5640
+ console.log(`Connected ${connection.app_name} (${connection.label})${opts.universe ? ` for ${opts.universe}` : ""}`);
5641
+ })
5642
+ );
5643
+ connector.command("disconnect <connectionId>").description("disconnect one of your connections; agents lose it at once").action(
5644
+ run(async (connectionId) => {
5645
+ await api("DELETE", `/connectors/connections/${encodeURIComponent(connectionId)}`);
5646
+ console.log(`Disconnected ${connectionId}`);
5647
+ })
5648
+ );
5649
+ connector.command("repos").description("repositories your GitHub connection can reach").action(
5650
+ run(async () => {
5651
+ const { repos } = await api("GET", "/connectors/github/repos");
5652
+ for (const r of repos) console.log(`${r.full_name} ${r.default_branch}${r.private ? " private" : ""}`);
5653
+ })
5654
+ );
5655
+ connector.command("workspace [slug]").description("connections shared with a workspace (default: the current one)").action(
5656
+ run(async (slug) => {
5657
+ const ws = await api("GET", `/universes/${encodeURIComponent(await workspaceSlug(slug))}/connections`);
5658
+ if (!ws.sharing) console.log("this workspace does not take shared connections");
5659
+ for (const c of ws.connections) console.log(workspaceConnectionLine(c));
5660
+ })
5661
+ );
5662
+ connector.command("share <connectionId>").description("share one of your connections with a workspace, so its sessions can call through it").option("--universe <slug>", "the workspace (default: the current one)").action(
5663
+ run(async (connectionId, opts) => {
5664
+ const slug = await workspaceSlug(opts.universe);
5665
+ await api("POST", `/universes/${encodeURIComponent(slug)}/connections`, { connection_id: connectionId });
5666
+ console.log(`Shared ${connectionId} with ${slug}`);
5667
+ })
5668
+ );
5669
+ connector.command("unshare <connectionId>").description("stop sharing a connection with a workspace").option("--universe <slug>", "the workspace (default: the current one)").action(
5670
+ run(async (connectionId, opts) => {
5671
+ const slug = await workspaceSlug(opts.universe);
5672
+ await api("DELETE", `/universes/${encodeURIComponent(slug)}/connections/${encodeURIComponent(connectionId)}`);
5673
+ console.log(`Stopped sharing ${connectionId} with ${slug}`);
5674
+ })
5675
+ );
5676
+ connector.command("shared").description("everything you share with workspaces, and the calls made through each").action(
5677
+ run(async () => {
5678
+ const { shared } = await api("GET", "/shared-connections");
5679
+ if (!shared.length) console.log("you share no connections");
5680
+ for (const s of shared) console.log(`${s.connection_id} ${s.app_name} (${s.label}) \u2192 ${s.workspace.slug} ${s.use_count} call${s.use_count === 1 ? "" : "s"}`);
5681
+ })
5682
+ );
5683
+ var vault = program.command("vault").description("vaults: credentials a session is granted when it starts (values write-only)");
5684
+ vault.command("list").description("the workspace's vaults").action(
5685
+ run(async () => {
5686
+ const vaults = await listVaults();
5687
+ if (!vaults.length) console.log("no vaults");
5688
+ for (const v of vaults) console.log(`${v.id} ${v.display_name}`);
5689
+ })
5690
+ );
5691
+ vault.command("create <name>").description("create a vault; pass its id as vault_ids when starting a session").action(
5692
+ run(async (name) => {
5693
+ const { vault: created } = await api("POST", "/vaults", { display_name: name });
5694
+ console.log(`${created.id} ${created.display_name}`);
5695
+ })
5696
+ );
5697
+ vault.command("show <vaultId>").description("a vault's secrets (metadata only)").action(
5698
+ run(async (vaultId) => {
5699
+ const secrets2 = await listSecrets(vaultId);
5700
+ if (!secrets2.length) console.log("no secrets");
5701
+ for (const secret of secrets2) console.log(secretLine(secret));
5702
+ })
5703
+ );
5704
+ vault.command("rm <vaultId>").description("delete a vault and every secret in it").option("-y, --yes", "skip the confirmation prompt").action(
5705
+ run(async (vaultId, opts) => {
5706
+ if (!opts.yes && !await confirmPrompt(`Delete ${vaultId} and every secret in it?`)) return void console.log("Aborted.");
5707
+ await api("DELETE", `/vaults/${encodeURIComponent(vaultId)}`);
5708
+ console.log(`Deleted ${vaultId}`);
5709
+ })
5710
+ );
5711
+ vault.command("move <intoVaultId>").description("move the secrets of other vaults into this one").requiredOption("--from <vaultId...>", "the vaults to move out of").option("--dry-run", "only report what would move").action(
5712
+ run(async (intoVaultId, opts) => {
5713
+ for (const m of await moveSecrets(intoVaultId, opts.from, Boolean(opts.dryRun))) {
5714
+ console.log(`${m.outcome.padEnd(8)} ${m.name} from ${m.from}${m.reason ? ` (${m.reason})` : ""}`);
5715
+ }
5716
+ })
5717
+ );
5718
+ var vaultSecret = vault.command("secret").description("the secrets in a vault");
5719
+ vaultSecret.command("add <vaultId> <name>").description("add an environment variable (value from stdin or a hidden prompt), or an MCP server's bearer token with --mcp").option("--host <host...>", "hosts the environment variable may be sent to").option("--header <name>", "the header it is sent in (default: Authorization)").option("--template <template>", "how the header value is built, e.g. 'Bearer {secret}'").option("--mcp <url>", "store a bearer token for this MCP server instead; <name> is its display name").action(
5720
+ run(async (vaultId, name, opts) => {
5721
+ const value = await readSecretValue(opts.mcp ? "Token: " : `Value for ${name}: `);
5722
+ if (!value) throw new ApiError(400, "no_value", "no value provided");
5723
+ const input = opts.mcp ? { kind: "mcp", url: opts.mcp, token: value, displayName: name } : { kind: "env", name, value, hosts: opts.host ?? [], header: opts.header, template: opts.template };
5724
+ console.log(secretLine(await addSecret(vaultId, input)));
5725
+ })
5726
+ );
5727
+ vaultSecret.command("rotate <vaultId> <secretId>").description("replace a secret's value in place (stdin or a hidden prompt); running sessions keep working").option("--host <host...>", "also replace the hosts it may be sent to").option("--keep-value", "only change --host, keep the value").action(
5728
+ run(async (vaultId, secretId, opts) => {
5729
+ const current = (await listSecrets(vaultId)).find((s) => s.id === secretId);
5730
+ if (!current) throw new ApiError(404, "not_found", `no secret ${secretId} in ${vaultId}`);
5731
+ const value = opts.keepValue ? void 0 : await readSecretValue("New value: ");
5732
+ if (value === "") throw new ApiError(400, "no_value", "no value provided");
5733
+ const field = current.auth_type === "environment_variable" ? "value" : "token";
5734
+ console.log(secretLine(await updateSecret(vaultId, secretId, { [field]: value, hosts: opts.host })));
5735
+ })
5736
+ );
5737
+ vaultSecret.command("rm <vaultId> <secretId>").description("delete a secret").action(
5738
+ run(async (vaultId, secretId) => {
5739
+ await api("DELETE", `/vaults/${encodeURIComponent(vaultId)}/secrets/${encodeURIComponent(secretId)}`);
5740
+ console.log(`Deleted ${secretId}`);
5741
+ })
5742
+ );
5743
+ vaultSecret.command("validate <vaultId> <secretId>").description("check that an MCP OAuth grant still works").action(
5744
+ run(async (vaultId, secretId) => {
5745
+ const { result } = await api("POST", `/vaults/${encodeURIComponent(vaultId)}/secrets/${encodeURIComponent(secretId)}/validate`);
5746
+ console.log(`${secretId}: ${result}`);
5747
+ })
5748
+ );
4871
5749
  var harness = program.command("harness").description("workspace harnesses: init, verify, push");
4872
5750
  harness.command("init [dir]").description("write a harness.json profile skeleton in the given directory (default: .)").action(run(async (dir) => {
4873
5751
  harnessInit(dir ?? ".");
@@ -4891,7 +5769,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
4891
5769
  }));
4892
5770
  var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
4893
5771
  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) => {
4894
- const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-PMUETYJS.js");
5772
+ const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-IOARVWJ6.js");
4895
5773
  if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
4896
5774
  const scopes = parseContentScopes(opts.capture);
4897
5775
  const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
@@ -4920,13 +5798,13 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
4920
5798
  ]);
4921
5799
  console.log(`Tracing ${trace.id} as ${opts.source} until ${grant.expires_at}`);
4922
5800
  console.log(scopes.length ? ` content: ${scopes.join(", ")} (redacted here before it leaves)` : " metadata only");
4923
- if (opts.hooks) console.log(` hooks: ${installClaudeHooks(resolve7(typeof opts.hooks === "string" ? opts.hooks : ".claude/settings.local.json"))}`);
5801
+ if (opts.hooks) console.log(` hooks: ${installClaudeHooks(resolve9(typeof opts.hooks === "string" ? opts.hooks : ".claude/settings.local.json"))}`);
4924
5802
  console.log(` a session started elsewhere joins it with metadata {"trace_id": "${trace.id}"}`);
4925
5803
  console.log(` sky trace show ${trace.id}`);
4926
5804
  }));
4927
5805
  traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
4928
5806
  try {
4929
- const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-PMUETYJS.js");
5807
+ const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-IOARVWJ6.js");
4930
5808
  const chunks = [];
4931
5809
  for await (const chunk of process.stdin) chunks.push(chunk);
4932
5810
  const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
@@ -4939,14 +5817,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
4939
5817
  }
4940
5818
  });
4941
5819
  traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
4942
- const { readState, flush, grantSender } = await import("./trace-PMUETYJS.js");
5820
+ const { readState, flush, grantSender } = await import("./trace-IOARVWJ6.js");
4943
5821
  const state = readState();
4944
5822
  if (!state) throw new Error("no trace is running here");
4945
5823
  const outcome = await flush(grantSender(state), { attempts: 4 });
4946
5824
  console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
4947
5825
  }));
4948
5826
  traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
4949
- const { readState, spoolStatus } = await import("./trace-PMUETYJS.js");
5827
+ const { readState, spoolStatus } = await import("./trace-IOARVWJ6.js");
4950
5828
  const state = readState();
4951
5829
  if (!state) {
4952
5830
  console.log("No trace is running here.");
@@ -4956,7 +5834,7 @@ traceCmd.command("status").description("the running trace and its spool").action
4956
5834
  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`);
4957
5835
  }));
4958
5836
  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) => {
4959
- const { readState } = await import("./trace-PMUETYJS.js");
5837
+ const { readState } = await import("./trace-IOARVWJ6.js");
4960
5838
  const ref = id ?? readState()?.trace_id;
4961
5839
  if (!ref) throw new Error("name a trace, or start one here");
4962
5840
  const bundle = await api("GET", `/traces/${ref}`);
@@ -4983,7 +5861,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
4983
5861
  for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
4984
5862
  }));
4985
5863
  traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
4986
- const { readState, record, flush, grantSender, clearState } = await import("./trace-PMUETYJS.js");
5864
+ const { readState, record, flush, grantSender, clearState } = await import("./trace-IOARVWJ6.js");
4987
5865
  const state = readState();
4988
5866
  if (!state) throw new Error("no trace is running here");
4989
5867
  if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
@@ -4993,7 +5871,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
4993
5871
  console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
4994
5872
  }));
4995
5873
  traceCmd.command("sync [payload]").description("upload this machine's Claude Code and Codex sessions to the matching workspace's trace library, redacted here first").option("--enable", "turn sync on for this machine").option("--disable", "turn sync off; nothing more is uploaded").option("--route <match=workspace>", "send sessions whose folder path contains <match> to <workspace> (repeatable; replaces the saved rules)", collect).option("--default <workspace>", "where a session no rule matches goes").option("--keep-local", "keep sessions no rule matches on this machine (the default)").option("--session <ref>", "only this session: its id or transcript path (repeatable)", collect).option("--full", "re-read every session, not only files that changed").option("--limit <n>", "at most this many changed sessions this run", (v) => Number.parseInt(v, 10)).option("--dry-run", "show what would be uploaded without uploading").option("--hook", "run from a Claude Code SessionEnd hook or Codex notify: upload that session in the background and return at once").option("--install-schedule", "also sweep every 15 minutes (macOS launchd)").option("--status", "what sync has done on this machine").option("--quiet", "print only failures").action(async (payload, opts) => {
4996
- const lib = await import("./trace-library-FIMBRED6.js");
5874
+ const lib = await import("./trace-library-K3NE26GH.js");
4997
5875
  if (opts.hook) {
4998
5876
  try {
4999
5877
  let raw = payload ?? "";
@@ -5041,10 +5919,10 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
5041
5919
  console.log(`A repository's .agentsky.json "workspace" wins over these rules.`);
5042
5920
  }
5043
5921
  if (opts.installSchedule) {
5044
- const { join: join12 } = await import("path");
5922
+ const { join: join14 } = await import("path");
5045
5923
  const { homedir: homedir8 } = await import("os");
5046
5924
  const { execFileSync } = await import("child_process");
5047
- const log = join12(homedir8(), ".config", "sky", "trace-sync.log");
5925
+ const log = join14(homedir8(), ".config", "sky", "trace-sync.log");
5048
5926
  const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
5049
5927
  try {
5050
5928
  execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
@@ -5054,11 +5932,11 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
5054
5932
  console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
5055
5933
  }
5056
5934
  if (opts.status) {
5057
- const { readFileSync: readFileSync14 } = await import("fs");
5058
- const { join: join12 } = await import("path");
5935
+ const { readFileSync: readFileSync15 } = await import("fs");
5936
+ const { join: join14 } = await import("path");
5059
5937
  let state = { sessions: {} };
5060
5938
  try {
5061
- state = JSON.parse(readFileSync14(join12(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
5939
+ state = JSON.parse(readFileSync15(join14(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
5062
5940
  } catch {
5063
5941
  }
5064
5942
  const counts = /* @__PURE__ */ new Map();
@@ -5091,7 +5969,7 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
5091
5969
  })();
5092
5970
  });
5093
5971
  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) => {
5094
- const { libraryRequest } = await import("./trace-library-FIMBRED6.js");
5972
+ const { libraryRequest } = await import("./trace-library-K3NE26GH.js");
5095
5973
  const workspace2 = opts.workspace ?? currentUniverse();
5096
5974
  if (!workspace2) throw new Error("name a workspace with --workspace");
5097
5975
  const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
@@ -5108,7 +5986,7 @@ traceCmd.command("search <query...>").description("search every session in a wor
5108
5986
  if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
5109
5987
  }));
5110
5988
  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) => {
5111
- const { libraryRequest } = await import("./trace-library-FIMBRED6.js");
5989
+ const { libraryRequest } = await import("./trace-library-K3NE26GH.js");
5112
5990
  const workspace2 = opts.workspace ?? currentUniverse();
5113
5991
  if (!workspace2) throw new Error("name a workspace with --workspace");
5114
5992
  const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);