@agentsky/sky 2.3.0 → 2.5.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
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AGENT_TYPES,
4
+ MODEL_SUBSCRIPTION_PLAN_PHRASE,
5
+ MODEL_SUBSCRIPTION_PROVIDER_NAME,
4
6
  REASONING_EFFORT_VALUES,
5
7
  capabilitiesSchema,
6
8
  customDataSchema,
@@ -10,13 +12,14 @@ import {
10
12
  mcpServersFromKept,
11
13
  modelSubscriptionProviderFor,
12
14
  reasoningEffortDocLine,
15
+ redactLine,
13
16
  skillRefSchema,
14
17
  splitMcpServers,
15
18
  supersededBy
16
- } from "./chunk-2KQLWHHC.js";
19
+ } from "./chunk-MHL5Z6IQ.js";
17
20
 
18
21
  // src/index.ts
19
- import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
22
+ import { existsSync as existsSync6, writeFileSync as writeFileSync5 } from "fs";
20
23
  import { basename as basename3, resolve as resolve3 } from "path";
21
24
  import { createInterface as createInterface3 } from "readline";
22
25
  import { Command } from "commander";
@@ -61,7 +64,7 @@ function currentUniverse() {
61
64
  }
62
65
 
63
66
  // src/version.ts
64
- var CLI_VERSION = "2.3.0";
67
+ var CLI_VERSION = "2.5.0";
65
68
  var USER_AGENT = `sky-cli/${CLI_VERSION} (${process.platform}; node/${process.versions.node})`;
66
69
 
67
70
  // src/api.ts
@@ -128,7 +131,7 @@ async function postEvents(sessionId, events, idempotencyKey) {
128
131
  }
129
132
  async function uploadFile(bytes, filename, mediaType, expiresInSeconds) {
130
133
  const form = new FormData();
131
- form.append("file", new Blob([bytes], { type: mediaType }), filename);
134
+ form.append("file", new Blob([new Uint8Array(bytes)], { type: mediaType }), filename);
132
135
  if (expiresInSeconds) form.append("expires_in_seconds", String(expiresInSeconds));
133
136
  const res = await fetch(`${apiBase()}/files`, {
134
137
  method: "POST",
@@ -164,7 +167,7 @@ async function resolveSessionRef(ref) {
164
167
  return agent2.defaultSessionId;
165
168
  }
166
169
  async function* streamEvents(sessionId, signal) {
167
- const { parseSseStream } = await import("./src-EGXYUSME.js");
170
+ const { parseSseStream } = await import("./src-GLBJHVIJ.js");
168
171
  const res = await fetch(
169
172
  `${apiBase()}/sessions/${encodeURIComponent(sessionId)}/stream`,
170
173
  {
@@ -678,8 +681,303 @@ async function clone(opts) {
678
681
  else await createClone(local, opts);
679
682
  }
680
683
 
684
+ // src/import-local.ts
685
+ import { closeSync, existsSync as existsSync3, openSync, readdirSync, readFileSync as readFileSync5, readSync, statSync } from "fs";
686
+ import { homedir as homedir3 } from "os";
687
+ import { join as join3 } from "path";
688
+ import { createHash } from "crypto";
689
+ function redactLine2(raw) {
690
+ return redactLine(raw).line;
691
+ }
692
+ function decodeWorkspace(encoded) {
693
+ try {
694
+ return decodeURIComponent(encoded.replace(/-/g, "%").replace(/_/g, "%5F"));
695
+ } catch {
696
+ return encoded;
697
+ }
698
+ }
699
+ function readPrefix(filePath) {
700
+ const fd = openSync(filePath, "r");
701
+ try {
702
+ const bytes = Buffer.alloc(256 * 1024);
703
+ return bytes.subarray(0, readSync(fd, bytes, 0, bytes.length, 0)).toString("utf8");
704
+ } finally {
705
+ closeSync(fd);
706
+ }
707
+ }
708
+ function sniffTitle(filePath) {
709
+ try {
710
+ const data = readPrefix(filePath);
711
+ for (const raw of data.split("\n")) {
712
+ if (!raw.trim()) continue;
713
+ let line;
714
+ try {
715
+ line = JSON.parse(redactLine2(raw));
716
+ } catch {
717
+ continue;
718
+ }
719
+ if (!line || typeof line !== "object") continue;
720
+ const rec = line;
721
+ if (rec.type === "user" && rec.message && typeof rec.message === "object") {
722
+ const msg = rec.message;
723
+ if (Array.isArray(msg.content)) {
724
+ for (const block of msg.content) {
725
+ if (typeof block === "object" && block !== null) {
726
+ const b = block;
727
+ if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
728
+ return b.text.trim().slice(0, 120);
729
+ }
730
+ }
731
+ }
732
+ }
733
+ if (typeof msg.content === "string" && msg.content.trim()) {
734
+ return msg.content.trim().slice(0, 120);
735
+ }
736
+ }
737
+ if (rec.type === "event_msg" && rec.payload && typeof rec.payload === "object") {
738
+ const payload = rec.payload;
739
+ if (payload.type === "user_message" && typeof payload.message === "string") return redactLine2(payload.message).slice(0, 120);
740
+ }
741
+ if (rec.type === "response_item" && rec.payload && typeof rec.payload === "object") {
742
+ const payload = rec.payload;
743
+ if (payload.type === "message" && payload.role === "user" && Array.isArray(payload.content)) {
744
+ const text = payload.content.find((part) => typeof part?.text === "string")?.text;
745
+ if (typeof text === "string" && text.trim()) return redactLine2(text.trim()).slice(0, 120);
746
+ }
747
+ }
748
+ if (rec.type === "session_meta") continue;
749
+ if (rec.type === "user_turn" || rec.type === "message" && rec.role === "user") {
750
+ const content = rec.content;
751
+ if (typeof content === "string" && content.trim()) return content.trim().slice(0, 120);
752
+ }
753
+ }
754
+ return null;
755
+ } catch {
756
+ return null;
757
+ }
758
+ }
759
+ function listClaudeCodeSessions(limit) {
760
+ const root = process.env.CLAUDE_CONFIG_DIR ?? join3(homedir3(), ".claude");
761
+ const projectsDir = join3(root, "projects");
762
+ if (!existsSync3(projectsDir)) return [];
763
+ const sessions = [];
764
+ try {
765
+ for (const encoded of readdirSync(projectsDir)) {
766
+ const projectDir = join3(projectsDir, encoded);
767
+ let entries;
768
+ try {
769
+ entries = readdirSync(projectDir);
770
+ } catch {
771
+ continue;
772
+ }
773
+ const workspace = decodeWorkspace(encoded);
774
+ for (const entry of entries) {
775
+ if (!entry.endsWith(".jsonl")) continue;
776
+ const sessionId = entry.slice(0, -6);
777
+ const mainFile = join3(projectDir, entry);
778
+ let stat;
779
+ try {
780
+ stat = statSync(mainFile);
781
+ } catch {
782
+ continue;
783
+ }
784
+ if (!stat.isFile()) continue;
785
+ let discoveryError;
786
+ const subagentFiles = [];
787
+ const subagentDir = join3(projectDir, sessionId, "subagents");
788
+ if (existsSync3(subagentDir)) {
789
+ try {
790
+ const subFiles = readdirSync(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
791
+ for (const f of subFiles) {
792
+ subagentFiles.push(join3(subagentDir, f));
793
+ }
794
+ } catch {
795
+ discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
796
+ }
797
+ }
798
+ sessions.push({
799
+ harness: "claude_code",
800
+ sessionId,
801
+ workspace,
802
+ title: sniffTitle(mainFile),
803
+ mtimeMs: stat.mtimeMs,
804
+ files: [mainFile, ...subagentFiles],
805
+ ...discoveryError ? { discoveryError } : {}
806
+ });
807
+ }
808
+ }
809
+ } catch {
810
+ }
811
+ return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
812
+ }
813
+ function listCodexSessions(limit) {
814
+ const sessionsDir = join3(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join3(homedir3(), ".codex"), "sessions");
815
+ const records = [];
816
+ function walk(dir) {
817
+ let entries;
818
+ try {
819
+ entries = readdirSync(dir, { withFileTypes: true });
820
+ } catch {
821
+ return;
822
+ }
823
+ for (const entry of entries) {
824
+ const full = join3(dir, entry.name);
825
+ if (entry.isDirectory()) {
826
+ walk(full);
827
+ continue;
828
+ }
829
+ if (!entry.isFile() || !entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl")) continue;
830
+ try {
831
+ const first = readPrefix(full).split("\n", 1)[0];
832
+ const meta = JSON.parse(first);
833
+ if (meta.type !== "session_meta" || typeof meta.payload?.id !== "string") continue;
834
+ const data = meta.payload;
835
+ const parent = data.forked_from_id ?? data.source?.spawn?.parent_thread_id ?? data.source?.subagent?.spawn?.parent_thread_id;
836
+ records.push({
837
+ session: {
838
+ harness: "codex",
839
+ sessionId: data.id,
840
+ workspace: typeof data.cwd === "string" ? data.cwd : dir,
841
+ title: sniffTitle(full),
842
+ mtimeMs: statSync(full).mtimeMs,
843
+ files: [full]
844
+ },
845
+ ...typeof parent === "string" ? { parent } : {}
846
+ });
847
+ } catch {
848
+ continue;
849
+ }
850
+ }
851
+ }
852
+ walk(sessionsDir);
853
+ function descendants(id, visited) {
854
+ if (visited.has(id)) return [];
855
+ visited.add(id);
856
+ return records.filter((item) => item.parent === id).flatMap((item) => [
857
+ ...item.session.files,
858
+ ...descendants(item.session.sessionId, visited)
859
+ ]);
860
+ }
861
+ return records.filter((item) => !item.parent).map(({ session: session2 }) => ({
862
+ ...session2,
863
+ files: [.../* @__PURE__ */ new Set([...session2.files, ...descendants(session2.sessionId, /* @__PURE__ */ new Set())])]
864
+ })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
865
+ }
866
+ function listRecentSessions(harness, limit) {
867
+ const claudeSessions = harness === "codex" ? [] : listClaudeCodeSessions(limit);
868
+ const codexSessions = harness === "claude_code" ? [] : listCodexSessions(limit);
869
+ return [...claudeSessions, ...codexSessions].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
870
+ }
871
+ function agentUseBase() {
872
+ const env = process.env.AGENT_USE_URL?.trim().replace(/\/$/, "");
873
+ if (env) return env;
874
+ const api2 = apiBase().replace(/\/$/, "");
875
+ const derived = api2.replace(/\/api\/v1$/, "").replace(/\/v1$/, "").replace("api.agentsky.dev", "agentsky.dev");
876
+ if (derived.includes("localhost:3000")) return derived.replace("localhost:3000", "localhost:3002");
877
+ return derived;
878
+ }
879
+ async function mintTrace(base, harness) {
880
+ const token = apiToken();
881
+ if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
882
+ const res = await fetch(`${base}/api/traces`, {
883
+ method: "POST",
884
+ redirect: "error",
885
+ signal: AbortSignal.timeout(15e3),
886
+ headers: {
887
+ "content-type": "application/json",
888
+ authorization: `Bearer ${token}`,
889
+ "user-agent": USER_AGENT
890
+ },
891
+ body: JSON.stringify({ harness })
892
+ });
893
+ if (!res.ok) {
894
+ const body = await res.json().catch(() => null);
895
+ throw new Error(
896
+ typeof body?.error === "string" ? body.error : `Failed to create import (${res.status}).`
897
+ );
898
+ }
899
+ return await res.json();
900
+ }
901
+ function prepareImport(session2) {
902
+ if (session2.discoveryError) throw new Error(session2.discoveryError);
903
+ const maximum = 16 * 1024 * 1024;
904
+ let inputBytes = 0;
905
+ let outputBytes = 0;
906
+ const lines = [];
907
+ const digest = createHash("sha256");
908
+ let nativeId;
909
+ for (const path of session2.files) {
910
+ const stat = statSync(path);
911
+ if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
912
+ const text = readFileSync5(path, "utf8");
913
+ for (const raw of text.split("\n")) {
914
+ if (!raw.trim()) continue;
915
+ const record = JSON.parse(raw);
916
+ if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("Transcript records must be JSON objects.");
917
+ if (String(record.type).startsWith("import.")) throw new Error("Native transcripts cannot supply import protocol records.");
918
+ const id = record.type === "session_meta" ? record.payload?.id : record.sessionId;
919
+ if (!nativeId && typeof id === "string") nativeId = id;
920
+ const clean = redactLine(raw.replace(/\r$/, ""), { requireStructured: true }).line;
921
+ const line = clean + "\n";
922
+ outputBytes += Buffer.byteLength(line);
923
+ if (outputBytes > maximum) throw new Error("Redacted transcript exceeds the 16 MiB limit.");
924
+ digest.update(line);
925
+ lines.push(line);
926
+ }
927
+ }
928
+ if (!lines.length) throw new Error("The selected transcript is empty.");
929
+ const sourceId = nativeId ?? session2.sessionId;
930
+ 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.");
931
+ const summary = {
932
+ format: "asteroids.session-summary",
933
+ format_version: 1,
934
+ source_harness: session2.harness,
935
+ source_harness_version: null,
936
+ source_session_id: sourceId,
937
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
938
+ 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.",
939
+ context: {
940
+ objective: "Review the imported conversation and continue its latest unfinished authorized objective.",
941
+ constraints: [],
942
+ decisions: [],
943
+ completed: [],
944
+ pending: ["Read the complete imported transcript, reconcile the latest human instructions and completed work, then continue."],
945
+ verification: [],
946
+ blockers: ["This handoff contains the recorded conversation only. Referenced local files, runtime state, and unreadable reasoning are not transferred."],
947
+ artifacts: [],
948
+ provenance: { native_checkpoint_sha256: digest.digest("hex"), selected_native_session_id: sourceId, context_bytes: outputBytes }
949
+ }
950
+ };
951
+ return lines.join("") + JSON.stringify({ type: "import.summary", package: summary }) + "\n" + JSON.stringify({ type: "import.end", records: lines.length + 1 }) + "\n";
952
+ }
953
+ async function importSession(opts) {
954
+ const base = (opts.agentUseUrl ?? agentUseBase()).replace(/\/$/, "");
955
+ const destination = new URL(base);
956
+ if (destination.username || destination.password || destination.protocol !== "https:" && !(destination.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(destination.hostname))) {
957
+ throw new Error("Import requires HTTPS, or a loopback development server.");
958
+ }
959
+ opts.onProgress?.("Reading transcript and redacting secrets locally\u2026");
960
+ const body = prepareImport(opts.session);
961
+ opts.onProgress?.("Minting import key\u2026");
962
+ const { traceId, key } = await mintTrace(base, opts.session.harness);
963
+ opts.onProgress?.("Uploading the complete transcript\u2026");
964
+ const response = await fetch(`${base}/api/traces/ingest`, {
965
+ method: "POST",
966
+ redirect: "error",
967
+ signal: AbortSignal.timeout(12e4),
968
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/x-ndjson", "x-source-harness": opts.session.harness, "user-agent": USER_AGENT },
969
+ body
970
+ });
971
+ if (!response.ok) {
972
+ const result2 = await response.json().catch(() => null);
973
+ throw new Error(typeof result2?.error === "string" ? result2.error : result2?.error?.message ?? `Upload failed (${response.status}).`);
974
+ }
975
+ const result = await response.json();
976
+ return { traceId: result.traceId ?? traceId, url: `${base}/playground/new?import=${encodeURIComponent(result.traceId ?? traceId)}` };
977
+ }
978
+
681
979
  // src/login.ts
682
- import { createHash, randomBytes } from "crypto";
980
+ import { createHash as createHash2, randomBytes } from "crypto";
683
981
  import { createServer } from "http";
684
982
  import { hostname } from "os";
685
983
  import { spawn as spawn2 } from "child_process";
@@ -695,7 +993,7 @@ function openBrowser2(url) {
695
993
  }
696
994
  async function login(opts) {
697
995
  const verifier = b64url(randomBytes(32));
698
- const challenge = b64url(createHash("sha256").update(verifier).digest());
996
+ const challenge = b64url(createHash2("sha256").update(verifier).digest());
699
997
  const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
700
998
  saveConfig({
701
999
  ...loadConfig(),
@@ -777,6 +1075,33 @@ async function headlessLogin(verifier, challenge) {
777
1075
  throw new Error("login timed out after 10 minutes");
778
1076
  }
779
1077
 
1078
+ // src/local-agents.ts
1079
+ import { existsSync as existsSync4 } from "fs";
1080
+ import { homedir as homedir4 } from "os";
1081
+ import { join as join4 } from "path";
1082
+ function localAgents(env = process.env, home = homedir4()) {
1083
+ const claude = env.CLAUDE_CONFIG_DIR || join4(home, ".claude");
1084
+ const codex = env.CODEX_HOME || join4(home, ".codex");
1085
+ const cursor = join4(home, ".cursor");
1086
+ return [
1087
+ { id: "claude_code", name: "Claude Code", root: claude, skillsDir: join4(claude, "skills") },
1088
+ { id: "codex", name: "Codex", root: codex, skillsDir: join4(codex, "skills") },
1089
+ { id: "cursor", name: "Cursor", root: cursor, skillsDir: join4(cursor, "skills") }
1090
+ ];
1091
+ }
1092
+ function detectLocalAgents(env = process.env, home = homedir4()) {
1093
+ return localAgents(env, home).filter((agent2) => existsSync4(agent2.root));
1094
+ }
1095
+ function selectLocalAgents(ids, env = process.env, home = homedir4()) {
1096
+ if (!ids?.length) return detectLocalAgents(env, home);
1097
+ const known = localAgents(env, home);
1098
+ return ids.map((id) => {
1099
+ const agent2 = known.find((candidate) => candidate.id === id);
1100
+ if (!agent2) throw new Error(`unknown agent ${id} \u2014 expected ${known.map((candidate) => candidate.id).join(" | ")}`);
1101
+ return agent2;
1102
+ });
1103
+ }
1104
+
780
1105
  // src/render.ts
781
1106
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
782
1107
  var red = (s) => `\x1B[31m${s}\x1B[0m`;
@@ -820,11 +1145,159 @@ function renderEvent(event, data) {
820
1145
  }
821
1146
  }
822
1147
 
1148
+ // src/session-read.ts
1149
+ var TURN_TYPES = ["user.message", "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
1150
+ function latestTurn(events) {
1151
+ let start = 0;
1152
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1153
+ if (events[index].type === "user.message") {
1154
+ start = index + 1;
1155
+ break;
1156
+ }
1157
+ }
1158
+ const reading = { finished: false, messages: [], errors: [] };
1159
+ for (const event of events.slice(start)) {
1160
+ if (event.type === "agent.message" && event.text?.trim()) reading.messages.push(event.text);
1161
+ else if (event.type === "session.error" && event.error?.retry_status !== "retrying") {
1162
+ reading.errors.push(event.error?.message ?? event.error?.type ?? "unknown error");
1163
+ } else if (event.type === "session.status_terminated") reading.finished = true;
1164
+ else if (event.type === "session.status_idle" && event.stop_reason?.type !== "requires_action") reading.finished = true;
1165
+ }
1166
+ return reading;
1167
+ }
1168
+ async function history(sessionId) {
1169
+ const events = [];
1170
+ let cursor = null;
1171
+ for (; ; ) {
1172
+ const query = new URLSearchParams({ limit: "500" });
1173
+ for (const type of TURN_TYPES) query.append("types[]", type);
1174
+ if (cursor) query.set("cursor", cursor);
1175
+ const page = await api(
1176
+ "GET",
1177
+ `/sessions/${encodeURIComponent(sessionId)}/events?${query}`
1178
+ );
1179
+ events.push(...page.events);
1180
+ if (!page.hasMore || !page.cursor) return events;
1181
+ cursor = page.cursor;
1182
+ }
1183
+ }
1184
+ async function readTurn(sessionId, opts = {}) {
1185
+ const deadline = Date.now() + (opts.timeoutSeconds ?? 600) * 1e3;
1186
+ for (; ; ) {
1187
+ const reading = latestTurn(await history(sessionId));
1188
+ if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
1189
+ await new Promise((resolve4) => setTimeout(resolve4, opts.intervalMs ?? 3e3));
1190
+ }
1191
+ }
1192
+
1193
+ // src/skill.ts
1194
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1195
+ import { join as join5 } from "path";
1196
+ var SKILL_NAME = "agentsky";
1197
+ var SKILL_MARKER = ".agentsky.json";
1198
+ function skillDocument() {
1199
+ return `---
1200
+ name: ${SKILL_NAME}
1201
+ description: Hand part of a job to another agent running in the cloud through AgentSky, and bring its result back. Use when the person asks for another agent or a named engine (Claude Code, Codex, Hermes, OpenClaw and others) to do, test or review something, or when independent work could run in parallel.
1202
+ ---
1203
+
1204
+ # Working with other agents through AgentSky
1205
+
1206
+ AgentSky runs long-lived agents in the cloud. With the \`sky\` command you can
1207
+ find one, give it a job, and read what it produced. You stay responsible for
1208
+ the result.
1209
+
1210
+ If the \`agentsky\` MCP tools are available in this session (\`search_catalog\`,
1211
+ \`start_session\`, \`read_session\`, \`send_message\`), use them instead of the
1212
+ commands below; they do the same things.
1213
+
1214
+ ## Before you start
1215
+
1216
+ Run \`sky auth whoami\`. If it says you are not logged in, ask the person to run
1217
+ \`sky auth login\` and stop there. Never ask them for a token.
1218
+
1219
+ ## When to hand work over
1220
+
1221
+ - When the person asks another agent to do, test or review something, do that.
1222
+ Honor an agent or engine they name.
1223
+ - Otherwise, hand over work another agent is better suited to, or independent
1224
+ parts that can run at the same time as your own.
1225
+ - Do the work yourself when it is small, when it depends on what you already
1226
+ have in front of you, or when explaining it would take as long as doing it.
1227
+
1228
+ ## How to pick
1229
+
1230
+ \`sky catalog search <words for the work> --json\`
1231
+
1232
+ Each result has a \`handle\`, what the agent does, its \`engine_type\`, its
1233
+ categories, how it is paid, and what it needs. If the person names an engine,
1234
+ match its \`engine_type\` and do not substitute another one silently. Run the
1235
+ command with no words to see every agent. If nothing fits, do the work
1236
+ yourself and say so.
1237
+
1238
+ ## How to brief
1239
+
1240
+ \`sky session create @<handle> --title "<short name>" --message "<brief>" --json\`
1241
+
1242
+ It prints the new session's \`id\`. The agent starts working at once, on its own
1243
+ computer in the cloud. It cannot see this conversation or this machine, so a
1244
+ brief is one self-contained message that states:
1245
+
1246
+ 1. what to produce,
1247
+ 2. what "done" means,
1248
+ 3. anything it must not do.
1249
+
1250
+ To give it a file, add \`--file <path>\`. Never put a password, key or token in a
1251
+ brief.
1252
+
1253
+ ## How to collect
1254
+
1255
+ \`sky session read <session id> --wait --json\`
1256
+
1257
+ It waits until the agent finishes its turn and prints its messages, with
1258
+ \`finished\` true or false. If \`finished\` is false the wait timed out: run it
1259
+ again. A first turn can take a minute to begin while the agent's computer
1260
+ starts.
1261
+
1262
+ To correct it, answer it or ask for more:
1263
+
1264
+ \`sky session message <session id> "<text>" --detach\`
1265
+
1266
+ then read again. An agent saying it is done is not evidence: check what it
1267
+ produced before you rely on it.
1268
+
1269
+ ## How to finish
1270
+
1271
+ Tell the person what was done and which agent did which part. Every agent you
1272
+ start is paid for by the person, so start only what the job needs.
1273
+ `;
1274
+ }
1275
+ function installedVersion(dir) {
1276
+ try {
1277
+ const marker = JSON.parse(readFileSync6(join5(dir, SKILL_MARKER), "utf8"));
1278
+ return typeof marker.version === "string" ? marker.version : null;
1279
+ } catch {
1280
+ return null;
1281
+ }
1282
+ }
1283
+ function installSkill(agent2, opts = {}) {
1284
+ const dir = join5(agent2.skillsDir, SKILL_NAME);
1285
+ const previous = installedVersion(dir);
1286
+ const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
1287
+ if (outcome !== "current" && !opts.dryRun) {
1288
+ mkdirSync2(dir, { recursive: true });
1289
+ writeFileSync4(join5(dir, "SKILL.md"), skillDocument());
1290
+ writeFileSync4(join5(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
1291
+ `);
1292
+ }
1293
+ return { agent: agent2.id, path: dir, outcome };
1294
+ }
1295
+
823
1296
  // src/subscriptions.ts
824
1297
  import { spawn as spawn3 } from "child_process";
825
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
826
- import { homedir as homedir3 } from "os";
827
- import { join as join3 } from "path";
1298
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
1299
+ import { homedir as homedir5 } from "os";
1300
+ import { join as join6 } from "path";
828
1301
  import { createInterface as createInterface2 } from "readline";
829
1302
  var ALIASES = {
830
1303
  claude: "anthropic",
@@ -840,6 +1313,14 @@ function providerFor(name) {
840
1313
  }
841
1314
  return provider;
842
1315
  }
1316
+ function managedProviderFor(name) {
1317
+ if (name.toLowerCase() === "cursor") return "cursor";
1318
+ const provider = ALIASES[name.toLowerCase()];
1319
+ if (!provider) {
1320
+ throw new ApiError(400, "unknown_provider", `unknown provider "${name}" \u2014 use claude, chatgpt or cursor`);
1321
+ }
1322
+ return provider;
1323
+ }
843
1324
  function ask(question) {
844
1325
  return new Promise((done) => {
845
1326
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
@@ -902,12 +1383,12 @@ async function connectClaude() {
902
1383
  console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
903
1384
  }
904
1385
  async function connectChatgpt(opts) {
905
- const codexHome = process.env.CODEX_HOME || join3(homedir3(), ".codex");
906
- const path = join3(codexHome, "auth.json");
907
- if (!existsSync3(path)) {
1386
+ const codexHome = process.env.CODEX_HOME || join6(homedir5(), ".codex");
1387
+ const path = join6(codexHome, "auth.json");
1388
+ if (!existsSync5(path)) {
908
1389
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
909
1390
  }
910
- const raw = readFileSync5(path, "utf8");
1391
+ const raw = readFileSync7(path, "utf8");
911
1392
  let accountId;
912
1393
  try {
913
1394
  const parsed = JSON.parse(raw);
@@ -937,7 +1418,7 @@ async function listSubscriptions() {
937
1418
  }
938
1419
  for (const s of modelSubscriptions) {
939
1420
  const bits = [
940
- s.provider === "anthropic" ? "claude" : "chatgpt",
1421
+ MODEL_SUBSCRIPTION_PROVIDER_NAME[s.provider].toLowerCase(),
941
1422
  s.status,
942
1423
  s.expiresAt ? `expires ~${s.expiresAt.slice(0, 10)}` : null,
943
1424
  s.lastUsedAt ? `last used ${s.lastUsedAt.slice(0, 10)}` : null,
@@ -949,8 +1430,8 @@ async function listSubscriptions() {
949
1430
  }
950
1431
  }
951
1432
  async function disconnect(name, opts) {
952
- const provider = providerFor(name);
953
- const label = provider === "anthropic" ? "Claude" : "ChatGPT";
1433
+ const provider = managedProviderFor(name);
1434
+ const label = MODEL_SUBSCRIPTION_PROVIDER_NAME[provider];
954
1435
  if (!opts.yes) {
955
1436
  const ok = await confirm2(`Disconnect ${label}? Agents running on it will fail until reconnected or switched to Platform.`);
956
1437
  if (!ok) {
@@ -976,7 +1457,7 @@ function printModelBilling(session2) {
976
1457
  if (session2.modelBilling !== "account") return;
977
1458
  const provider = modelSubscriptionProviderFor(session2.agentType, session2.llm);
978
1459
  if (!provider) return;
979
- const label = provider === "anthropic" ? "your Claude subscription" : "your ChatGPT plan";
1460
+ const label = `your ${MODEL_SUBSCRIPTION_PLAN_PHRASE[provider]}`;
980
1461
  console.log(`Model billing: ${label} \u2014 $0 model usage`);
981
1462
  }
982
1463
  var program = new Command();
@@ -1149,7 +1630,7 @@ function renderResource(r) {
1149
1630
  const ref = r.checkout ? `@${r.checkout.type === "branch" ? r.checkout.name : r.checkout.sha}` : "";
1150
1631
  return `${r.id} repo ${r.mount_path} ${r.url}${ref}`;
1151
1632
  }
1152
- session.command("create <agent>").description("create a session (starts provisioning by default)").option("--title <title>", "session title").option("--message <text>", "start the first turn immediately, provisioning even with --deferred").option("--deferred", "leave the session idle without a pod until its first turn").option("--vcpus <n>", "machine vCPUs: 1, 2, 4 or 8 (default 2)").option("--memory <mb>", "machine memory in MB, 256MB steps (default 4096)").option("--file <path...>", "mount local files read-only in the working directory: path[:/mount] (uploaded first)").option("--repo <url...>", "clone GitHub repositories into the working directory: https://github.com/{owner}/{repo}[@branch|@sha]").option("--repo-token <token>", "token for the --repo clones; compiled into the sandbox's egress policy, never sent to it").action(
1633
+ session.command("create <agent>").description("create a session (starts provisioning by default)").option("--title <title>", "session title").option("--message <text>", "start the first turn immediately, provisioning even with --deferred").option("--deferred", "leave the session idle without a pod until its first turn").option("--vcpus <n>", "machine vCPUs: 1, 2, 4 or 8 (default 2)").option("--memory <mb>", "machine memory in MB, 256MB steps (default 4096)").option("--file <path...>", "mount local files read-only in the working directory: path[:/mount] (uploaded first)").option("--repo <url...>", "clone GitHub repositories into the working directory: https://github.com/{owner}/{repo}[@branch|@sha]").option("--repo-token <token>", "token for the --repo clones; compiled into the sandbox's egress policy, never sent to it").option("--json", "print the created session as JSON").action(
1153
1634
  run(
1154
1635
  async (agentRef, opts) => {
1155
1636
  const resources = await resourceParams(opts);
@@ -1162,6 +1643,10 @@ session.command("create <agent>").description("create a session (starts provisio
1162
1643
  ...resources.length ? { resources } : {},
1163
1644
  ...opts.message ? { initial_events: [{ type: "user.message", content: [{ type: "text", text: opts.message }] }] } : {}
1164
1645
  });
1646
+ if (opts.json) {
1647
+ console.log(JSON.stringify(created));
1648
+ return;
1649
+ }
1165
1650
  console.log(`Created ${created.id} (${created.agent} \xB7 ${created.status})${created.default ? " [default]" : ""}`);
1166
1651
  printModelBilling(created);
1167
1652
  for (const r of created.resources ?? []) console.log(` ${renderResource(r)}`);
@@ -1238,6 +1723,21 @@ session.command("interrupt <ref>").description("abort the in-flight turn").actio
1238
1723
  console.log(res.status === "no_turn" ? "nothing in flight" : "interrupting");
1239
1724
  })
1240
1725
  );
1726
+ session.command("read <ref>").description("what the agent has said since the last message it was sent").option("--wait", "wait until the agent finishes its turn").option("--timeout <seconds>", "how long --wait waits (default 600)").option("--json", "print { finished, messages, errors } as JSON").action(
1727
+ run(async (ref, opts) => {
1728
+ const reading = await readTurn(await resolveSessionRef(ref), {
1729
+ wait: opts.wait,
1730
+ ...opts.timeout !== void 0 ? { timeoutSeconds: Number(opts.timeout) } : {}
1731
+ });
1732
+ if (opts.json) {
1733
+ console.log(JSON.stringify(reading));
1734
+ return;
1735
+ }
1736
+ for (const message of reading.messages) console.log(message);
1737
+ for (const error of reading.errors) console.error(`error: ${error}`);
1738
+ if (!reading.finished) console.error("The agent is still working. Run this again to read more.");
1739
+ })
1740
+ );
1241
1741
  var sessionResources = session.command("resources").description("files and GitHub repositories mounted into a session's working directory");
1242
1742
  var resourcesPath = (id) => `/sessions/${encodeURIComponent(id)}/resources`;
1243
1743
  sessionResources.command("list <ref>").description("the session's resources, oldest first").action(
@@ -1392,6 +1892,40 @@ async function chatLoop(sessionId, opts = {}) {
1392
1892
  await new Promise(() => {
1393
1893
  });
1394
1894
  }
1895
+ program.command("catalog").description("the agents that can be started by handle").command("search [words...]").description("find an agent for a piece of work; no words lists every agent").option("--category <category>", "only this category").option("--json", "print the full listings as JSON").action(
1896
+ run(async (words, opts) => {
1897
+ const query = new URLSearchParams();
1898
+ if (words.length) query.set("q", words.join(" "));
1899
+ if (opts.category) query.set("category", opts.category);
1900
+ const { agents } = await api("GET", `/catalog/agents${query.size ? `?${query}` : ""}`);
1901
+ if (opts.json) {
1902
+ console.log(JSON.stringify(agents));
1903
+ return;
1904
+ }
1905
+ if (!agents.length) {
1906
+ console.log("No agent matches. `sky catalog search` with no words lists every agent.");
1907
+ return;
1908
+ }
1909
+ for (const a of agents) console.log(`@${a.handle.padEnd(24)} ${a.name.padEnd(20)} ${a.tagline}`);
1910
+ console.log(`
1911
+ Start one: sky session create @<handle> --message "<the job>"`);
1912
+ })
1913
+ );
1914
+ program.command("skill").description("the AgentSky skill for the agents on this machine").command("install").description("add the AgentSky skill to each installed agent (Claude Code, Codex, Cursor)").option("--agent <id...>", "only these agents: claude_code | codex | cursor").option("--dry-run", "say what would be written; write nothing").option("--json", "print the outcome per agent as JSON").action(
1915
+ run(async (opts) => {
1916
+ const installs = selectLocalAgents(opts.agent).map((a) => installSkill(a, { dryRun: opts.dryRun }));
1917
+ if (opts.json) {
1918
+ console.log(JSON.stringify(installs));
1919
+ return;
1920
+ }
1921
+ if (!installs.length) {
1922
+ console.log("No agent found on this machine (looked for Claude Code, Codex and Cursor).");
1923
+ return;
1924
+ }
1925
+ const verb = { installed: opts.dryRun ? "would install" : "installed", updated: opts.dryRun ? "would update" : "updated", current: "up to date" };
1926
+ for (const i of installs) console.log(`${i.agent.padEnd(12)} ${verb[i.outcome].padEnd(14)} ${i.path}`);
1927
+ })
1928
+ );
1395
1929
  var secrets = program.command("secrets").description("agent secrets (values write-only)");
1396
1930
  secrets.command("set <id> <pairs...>").description("set KEY=VALUE pairs on the agent").action(
1397
1931
  run(async (id, pairs) => {
@@ -1427,7 +1961,7 @@ secrets.command("unset <id> <key>").action(
1427
1961
  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(
1428
1962
  run(async (opts) => {
1429
1963
  const path = resolve3(opts.file);
1430
- if (!existsSync4(path)) {
1964
+ if (!existsSync6(path)) {
1431
1965
  const name = basename3(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
1432
1966
  scaffoldAgentFile(path, name);
1433
1967
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
@@ -1493,6 +2027,103 @@ program.command("clone").description("clone your local agent (Claude Code, \u202
1493
2027
  async (opts) => clone(opts)
1494
2028
  )
1495
2029
  );
2030
+ function formatRelative(msAgo) {
2031
+ const seconds = Math.round(msAgo / 1e3);
2032
+ if (seconds < 90) return "just now";
2033
+ const minutes = Math.round(seconds / 60);
2034
+ if (minutes < 90) return `${minutes}m ago`;
2035
+ const hours = Math.round(minutes / 60);
2036
+ if (hours < 48) return `${hours}h ago`;
2037
+ return `${Math.round(hours / 24)}d ago`;
2038
+ }
2039
+ 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(
2040
+ run(
2041
+ async (opts) => {
2042
+ const limit = Math.max(1, Math.min(50, parseInt(opts.limit ?? "10", 10) || 10));
2043
+ if (opts.harness && !["claude_code", "codex", "all"].includes(opts.harness)) throw new Error("Use --harness claude_code, codex, or all.");
2044
+ const harness = opts.harness ?? "all";
2045
+ const base = opts.url ?? agentUseBase();
2046
+ const candidates = opts.session ? listRecentSessions(harness === "all" ? void 0 : harness, Number.POSITIVE_INFINITY).filter(
2047
+ (s) => s.sessionId === opts.session
2048
+ ) : listRecentSessions(harness === "all" ? void 0 : harness, limit);
2049
+ if (candidates.length === 0) {
2050
+ if (opts.session) {
2051
+ throw new Error(`Session "${opts.session}" not found in ~/.claude or ~/.codex.`);
2052
+ }
2053
+ console.log(
2054
+ "No recent sessions found. Make sure you have run Claude Code (sessions in ~/.claude) or Codex (sessions in ~/.codex)."
2055
+ );
2056
+ return;
2057
+ }
2058
+ let toImport;
2059
+ if (opts.all || opts.session || candidates.length === 1) {
2060
+ toImport = candidates;
2061
+ } else {
2062
+ if (!process.stdin.isTTY) throw new Error("Use --session or --all to select conversations in a non-interactive terminal.");
2063
+ console.log("\nRecent local sessions:\n");
2064
+ candidates.forEach((s, i) => {
2065
+ const label = s.title ? `"${s.title.slice(0, 70)}"` : "(no title)";
2066
+ const ago = formatRelative(Date.now() - s.mtimeMs);
2067
+ const harnessBadge = s.harness === "codex" ? "[Codex]" : "[Claude Code]";
2068
+ const ws = s.workspace.replace(process.env.HOME ?? "", "~").slice(0, 60);
2069
+ console.log(` ${i + 1}. ${harnessBadge} ${ago} ${label}`);
2070
+ console.log(` ${ws}`);
2071
+ });
2072
+ console.log();
2073
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
2074
+ const answer = await new Promise(
2075
+ (resolve4) => rl.question(
2076
+ `Import which session? [1-${candidates.length}, or comma-separated, or "all"]: `,
2077
+ (a) => {
2078
+ rl.close();
2079
+ resolve4(a.trim());
2080
+ }
2081
+ )
2082
+ );
2083
+ if (answer.toLowerCase() === "all") {
2084
+ toImport = candidates;
2085
+ } else {
2086
+ const picks = answer.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < candidates.length);
2087
+ if (picks.length === 0) {
2088
+ console.log("Nothing selected.");
2089
+ return;
2090
+ }
2091
+ toImport = picks.map((i) => candidates[i]);
2092
+ }
2093
+ }
2094
+ toImport = [...new Map(toImport.map((session2) => [`${session2.harness}:${session2.sessionId}`, session2])).values()];
2095
+ console.log(`Selected ${toImport.length} conversation(s), ${toImport.reduce((count, session2) => count + session2.files.length, 0)} transcript file(s). Destination: ${base}`);
2096
+ console.log("The recorded conversation is redacted locally. Local project files and login credentials are not transferred.");
2097
+ if (!opts.yes) {
2098
+ if (!process.stdin.isTTY) throw new Error("Use --yes to confirm this transfer in a non-interactive terminal.");
2099
+ const prompt = createInterface3({ input: process.stdin, output: process.stdout });
2100
+ const answer = await new Promise((resolve4) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
2101
+ prompt.close();
2102
+ resolve4(value);
2103
+ }));
2104
+ if (!/^y(?:es)?$/i.test(answer.trim())) {
2105
+ console.log("Cancelled. Nothing uploaded.");
2106
+ return;
2107
+ }
2108
+ }
2109
+ for (const session2 of toImport) {
2110
+ const label = session2.title ? `"${session2.title.slice(0, 60)}"` : session2.sessionId;
2111
+ process.stdout.write(`
2112
+ Importing ${label} \u2026
2113
+ `);
2114
+ const result = await importSession({
2115
+ session: session2,
2116
+ agentUseUrl: base,
2117
+ onProgress: (msg) => process.stdout.write(` ${msg}
2118
+ `)
2119
+ });
2120
+ console.log(` \u2713 Imported. Continue here:
2121
+ ${result.url}
2122
+ `);
2123
+ }
2124
+ }
2125
+ )
2126
+ );
1496
2127
  var files = program.command("files").description("files: upload once, reference by id from any message");
1497
2128
  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(
1498
2129
  run(async (path, opts) => {
@@ -1528,7 +2159,7 @@ files.command("rm <id>").description("delete a file (a session that already rece
1528
2159
  files.command("download <id>").description("download a session file's bytes").requiredOption("-o, --out <path>", "where to write the bytes").action(
1529
2160
  run(async (id, opts) => {
1530
2161
  const { bytes } = await downloadFile(id);
1531
- writeFileSync4(opts.out, bytes);
2162
+ writeFileSync5(opts.out, bytes);
1532
2163
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
1533
2164
  })
1534
2165
  );