@agentsky/sky 2.9.1 → 2.10.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
@@ -16,7 +16,7 @@ import {
16
16
  skillRefSchema,
17
17
  splitMcpServers,
18
18
  supersededBy
19
- } from "./chunk-YN7PUNIB.js";
19
+ } from "./chunk-5QRKU3BR.js";
20
20
 
21
21
  // src/index.ts
22
22
  import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
@@ -60,11 +60,11 @@ function apiToken() {
60
60
  return process.env.SKY_API_TOKEN ?? loadConfig().token;
61
61
  }
62
62
  function currentUniverse() {
63
- return process.env.SKY_UNIVERSE ?? loadConfig().currentUniverse;
63
+ return process.env.SKY_WORKSPACE ?? process.env.SKY_UNIVERSE ?? loadConfig().currentUniverse;
64
64
  }
65
65
 
66
66
  // src/version.ts
67
- var CLI_VERSION = "2.9.1";
67
+ var CLI_VERSION = "2.10.0";
68
68
  var USER_AGENT = `sky-cli/${CLI_VERSION} (${process.platform}; node/${process.versions.node})`;
69
69
 
70
70
  // src/api.ts
@@ -185,7 +185,7 @@ async function resolveSessionRef(ref) {
185
185
  return agent2.defaultSessionId;
186
186
  }
187
187
  async function* streamEvents(sessionId, signal) {
188
- const { parseSseStream } = await import("./src-QFDLKCFY.js");
188
+ const { parseSseStream } = await import("./src-RYECJN5I.js");
189
189
  const res = await fetch(
190
190
  `${apiBase()}/sessions/${encodeURIComponent(sessionId)}/stream`,
191
191
  {
@@ -559,7 +559,19 @@ function readClaudeCode() {
559
559
  }
560
560
  }
561
561
  const { prompt, truncated } = assemblePrompt(parts);
562
- const { kept, dropped } = splitMcpServers(claudeMcpServers());
562
+ const allServers = claudeMcpServers();
563
+ const { kept, dropped } = splitMcpServers(allServers);
564
+ const secretCandidates = [];
565
+ for (const d of dropped) {
566
+ const def = allServers[d.name];
567
+ if (!def?.env) continue;
568
+ for (const key of d.envKeys) {
569
+ const value = def.env[key];
570
+ if (value !== void 0 && value.trim()) {
571
+ secretCandidates.push({ serverName: d.name, envKey: key, value, url: d.url });
572
+ }
573
+ }
574
+ }
563
575
  return {
564
576
  harness: "claude_code",
565
577
  configRoot,
@@ -568,7 +580,8 @@ function readClaudeCode() {
568
580
  truncated,
569
581
  model: claudeModel(configRoot),
570
582
  keptServers: kept,
571
- droppedServers: dropped
583
+ droppedServers: dropped,
584
+ secretCandidates
572
585
  };
573
586
  }
574
587
  function detectHarness(explicit) {
@@ -602,6 +615,79 @@ function writeLink(configRoot, link) {
602
615
  writeFileSync3(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
603
616
  `);
604
617
  }
618
+ var DEFAULT_VAULT_NAME = "default";
619
+ async function fetchDefaultVaultId() {
620
+ const { vaults } = await api(
621
+ "GET",
622
+ "/vaults"
623
+ );
624
+ return vaults.find((v) => v.display_name === DEFAULT_VAULT_NAME)?.id ?? null;
625
+ }
626
+ async function ensureDefaultVaultId() {
627
+ const existing = await fetchDefaultVaultId();
628
+ if (existing) return existing;
629
+ try {
630
+ const { vault } = await api("POST", "/vaults", {
631
+ display_name: DEFAULT_VAULT_NAME
632
+ });
633
+ return vault.id;
634
+ } catch (error) {
635
+ if (error instanceof ApiError && error.status === 409) {
636
+ const raced = await fetchDefaultVaultId();
637
+ if (raced) return raced;
638
+ }
639
+ throw error;
640
+ }
641
+ }
642
+ async function confirmAndVaultSecrets(candidates, yes) {
643
+ if (!candidates.length) return;
644
+ if (yes) {
645
+ console.log(
646
+ ` ${candidates.length} secret${candidates.length === 1 ? "" : "s"} from dropped MCP servers not saved (--yes skips vault confirmation).`
647
+ );
648
+ return;
649
+ }
650
+ console.log("\nSecret-bearing MCP servers were left out of the clone payload.");
651
+ console.log("You can save their keys to your workspace vault now (values are sent encrypted, never displayed).\n");
652
+ let vaultId;
653
+ let saved = 0;
654
+ let skipped = 0;
655
+ for (const c of candidates) {
656
+ const chosen = await confirm(` Add ${c.envKey} (from ${c.serverName}) to your default vault?`);
657
+ if (!chosen) {
658
+ console.log(` \u2014 skipped ${c.envKey}`);
659
+ skipped += 1;
660
+ continue;
661
+ }
662
+ try {
663
+ vaultId ??= await ensureDefaultVaultId();
664
+ await api("POST", `/vaults/${encodeURIComponent(vaultId)}/secrets`, {
665
+ display_name: `${c.envKey} (${c.serverName})`,
666
+ auth: {
667
+ type: "environment_variable",
668
+ secret_name: c.envKey,
669
+ value: c.value,
670
+ allowed_hosts: c.url ? [c.url] : []
671
+ }
672
+ });
673
+ console.log(` \u2713 saved ${c.envKey}`);
674
+ saved += 1;
675
+ } catch (error) {
676
+ if (error instanceof ApiError && error.status === 409) {
677
+ console.log(` \u2014 ${c.envKey} already in vault`);
678
+ } else {
679
+ const msg = error instanceof Error ? error.message : String(error);
680
+ console.log(` \u2717 ${c.envKey} failed: ${msg}`);
681
+ }
682
+ }
683
+ }
684
+ if (saved || skipped) {
685
+ console.log(
686
+ `
687
+ Vault: ${saved} saved, ${skipped} skipped. Attach the "default" vault to a session to use these keys.`
688
+ );
689
+ }
690
+ }
605
691
  function confirm(question) {
606
692
  return new Promise((done) => {
607
693
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -633,7 +719,7 @@ async function createClone(local, opts) {
633
719
  if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
634
720
  if (local.droppedServers.length) {
635
721
  console.log(
636
- `Left out (need a key): ${local.droppedServers.map((n) => `${n} MCP`).join(", ")}. Run \`sky setup secrets\` to choose which keys to save to your vault.`
722
+ `Left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
637
723
  );
638
724
  }
639
725
  if (opts.dryRun) {
@@ -689,6 +775,7 @@ async function createClone(local, opts) {
689
775
  console.log(
690
776
  `Linked this machine (${linkPath(local.configRoot)}). Push prompt changes anytime by running \`sky clone\` again.`
691
777
  );
778
+ await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
692
779
  if (opts.open) openBrowser(url);
693
780
  }
694
781
  async function syncClone(local, link, opts) {
@@ -747,11 +834,12 @@ async function syncClone(local, link, opts) {
747
834
  }
748
835
  if (local.droppedServers.length) {
749
836
  console.log(
750
- ` left out (need a key): ${local.droppedServers.map((n) => `${n} MCP`).join(", ")}. Run \`sky setup secrets\` to choose which keys to save to your vault.`
837
+ ` left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
751
838
  );
752
839
  }
753
840
  writeLink(local.configRoot, { ...link, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
754
841
  console.log(`Synced ${link.agentId}.`);
842
+ await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
755
843
  if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentId}`);
756
844
  }
757
845
  async function clone(opts) {
@@ -851,7 +939,7 @@ function listClaudeCodeSessions(limit) {
851
939
  } catch {
852
940
  continue;
853
941
  }
854
- const workspace = decodeWorkspace(encoded);
942
+ const workspace2 = decodeWorkspace(encoded);
855
943
  for (const entry2 of entries) {
856
944
  if (!entry2.endsWith(".jsonl")) continue;
857
945
  const sessionId = entry2.slice(0, -6);
@@ -879,7 +967,7 @@ function listClaudeCodeSessions(limit) {
879
967
  sessions.push({
880
968
  harness: "claude_code",
881
969
  sessionId,
882
- workspace,
970
+ workspace: workspace2,
883
971
  title: sniffTitle(mainFile),
884
972
  mtimeMs: stat.mtimeMs,
885
973
  files: [mainFile, ...subagentFiles],
@@ -957,6 +1045,13 @@ function agentUseBase() {
957
1045
  if (derived.includes("localhost:3000")) return derived.replace("localhost:3000", "localhost:3002");
958
1046
  return derived;
959
1047
  }
1048
+ function currentSessionEnv() {
1049
+ const claudeId = process.env.CLAUDE_CODE_SESSION_ID;
1050
+ if (claudeId) return { harness: "claude_code", sessionId: claudeId };
1051
+ const codexId = process.env.CODEX_THREAD_ID;
1052
+ if (codexId) return { harness: "codex", sessionId: codexId };
1053
+ return void 0;
1054
+ }
960
1055
  async function mintTrace(base, harness) {
961
1056
  const token = apiToken();
962
1057
  if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
@@ -979,7 +1074,27 @@ async function mintTrace(base, harness) {
979
1074
  }
980
1075
  return await res.json();
981
1076
  }
982
- function prepareImport(session2) {
1077
+ function loadSummaryFile(summaryPath) {
1078
+ let raw;
1079
+ try {
1080
+ raw = readFileSync5(summaryPath, "utf8");
1081
+ } catch {
1082
+ throw new Error(`Cannot read summary file: ${summaryPath}`);
1083
+ }
1084
+ let pkg;
1085
+ try {
1086
+ pkg = JSON.parse(raw);
1087
+ } catch {
1088
+ throw new Error("Summary file is not valid JSON.");
1089
+ }
1090
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) throw new Error("Summary file must be a JSON object.");
1091
+ const p = pkg;
1092
+ if (p.format !== "asteroids.session-summary") throw new Error('Summary file must have format "asteroids.session-summary".');
1093
+ if (p.format_version !== 1) throw new Error("Summary file must have format_version 1.");
1094
+ const redacted = redactLine2(raw.trimEnd());
1095
+ return redacted;
1096
+ }
1097
+ function prepareImport(session2, summaryPath) {
983
1098
  if (session2.discoveryError) throw new Error(session2.discoveryError);
984
1099
  const maximum = 16 * 1024 * 1024;
985
1100
  let inputBytes = 0;
@@ -987,19 +1102,40 @@ function prepareImport(session2) {
987
1102
  const lines = [];
988
1103
  const digest = createHash("sha256");
989
1104
  let nativeId;
1105
+ let redactionCount = 0;
1106
+ let encryptedReasoningCount = 0;
1107
+ let compactionCount = 0;
1108
+ let unreadableCount = 0;
990
1109
  for (const path of session2.files) {
991
1110
  const stat = statSync(path);
992
1111
  if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
993
1112
  const text = readFileSync5(path, "utf8");
994
1113
  for (const raw of text.split("\n")) {
995
1114
  if (!raw.trim()) continue;
996
- const record = JSON.parse(raw);
1115
+ let record;
1116
+ try {
1117
+ record = JSON.parse(raw);
1118
+ } catch {
1119
+ unreadableCount++;
1120
+ continue;
1121
+ }
997
1122
  if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("Transcript records must be JSON objects.");
998
- if (String(record.type).startsWith("import.")) throw new Error("Native transcripts cannot supply import protocol records.");
999
- const id = record.type === "session_meta" ? record.payload?.id : record.sessionId;
1123
+ const rec = record;
1124
+ if (String(rec.type).startsWith("import.")) throw new Error("Native transcripts cannot supply import protocol records.");
1125
+ if (rec.type === "response_item") {
1126
+ const payload = rec.payload;
1127
+ if (payload?.type === "reasoning" && typeof payload?.encrypted_content === "string" && payload.encrypted_content) {
1128
+ encryptedReasoningCount++;
1129
+ }
1130
+ }
1131
+ if (rec.type === "compacted") {
1132
+ compactionCount++;
1133
+ }
1134
+ const id = rec.type === "session_meta" ? rec.payload?.id : rec.sessionId;
1000
1135
  if (!nativeId && typeof id === "string") nativeId = id;
1001
- const clean = redactLine(raw.replace(/\r$/, ""), { requireStructured: true }).line;
1002
- const line = clean + "\n";
1136
+ const result = redactLine(raw.replace(/\r$/, ""), { requireStructured: true });
1137
+ if (Object.keys(result.hits).length > 0) redactionCount++;
1138
+ const line = result.line + "\n";
1003
1139
  outputBytes += Buffer.byteLength(line);
1004
1140
  if (outputBytes > maximum) throw new Error("Redacted transcript exceeds the 16 MiB limit.");
1005
1141
  digest.update(line);
@@ -1009,27 +1145,38 @@ function prepareImport(session2) {
1009
1145
  if (!lines.length) throw new Error("The selected transcript is empty.");
1010
1146
  const sourceId = nativeId ?? session2.sessionId;
1011
1147
  if (!/^[A-Za-z0-9_-]{1,200}$/.test(sourceId) || redactLine2(JSON.stringify(sourceId)) !== JSON.stringify(sourceId)) throw new Error("The source conversation has an invalid identifier.");
1012
- const summary = {
1013
- format: "asteroids.session-summary",
1014
- format_version: 1,
1015
- source_harness: session2.harness,
1016
- source_harness_version: null,
1017
- source_session_id: sourceId,
1018
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
1019
- summary: "The complete selected local conversation is attached as a transcript. Review it before continuing; this automatic handoff does not infer the user's unfinished objective or claim any work is complete.",
1020
- context: {
1021
- objective: "Review the imported conversation and continue its latest unfinished authorized objective.",
1022
- constraints: [],
1023
- decisions: [],
1024
- completed: [],
1025
- pending: ["Read the complete imported transcript, reconcile the latest human instructions and completed work, then continue."],
1026
- verification: [],
1027
- blockers: ["This handoff contains the recorded conversation only. Referenced local files, runtime state, and unreadable reasoning are not transferred."],
1028
- artifacts: [],
1029
- provenance: { native_checkpoint_sha256: digest.digest("hex"), selected_native_session_id: sourceId, context_bytes: outputBytes }
1030
- }
1031
- };
1032
- return lines.join("") + JSON.stringify({ type: "import.summary", package: summary }) + "\n" + JSON.stringify({ type: "import.end", records: lines.length + 1 }) + "\n";
1148
+ let summaryJson;
1149
+ if (summaryPath) {
1150
+ summaryJson = loadSummaryFile(summaryPath);
1151
+ } else {
1152
+ const summary = {
1153
+ format: "asteroids.session-summary",
1154
+ format_version: 1,
1155
+ source_harness: session2.harness,
1156
+ source_harness_version: null,
1157
+ source_session_id: sourceId,
1158
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1159
+ summary: "The complete selected local conversation is attached as a transcript. Review it before continuing; this automatic handoff does not infer the user's unfinished objective or claim any work is complete.",
1160
+ context: {
1161
+ objective: "Review the imported conversation and continue its latest unfinished authorized objective.",
1162
+ constraints: [],
1163
+ decisions: [],
1164
+ completed: [],
1165
+ pending: ["Read the complete imported transcript, reconcile the latest human instructions and completed work, then continue."],
1166
+ verification: [],
1167
+ blockers: ["This handoff contains the recorded conversation only. Referenced local files, runtime state, and unreadable reasoning are not transferred."],
1168
+ artifacts: [],
1169
+ provenance: { native_checkpoint_sha256: digest.digest("hex"), selected_native_session_id: sourceId, context_bytes: outputBytes }
1170
+ }
1171
+ };
1172
+ summaryJson = JSON.stringify(summary);
1173
+ }
1174
+ const body = lines.join("") + JSON.stringify({ type: "import.summary", package: JSON.parse(summaryJson) }) + "\n" + JSON.stringify({ type: "import.end", records: lines.length + 1 }) + "\n";
1175
+ const gaps = [];
1176
+ if (encryptedReasoningCount > 0) gaps.push({ kind: "encrypted_reasoning", count: encryptedReasoningCount });
1177
+ if (compactionCount > 0) gaps.push({ kind: "compaction", count: compactionCount });
1178
+ if (unreadableCount > 0) gaps.push({ kind: "unreadable", count: unreadableCount });
1179
+ return { body, transcriptLines: lines.length, outputBytes, redactionCount, gaps };
1033
1180
  }
1034
1181
  async function importSession(opts) {
1035
1182
  const base = (opts.agentUseUrl ?? agentUseBase()).replace(/\/$/, "");
@@ -1038,9 +1185,16 @@ async function importSession(opts) {
1038
1185
  throw new Error("Import requires HTTPS, or a loopback development server.");
1039
1186
  }
1040
1187
  opts.onProgress?.("Reading transcript and redacting secrets locally\u2026");
1041
- const body = prepareImport(opts.session);
1042
- opts.onProgress?.("Minting import key\u2026");
1043
- const { traceId, key } = await mintTrace(base, opts.session.harness);
1188
+ const { body, transcriptLines, outputBytes, redactionCount, gaps } = prepareImport(opts.session, opts.summaryPath);
1189
+ let traceId;
1190
+ let key;
1191
+ if (opts.importKey && opts.grantTraceId) {
1192
+ traceId = opts.grantTraceId;
1193
+ key = opts.importKey;
1194
+ } else {
1195
+ opts.onProgress?.("Minting import key\u2026");
1196
+ ({ traceId, key } = await mintTrace(base, opts.session.harness));
1197
+ }
1044
1198
  opts.onProgress?.("Uploading the complete transcript\u2026");
1045
1199
  const response = await fetch(`${base}/api/traces/ingest`, {
1046
1200
  method: "POST",
@@ -1054,7 +1208,17 @@ async function importSession(opts) {
1054
1208
  throw new Error(typeof result2?.error === "string" ? result2.error : result2?.error?.message ?? `Upload failed (${response.status}).`);
1055
1209
  }
1056
1210
  const result = await response.json();
1057
- return { traceId: result.traceId ?? traceId, url: `${base}/playground/new?import=${encodeURIComponent(result.traceId ?? traceId)}` };
1211
+ const finalTraceId = result.traceId ?? traceId;
1212
+ return {
1213
+ traceId: finalTraceId,
1214
+ url: `${base}/playground/new?import=${encodeURIComponent(finalTraceId)}`,
1215
+ harness: opts.session.harness,
1216
+ sessionId: opts.session.sessionId,
1217
+ transcriptFiles: opts.session.files.length,
1218
+ bytes: outputBytes,
1219
+ redactionCount,
1220
+ gaps
1221
+ };
1058
1222
  }
1059
1223
 
1060
1224
  // src/login.ts
@@ -1332,12 +1496,13 @@ function renderCatalog(agents, generatedAt) {
1332
1496
  }
1333
1497
 
1334
1498
  // src/skill.ts
1499
+ var REFERENCES_DIR = "references";
1335
1500
  var SKILL_NAME = "agentsky";
1336
1501
  var SKILL_MARKER = ".agentsky.json";
1337
1502
  function skillDocument() {
1338
1503
  return `---
1339
1504
  name: ${SKILL_NAME}
1340
- description: "Hire cloud agents from the AgentSky catalog (included as ${CATALOG_FILE}) and coordinate them on one task. Use when the person names an agent by @handle, asks to have another agent (or another engine such as Codex, Claude Code, Gemini or Hermes) do, test or review part of the work, wants work to keep running in the cloud, or the work splits into independent parts that can run at the same time. Works through the agentsky MCP server, or the sky command where those tools are missing."
1505
+ description: "Hire cloud agents from the AgentSky catalog (included as ${CATALOG_FILE}) and coordinate them on one task. Use when: the person names an agent by @handle or wants to orchestrate agents; asks another engine (Codex, Claude Code, Gemini, Hermes) to do, test or review part of the work; wants to import, move or continue this conversation in AgentSky or the cloud; names a workspace; or the work splits into independent parts that can run at the same time. Works through the agentsky MCP server, or the sky command where those tools are missing."
1341
1506
  ---
1342
1507
 
1343
1508
  # Working with other agents through AgentSky
@@ -1352,6 +1517,14 @@ Everything below is a tool of the \`agentsky\` MCP server. If those tools are
1352
1517
  missing in this session, use the \`sky\` commands under "Without the MCP tools"
1353
1518
  at the end; they do the same things, one agent at a time.
1354
1519
 
1520
+ ## Workspace
1521
+
1522
+ Use the workspace the person named. If they named none, check the git root for
1523
+ \`.agentsky.json\` \u2014 if it has a \`"workspace"\` key, use that. Otherwise use the
1524
+ account default (personal). Never ask. Pass the chosen slug as \`workspace\` on
1525
+ every MCP tool that accepts it, or as \`--workspace <slug>\` on sky commands.
1526
+ Details: \`${REFERENCES_DIR}/workspaces.md\` in this skill's folder.
1527
+
1355
1528
  ## When to hire
1356
1529
 
1357
1530
  - When the person names an agent (\`@handle\`) or an engine, or asks another agent
@@ -1362,9 +1535,6 @@ at the end; they do the same things, one agent at a time.
1362
1535
  going after this conversation ends.
1363
1536
  - Do the work yourself when it is small, when it depends on what you already have
1364
1537
  in front of you, or when explaining it would take as long as doing it.
1365
- - Every hire spends the person's AgentSky balance. Say what you are about to hire
1366
- and roughly what it may cost before the first one, unless they already asked
1367
- for it.
1368
1538
 
1369
1539
  ## How to pick
1370
1540
 
@@ -1432,6 +1602,24 @@ it, and name its path in the brief.
1432
1602
  6. Tell the person what was done, which agent did which part, and what it cost
1433
1603
  (\`get_task\` reports the task's cost).
1434
1604
 
1605
+ ## Import this conversation to AgentSky
1606
+
1607
+ When the person asks to import, move, or continue this conversation in AgentSky
1608
+ or the cloud:
1609
+
1610
+ 1. Write a summary JSON file following the schema in \`${REFERENCES_DIR}/import.md\`
1611
+ (the only step that needs judgment \u2014 capture the objective, key decisions,
1612
+ completed work, and next steps).
1613
+ 2. Run:
1614
+ \`\`\`
1615
+ npx -y @agentsky/sky import --current --summary <path-to-summary.json> --yes --json
1616
+ \`\`\`
1617
+ Add \`--workspace <slug>\` when a workspace was chosen. Do NOT confirm or ask
1618
+ before running; the \`--yes --json\` flags suppress prompts.
1619
+ 3. Report the \`continueUrl\` from the JSON result. That is all.
1620
+
1621
+ Only stop (non-zero exit) on a size limit, dropped content, or redaction failure.
1622
+
1435
1623
  ## Without the MCP tools
1436
1624
 
1437
1625
  Run \`sky auth whoami\`. If it says you are not logged in, ask the person to run
@@ -1451,6 +1639,88 @@ Run \`sky auth whoami\`. If it says you are not logged in, ask the person to run
1451
1639
  read again.
1452
1640
  `;
1453
1641
  }
1642
+ function importReferenceDoc() {
1643
+ return `# Import this conversation to AgentSky
1644
+
1645
+ ## Summary schema (asteroids.session-summary v1)
1646
+
1647
+ Write a JSON file with these fields:
1648
+
1649
+ \`\`\`json
1650
+ {
1651
+ "format": "asteroids.session-summary",
1652
+ "format_version": 1,
1653
+ "source_harness": "claude_code",
1654
+ "source_harness_version": null,
1655
+ "source_session_id": "<the session id, e.g. from CLAUDE_CODE_SESSION_ID>",
1656
+ "created_at": "<ISO 8601 timestamp>",
1657
+ "summary": "<one paragraph: what this conversation accomplished>",
1658
+ "context": {
1659
+ "objective": "<what the person ultimately wants>",
1660
+ "constraints": ["<must not do X>"],
1661
+ "decisions": ["<decided to use Y approach>"],
1662
+ "completed": ["<finished task A>"],
1663
+ "pending": ["<next: do B>"],
1664
+ "verification": ["<how to confirm B is correct>"],
1665
+ "blockers": ["<needs access to X>"],
1666
+ "artifacts": [{"path": "path/to/file.ts", "description": "what it is"}]
1667
+ }
1668
+ }
1669
+ \`\`\`
1670
+
1671
+ Only \`summary\` and \`context\` require judgment. All other fields are fixed values
1672
+ or can be omitted (the CLI validates \`format\` and \`format_version\`).
1673
+
1674
+ ## Gaps
1675
+
1676
+ The import transfers the recorded conversation only. These are NOT transferred:
1677
+ - Local files not included in the transcript
1678
+ - Runtime state (variables, shell state, running processes)
1679
+ - Unrecorded tool outputs or reasoning
1680
+ - Login credentials and secrets
1681
+
1682
+ List known gaps in \`context.blockers\`.
1683
+
1684
+ ## Non-Claude/Codex harnesses
1685
+
1686
+ For harnesses that do not set \`CLAUDE_CODE_SESSION_ID\` or \`CODEX_THREAD_ID\`:
1687
+ use \`--session <id>\` instead of \`--current\`, and set \`source_harness\` to the
1688
+ harness name (e.g. \`"hermes"\`, \`"openclaw"\`).
1689
+ `;
1690
+ }
1691
+ function workspacesReferenceDoc() {
1692
+ return `# Workspaces in AgentSky
1693
+
1694
+ A workspace (also called a universe) groups agents and sessions. Every API call
1695
+ and MCP tool accepts an optional workspace slug.
1696
+
1697
+ ## How to choose
1698
+
1699
+ 1. The person named one \u2192 use it.
1700
+ 2. Check the git root for \`.agentsky.json\` with a \`"workspace"\` key \u2192 use that.
1701
+ 3. Neither \u2192 use the account default (personal workspace). Never ask.
1702
+
1703
+ ## Applying it
1704
+
1705
+ - MCP tools: pass \`workspace: "<slug>"\` on tools that accept it.
1706
+ - sky CLI: \`--workspace <slug>\` on any command, or set \`SKY_WORKSPACE=<slug>\`.
1707
+ - Tools that take a \`task_id\` or \`session_id\` inherit that task's workspace
1708
+ automatically; one task never spans workspaces.
1709
+
1710
+ ## Listing workspaces
1711
+
1712
+ \`sky workspace list\` (or \`sky universe list\`) prints your workspaces, with
1713
+ \`*\` marking the current default.
1714
+
1715
+ ## .agentsky.json
1716
+
1717
+ To set a per-repo default, create \`.agentsky.json\` at the git root:
1718
+
1719
+ \`\`\`json
1720
+ {"workspace": "my-team-slug"}
1721
+ \`\`\`
1722
+ `;
1723
+ }
1454
1724
  function installedVersion(dir) {
1455
1725
  try {
1456
1726
  const marker = JSON.parse(readFileSync6(join5(dir, SKILL_MARKER), "utf8"));
@@ -1477,6 +1747,10 @@ function installSkill(agent2, opts = {}) {
1477
1747
  mkdirSync2(dir, { recursive: true });
1478
1748
  writeFileSync4(join5(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
1479
1749
  }
1750
+ const refsDir = join5(dir, REFERENCES_DIR);
1751
+ mkdirSync2(refsDir, { recursive: true });
1752
+ writeFileSync4(join5(refsDir, "import.md"), importReferenceDoc());
1753
+ writeFileSync4(join5(refsDir, "workspaces.md"), workspacesReferenceDoc());
1480
1754
  return { agent: agent2.id, path: dir, outcome };
1481
1755
  }
1482
1756
 
@@ -2328,7 +2602,11 @@ function printModelBilling(session2) {
2328
2602
  console.log(`Model billing: ${label} \u2014 $0 model usage`);
2329
2603
  }
2330
2604
  var program = new Command();
2331
- program.name("sky").description("Launch and talk to long-lived agents \u2014 agentsky.dev").version(CLI_VERSION);
2605
+ program.name("sky").description("Launch and talk to long-lived agents \u2014 agentsky.dev").version(CLI_VERSION).option("--workspace <slug>", "workspace to act in (overrides sky universe switch and SKY_UNIVERSE; alias of --universe on sub-commands)");
2606
+ program.hook("preAction", () => {
2607
+ const workspace2 = program.opts().workspace;
2608
+ if (workspace2) process.env.SKY_WORKSPACE = workspace2;
2609
+ });
2332
2610
  function fail(error) {
2333
2611
  if (error instanceof ApiError) console.error(`sky: ${error.message} (${error.code})`);
2334
2612
  else console.error(`sky: ${error instanceof Error ? error.message : String(error)}`);
@@ -2430,8 +2708,19 @@ universe.command("remove <member>").description("remove a member (user id or ema
2430
2708
  );
2431
2709
  universe.command("join <token>").description("accept an invitation sent to your verified email").action(
2432
2710
  run(async (token) => {
2433
- const workspace = await joinWorkspace(token);
2434
- console.log(`Joined ${workspace.slug} (${workspace.name}) \u2014 sky universe switch ${workspace.slug}`);
2711
+ const workspace2 = await joinWorkspace(token);
2712
+ console.log(`Joined ${workspace2.slug} (${workspace2.name}) \u2014 sky universe switch ${workspace2.slug}`);
2713
+ })
2714
+ );
2715
+ var workspace = program.command("workspace").description("alias of universe: grouping boundary for agents");
2716
+ workspace.command("list").action(
2717
+ run(async () => {
2718
+ const { universes } = await api("GET", "/universes");
2719
+ const current = currentUniverse();
2720
+ for (const u of universes) {
2721
+ const marker = u.slug === current || !current && u.isPersonal ? "*" : " ";
2722
+ console.log(`${marker} ${u.slug}${u.isPersonal ? " (personal)" : ""}`);
2723
+ }
2435
2724
  })
2436
2725
  );
2437
2726
  var policy = program.command("policy").description("policy rules: tool calls, spend caps, call counts, capabilities");
@@ -2993,30 +3282,54 @@ function formatRelative(msAgo) {
2993
3282
  if (hours < 48) return `${hours}h ago`;
2994
3283
  return `${Math.round(hours / 24)}d ago`;
2995
3284
  }
2996
- program.command("import").description("import a local Claude Code or Codex session to AgentSky and continue it in the cloud").option("--all", "import all found sessions up to --limit").option("-y, --yes", "confirm transfer of the selected conversations without prompting").option("--limit <n>", "maximum sessions to list or import", "10").option("--session <id>", "import one specific session by its ID").option("--harness <type>", "claude_code | codex | all (default: all)").option("--url <base>", "agent-use app URL (default: derived from SKY_API_URL or AGENT_USE_URL)").action(
3285
+ program.command("import").description("import a local Claude Code or Codex session to AgentSky and continue it in the cloud").option("--current", "import the session this agent is running in (reads CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID, then falls back to most recent session in the cwd)").option("--all", "import all found sessions up to --limit").option("-y, --yes", "confirm transfer without prompting (default when --current or --session is given in non-TTY)").option("--json", "print one JSON result object per import and nothing else to stdout").option("--limit <n>", "maximum sessions to list or import", "10").option("--session <id>", "import one specific session by its ID").option("--harness <type>", "claude_code | codex | all (default: all)").option("--summary <path>", "attach a pre-written summary JSON (asteroids.session-summary v1) instead of the auto-generated one").option("--key <importKey>", "import grant key minted by the web page (skips sky auth login)").option("--trace <traceId>", "trace id paired with --key").option("--url <base>", "agent-use app URL (default: derived from SKY_API_URL or AGENT_USE_URL)").action(
2997
3286
  run(
2998
3287
  async (opts) => {
3288
+ if (opts.key && !opts.trace) throw new Error("--key requires --trace <traceId>");
3289
+ if (opts.trace && !opts.key) throw new Error("--trace requires --key <importKey>");
2999
3290
  const limit = Math.max(1, Math.min(50, parseInt(opts.limit ?? "10", 10) || 10));
3000
3291
  if (opts.harness && !["claude_code", "codex", "all"].includes(opts.harness)) throw new Error("Use --harness claude_code, codex, or all.");
3001
3292
  const harness = opts.harness ?? "all";
3002
3293
  const base = opts.url ?? agentUseBase();
3003
- const candidates = opts.session ? listRecentSessions(harness === "all" ? void 0 : harness, Number.POSITIVE_INFINITY).filter(
3004
- (s) => s.sessionId === opts.session
3005
- ) : listRecentSessions(harness === "all" ? void 0 : harness, limit);
3006
- if (candidates.length === 0) {
3007
- if (opts.session) {
3008
- throw new Error(`Session "${opts.session}" not found in ~/.claude or ~/.codex.`);
3294
+ const nonInteractive = !process.stdin.isTTY;
3295
+ const autoConfirm = opts.yes || opts.current || !!opts.session && nonInteractive;
3296
+ let candidates;
3297
+ if (opts.current) {
3298
+ const env = currentSessionEnv();
3299
+ if (env) {
3300
+ candidates = listRecentSessions(env.harness, Number.POSITIVE_INFINITY).filter(
3301
+ (s) => s.sessionId === env.sessionId
3302
+ );
3303
+ if (candidates.length === 0) {
3304
+ const cwd = process.cwd();
3305
+ const all = listRecentSessions(env.harness, 50);
3306
+ const byCwd = all.filter((s) => s.workspace === cwd || s.workspace.startsWith(cwd + "/"));
3307
+ candidates = byCwd.length > 0 ? [byCwd[0]] : all.length > 0 ? [all[0]] : [];
3308
+ }
3309
+ } else {
3310
+ const cwd = process.cwd();
3311
+ const all = listRecentSessions(harness === "all" ? void 0 : harness, 50);
3312
+ const byCwd = all.filter((s) => s.workspace === cwd || s.workspace.startsWith(cwd + "/"));
3313
+ candidates = byCwd.length > 0 ? [byCwd[0]] : all.length > 0 ? [all[0]] : [];
3009
3314
  }
3010
- console.log(
3011
- "No recent sessions found. Make sure you have run Claude Code (sessions in ~/.claude) or Codex (sessions in ~/.codex)."
3315
+ if (candidates.length === 0) throw new Error("No local session found for the current environment. Run with --session <id> or start an agent session first.");
3316
+ } else if (opts.session) {
3317
+ candidates = listRecentSessions(harness === "all" ? void 0 : harness, Number.POSITIVE_INFINITY).filter(
3318
+ (s) => s.sessionId === opts.session
3012
3319
  );
3013
- return;
3320
+ if (candidates.length === 0) throw new Error(`Session "${opts.session}" not found in ~/.claude or ~/.codex.`);
3321
+ } else {
3322
+ candidates = listRecentSessions(harness === "all" ? void 0 : harness, limit);
3323
+ if (candidates.length === 0) {
3324
+ console.log("No recent sessions found. Make sure you have run Claude Code (sessions in ~/.claude) or Codex (sessions in ~/.codex).");
3325
+ return;
3326
+ }
3014
3327
  }
3015
3328
  let toImport;
3016
- if (opts.all || opts.session || candidates.length === 1) {
3329
+ if (opts.current || opts.all || opts.session || candidates.length === 1) {
3017
3330
  toImport = candidates;
3018
3331
  } else {
3019
- if (!process.stdin.isTTY) throw new Error("Use --session or --all to select conversations in a non-interactive terminal.");
3332
+ if (nonInteractive) throw new Error("Use --session, --current, or --all to select conversations in a non-interactive terminal.");
3020
3333
  console.log("\nRecent local sessions:\n");
3021
3334
  candidates.forEach((s, i) => {
3022
3335
  const label = s.title ? `"${s.title.slice(0, 70)}"` : "(no title)";
@@ -3049,10 +3362,12 @@ program.command("import").description("import a local Claude Code or Codex sessi
3049
3362
  }
3050
3363
  }
3051
3364
  toImport = [...new Map(toImport.map((session2) => [`${session2.harness}:${session2.sessionId}`, session2])).values()];
3052
- console.log(`Selected ${toImport.length} conversation(s), ${toImport.reduce((count, session2) => count + session2.files.length, 0)} transcript file(s). Destination: ${base}`);
3053
- console.log("The recorded conversation is redacted locally. Local project files and login credentials are not transferred.");
3054
- if (!opts.yes) {
3055
- if (!process.stdin.isTTY) throw new Error("Use --yes to confirm this transfer in a non-interactive terminal.");
3365
+ if (!opts.json) {
3366
+ console.log(`Selected ${toImport.length} conversation(s), ${toImport.reduce((count, session2) => count + session2.files.length, 0)} transcript file(s). Destination: ${base}`);
3367
+ console.log("The recorded conversation is redacted locally. Local project files and login credentials are not transferred.");
3368
+ }
3369
+ if (!autoConfirm) {
3370
+ if (nonInteractive) throw new Error("Use --yes to confirm this transfer in a non-interactive terminal.");
3056
3371
  const prompt = createInterface4({ input: process.stdin, output: process.stdout });
3057
3372
  const answer = await new Promise((resolve5) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
3058
3373
  prompt.close();
@@ -3065,18 +3380,36 @@ program.command("import").description("import a local Claude Code or Codex sessi
3065
3380
  }
3066
3381
  for (const session2 of toImport) {
3067
3382
  const label = session2.title ? `"${session2.title.slice(0, 60)}"` : session2.sessionId;
3068
- process.stdout.write(`
3383
+ if (!opts.json) process.stdout.write(`
3069
3384
  Importing ${label} \u2026
3070
3385
  `);
3071
3386
  const result = await importSession({
3072
3387
  session: session2,
3073
3388
  agentUseUrl: base,
3074
- onProgress: (msg) => process.stdout.write(` ${msg}
3389
+ summaryPath: opts.summary,
3390
+ importKey: opts.key,
3391
+ grantTraceId: opts.trace,
3392
+ onProgress: opts.json ? void 0 : (msg) => process.stdout.write(` ${msg}
3075
3393
  `)
3076
3394
  });
3077
- console.log(` \u2713 Imported. Continue here:
3395
+ if (opts.json) {
3396
+ console.log(JSON.stringify({
3397
+ traceId: result.traceId,
3398
+ url: result.url,
3399
+ workspace: currentUniverse() ?? null,
3400
+ harness: result.harness,
3401
+ sessionId: result.sessionId,
3402
+ transcriptFiles: result.transcriptFiles,
3403
+ bytes: result.bytes,
3404
+ redactionCount: result.redactionCount,
3405
+ gaps: result.gaps,
3406
+ continueUrl: result.url
3407
+ }));
3408
+ } else {
3409
+ console.log(` \u2713 Imported. Continue here:
3078
3410
  ${result.url}
3079
3411
  `);
3412
+ }
3080
3413
  }
3081
3414
  }
3082
3415
  )