@agentsky/sky 2.2.1 → 2.4.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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js CHANGED
@@ -4,18 +4,20 @@ import {
4
4
  REASONING_EFFORT_VALUES,
5
5
  capabilitiesSchema,
6
6
  customDataSchema,
7
+ defaultLlm,
7
8
  external_exports,
8
- isEffortSupported,
9
+ isModelEffortSupported,
9
10
  mcpServersFromKept,
10
11
  modelSubscriptionProviderFor,
11
12
  reasoningEffortDocLine,
13
+ redactLine,
12
14
  skillRefSchema,
13
15
  splitMcpServers,
14
16
  supersededBy
15
- } from "./chunk-TMLNYYYY.js";
17
+ } from "./chunk-FSAF6HIL.js";
16
18
 
17
19
  // src/index.ts
18
- import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
20
+ import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
19
21
  import { basename as basename3, resolve as resolve3 } from "path";
20
22
  import { createInterface as createInterface3 } from "readline";
21
23
  import { Command } from "commander";
@@ -60,7 +62,7 @@ function currentUniverse() {
60
62
  }
61
63
 
62
64
  // src/version.ts
63
- var CLI_VERSION = "2.2.1";
65
+ var CLI_VERSION = "2.4.0";
64
66
  var USER_AGENT = `sky-cli/${CLI_VERSION} (${process.platform}; node/${process.versions.node})`;
65
67
 
66
68
  // src/api.ts
@@ -108,6 +110,7 @@ async function api(method, path, body, opts = {}) {
108
110
  body: body !== void 0 ? JSON.stringify(body) : void 0
109
111
  });
110
112
  if (!res.ok) await parseError(res);
113
+ if (res.status === 204) return void 0;
111
114
  return await res.json();
112
115
  }
113
116
  async function postEvents(sessionId, events, idempotencyKey) {
@@ -126,7 +129,7 @@ async function postEvents(sessionId, events, idempotencyKey) {
126
129
  }
127
130
  async function uploadFile(bytes, filename, mediaType, expiresInSeconds) {
128
131
  const form = new FormData();
129
- form.append("file", new Blob([bytes], { type: mediaType }), filename);
132
+ form.append("file", new Blob([new Uint8Array(bytes)], { type: mediaType }), filename);
130
133
  if (expiresInSeconds) form.append("expires_in_seconds", String(expiresInSeconds));
131
134
  const res = await fetch(`${apiBase()}/files`, {
132
135
  method: "POST",
@@ -162,7 +165,7 @@ async function resolveSessionRef(ref) {
162
165
  return agent2.defaultSessionId;
163
166
  }
164
167
  async function* streamEvents(sessionId, signal) {
165
- const { parseSseStream } = await import("./src-NNPV5GRJ.js");
168
+ const { parseSseStream } = await import("./src-67OBSB2Q.js");
166
169
  const res = await fetch(
167
170
  `${apiBase()}/sessions/${encodeURIComponent(sessionId)}/stream`,
168
171
  {
@@ -341,7 +344,7 @@ function readAgentFile(path) {
341
344
  throw new Error(`${path}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? "invalid"}`);
342
345
  }
343
346
  const file = parsed.data;
344
- if (file.reasoning_effort && file.type && !isEffortSupported(file.type, file.reasoning_effort)) {
347
+ if (file.reasoning_effort && file.type && !isModelEffortSupported(file.type, file.llm ?? defaultLlm(file.type), file.reasoning_effort)) {
345
348
  throw new Error(
346
349
  `${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
347
350
  );
@@ -676,8 +679,303 @@ async function clone(opts) {
676
679
  else await createClone(local, opts);
677
680
  }
678
681
 
682
+ // src/import-local.ts
683
+ import { closeSync, existsSync as existsSync3, openSync, readdirSync, readFileSync as readFileSync5, readSync, statSync } from "fs";
684
+ import { homedir as homedir3 } from "os";
685
+ import { join as join3 } from "path";
686
+ import { createHash } from "crypto";
687
+ function redactLine2(raw) {
688
+ return redactLine(raw).line;
689
+ }
690
+ function decodeWorkspace(encoded) {
691
+ try {
692
+ return decodeURIComponent(encoded.replace(/-/g, "%").replace(/_/g, "%5F"));
693
+ } catch {
694
+ return encoded;
695
+ }
696
+ }
697
+ function readPrefix(filePath) {
698
+ const fd = openSync(filePath, "r");
699
+ try {
700
+ const bytes = Buffer.alloc(256 * 1024);
701
+ return bytes.subarray(0, readSync(fd, bytes, 0, bytes.length, 0)).toString("utf8");
702
+ } finally {
703
+ closeSync(fd);
704
+ }
705
+ }
706
+ function sniffTitle(filePath) {
707
+ try {
708
+ const data = readPrefix(filePath);
709
+ for (const raw of data.split("\n")) {
710
+ if (!raw.trim()) continue;
711
+ let line;
712
+ try {
713
+ line = JSON.parse(redactLine2(raw));
714
+ } catch {
715
+ continue;
716
+ }
717
+ if (!line || typeof line !== "object") continue;
718
+ const rec = line;
719
+ if (rec.type === "user" && rec.message && typeof rec.message === "object") {
720
+ const msg = rec.message;
721
+ if (Array.isArray(msg.content)) {
722
+ for (const block of msg.content) {
723
+ if (typeof block === "object" && block !== null) {
724
+ const b = block;
725
+ if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
726
+ return b.text.trim().slice(0, 120);
727
+ }
728
+ }
729
+ }
730
+ }
731
+ if (typeof msg.content === "string" && msg.content.trim()) {
732
+ return msg.content.trim().slice(0, 120);
733
+ }
734
+ }
735
+ if (rec.type === "event_msg" && rec.payload && typeof rec.payload === "object") {
736
+ const payload = rec.payload;
737
+ if (payload.type === "user_message" && typeof payload.message === "string") return redactLine2(payload.message).slice(0, 120);
738
+ }
739
+ if (rec.type === "response_item" && rec.payload && typeof rec.payload === "object") {
740
+ const payload = rec.payload;
741
+ if (payload.type === "message" && payload.role === "user" && Array.isArray(payload.content)) {
742
+ const text = payload.content.find((part) => typeof part?.text === "string")?.text;
743
+ if (typeof text === "string" && text.trim()) return redactLine2(text.trim()).slice(0, 120);
744
+ }
745
+ }
746
+ if (rec.type === "session_meta") continue;
747
+ if (rec.type === "user_turn" || rec.type === "message" && rec.role === "user") {
748
+ const content = rec.content;
749
+ if (typeof content === "string" && content.trim()) return content.trim().slice(0, 120);
750
+ }
751
+ }
752
+ return null;
753
+ } catch {
754
+ return null;
755
+ }
756
+ }
757
+ function listClaudeCodeSessions(limit) {
758
+ const root = process.env.CLAUDE_CONFIG_DIR ?? join3(homedir3(), ".claude");
759
+ const projectsDir = join3(root, "projects");
760
+ if (!existsSync3(projectsDir)) return [];
761
+ const sessions = [];
762
+ try {
763
+ for (const encoded of readdirSync(projectsDir)) {
764
+ const projectDir = join3(projectsDir, encoded);
765
+ let entries;
766
+ try {
767
+ entries = readdirSync(projectDir);
768
+ } catch {
769
+ continue;
770
+ }
771
+ const workspace = decodeWorkspace(encoded);
772
+ for (const entry of entries) {
773
+ if (!entry.endsWith(".jsonl")) continue;
774
+ const sessionId = entry.slice(0, -6);
775
+ const mainFile = join3(projectDir, entry);
776
+ let stat;
777
+ try {
778
+ stat = statSync(mainFile);
779
+ } catch {
780
+ continue;
781
+ }
782
+ if (!stat.isFile()) continue;
783
+ let discoveryError;
784
+ const subagentFiles = [];
785
+ const subagentDir = join3(projectDir, sessionId, "subagents");
786
+ if (existsSync3(subagentDir)) {
787
+ try {
788
+ const subFiles = readdirSync(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
789
+ for (const f of subFiles) {
790
+ subagentFiles.push(join3(subagentDir, f));
791
+ }
792
+ } catch {
793
+ discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
794
+ }
795
+ }
796
+ sessions.push({
797
+ harness: "claude_code",
798
+ sessionId,
799
+ workspace,
800
+ title: sniffTitle(mainFile),
801
+ mtimeMs: stat.mtimeMs,
802
+ files: [mainFile, ...subagentFiles],
803
+ ...discoveryError ? { discoveryError } : {}
804
+ });
805
+ }
806
+ }
807
+ } catch {
808
+ }
809
+ return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
810
+ }
811
+ function listCodexSessions(limit) {
812
+ const sessionsDir = join3(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join3(homedir3(), ".codex"), "sessions");
813
+ const records = [];
814
+ function walk(dir) {
815
+ let entries;
816
+ try {
817
+ entries = readdirSync(dir, { withFileTypes: true });
818
+ } catch {
819
+ return;
820
+ }
821
+ for (const entry of entries) {
822
+ const full = join3(dir, entry.name);
823
+ if (entry.isDirectory()) {
824
+ walk(full);
825
+ continue;
826
+ }
827
+ if (!entry.isFile() || !entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl")) continue;
828
+ try {
829
+ const first = readPrefix(full).split("\n", 1)[0];
830
+ const meta = JSON.parse(first);
831
+ if (meta.type !== "session_meta" || typeof meta.payload?.id !== "string") continue;
832
+ const data = meta.payload;
833
+ const parent = data.forked_from_id ?? data.source?.spawn?.parent_thread_id ?? data.source?.subagent?.spawn?.parent_thread_id;
834
+ records.push({
835
+ session: {
836
+ harness: "codex",
837
+ sessionId: data.id,
838
+ workspace: typeof data.cwd === "string" ? data.cwd : dir,
839
+ title: sniffTitle(full),
840
+ mtimeMs: statSync(full).mtimeMs,
841
+ files: [full]
842
+ },
843
+ ...typeof parent === "string" ? { parent } : {}
844
+ });
845
+ } catch {
846
+ continue;
847
+ }
848
+ }
849
+ }
850
+ walk(sessionsDir);
851
+ function descendants(id, visited) {
852
+ if (visited.has(id)) return [];
853
+ visited.add(id);
854
+ return records.filter((item) => item.parent === id).flatMap((item) => [
855
+ ...item.session.files,
856
+ ...descendants(item.session.sessionId, visited)
857
+ ]);
858
+ }
859
+ return records.filter((item) => !item.parent).map(({ session: session2 }) => ({
860
+ ...session2,
861
+ files: [.../* @__PURE__ */ new Set([...session2.files, ...descendants(session2.sessionId, /* @__PURE__ */ new Set())])]
862
+ })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
863
+ }
864
+ function listRecentSessions(harness, limit) {
865
+ const claudeSessions = harness === "codex" ? [] : listClaudeCodeSessions(limit);
866
+ const codexSessions = harness === "claude_code" ? [] : listCodexSessions(limit);
867
+ return [...claudeSessions, ...codexSessions].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
868
+ }
869
+ function agentUseBase() {
870
+ const env = process.env.AGENT_USE_URL?.trim().replace(/\/$/, "");
871
+ if (env) return env;
872
+ const api2 = apiBase().replace(/\/$/, "");
873
+ const derived = api2.replace(/\/api\/v1$/, "").replace(/\/v1$/, "").replace("api.agentsky.dev", "agentsky.dev");
874
+ if (derived.includes("localhost:3000")) return derived.replace("localhost:3000", "localhost:3002");
875
+ return derived;
876
+ }
877
+ async function mintTrace(base, harness) {
878
+ const token = apiToken();
879
+ if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
880
+ const res = await fetch(`${base}/api/traces`, {
881
+ method: "POST",
882
+ redirect: "error",
883
+ signal: AbortSignal.timeout(15e3),
884
+ headers: {
885
+ "content-type": "application/json",
886
+ authorization: `Bearer ${token}`,
887
+ "user-agent": USER_AGENT
888
+ },
889
+ body: JSON.stringify({ harness })
890
+ });
891
+ if (!res.ok) {
892
+ const body = await res.json().catch(() => null);
893
+ throw new Error(
894
+ typeof body?.error === "string" ? body.error : `Failed to create import (${res.status}).`
895
+ );
896
+ }
897
+ return await res.json();
898
+ }
899
+ function prepareImport(session2) {
900
+ if (session2.discoveryError) throw new Error(session2.discoveryError);
901
+ const maximum = 16 * 1024 * 1024;
902
+ let inputBytes = 0;
903
+ let outputBytes = 0;
904
+ const lines = [];
905
+ const digest = createHash("sha256");
906
+ let nativeId;
907
+ for (const path of session2.files) {
908
+ const stat = statSync(path);
909
+ if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
910
+ const text = readFileSync5(path, "utf8");
911
+ for (const raw of text.split("\n")) {
912
+ if (!raw.trim()) continue;
913
+ const record = JSON.parse(raw);
914
+ if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("Transcript records must be JSON objects.");
915
+ if (String(record.type).startsWith("import.")) throw new Error("Native transcripts cannot supply import protocol records.");
916
+ const id = record.type === "session_meta" ? record.payload?.id : record.sessionId;
917
+ if (!nativeId && typeof id === "string") nativeId = id;
918
+ const clean = redactLine(raw.replace(/\r$/, ""), { requireStructured: true }).line;
919
+ const line = clean + "\n";
920
+ outputBytes += Buffer.byteLength(line);
921
+ if (outputBytes > maximum) throw new Error("Redacted transcript exceeds the 16 MiB limit.");
922
+ digest.update(line);
923
+ lines.push(line);
924
+ }
925
+ }
926
+ if (!lines.length) throw new Error("The selected transcript is empty.");
927
+ const sourceId = nativeId ?? session2.sessionId;
928
+ 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.");
929
+ const summary = {
930
+ format: "asteroids.session-summary",
931
+ format_version: 1,
932
+ source_harness: session2.harness,
933
+ source_harness_version: null,
934
+ source_session_id: sourceId,
935
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
936
+ 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.",
937
+ context: {
938
+ objective: "Review the imported conversation and continue its latest unfinished authorized objective.",
939
+ constraints: [],
940
+ decisions: [],
941
+ completed: [],
942
+ pending: ["Read the complete imported transcript, reconcile the latest human instructions and completed work, then continue."],
943
+ verification: [],
944
+ blockers: ["This handoff contains the recorded conversation only. Referenced local files, runtime state, and unreadable reasoning are not transferred."],
945
+ artifacts: [],
946
+ provenance: { native_checkpoint_sha256: digest.digest("hex"), selected_native_session_id: sourceId, context_bytes: outputBytes }
947
+ }
948
+ };
949
+ return lines.join("") + JSON.stringify({ type: "import.summary", package: summary }) + "\n" + JSON.stringify({ type: "import.end", records: lines.length + 1 }) + "\n";
950
+ }
951
+ async function importSession(opts) {
952
+ const base = (opts.agentUseUrl ?? agentUseBase()).replace(/\/$/, "");
953
+ const destination = new URL(base);
954
+ if (destination.username || destination.password || destination.protocol !== "https:" && !(destination.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(destination.hostname))) {
955
+ throw new Error("Import requires HTTPS, or a loopback development server.");
956
+ }
957
+ opts.onProgress?.("Reading transcript and redacting secrets locally\u2026");
958
+ const body = prepareImport(opts.session);
959
+ opts.onProgress?.("Minting import key\u2026");
960
+ const { traceId, key } = await mintTrace(base, opts.session.harness);
961
+ opts.onProgress?.("Uploading the complete transcript\u2026");
962
+ const response = await fetch(`${base}/api/traces/ingest`, {
963
+ method: "POST",
964
+ redirect: "error",
965
+ signal: AbortSignal.timeout(12e4),
966
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/x-ndjson", "x-source-harness": opts.session.harness, "user-agent": USER_AGENT },
967
+ body
968
+ });
969
+ if (!response.ok) {
970
+ const result2 = await response.json().catch(() => null);
971
+ throw new Error(typeof result2?.error === "string" ? result2.error : result2?.error?.message ?? `Upload failed (${response.status}).`);
972
+ }
973
+ const result = await response.json();
974
+ return { traceId: result.traceId ?? traceId, url: `${base}/playground/new?import=${encodeURIComponent(result.traceId ?? traceId)}` };
975
+ }
976
+
679
977
  // src/login.ts
680
- import { createHash, randomBytes } from "crypto";
978
+ import { createHash as createHash2, randomBytes } from "crypto";
681
979
  import { createServer } from "http";
682
980
  import { hostname } from "os";
683
981
  import { spawn as spawn2 } from "child_process";
@@ -693,7 +991,7 @@ function openBrowser2(url) {
693
991
  }
694
992
  async function login(opts) {
695
993
  const verifier = b64url(randomBytes(32));
696
- const challenge = b64url(createHash("sha256").update(verifier).digest());
994
+ const challenge = b64url(createHash2("sha256").update(verifier).digest());
697
995
  const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
698
996
  saveConfig({
699
997
  ...loadConfig(),
@@ -820,9 +1118,9 @@ function renderEvent(event, data) {
820
1118
 
821
1119
  // src/subscriptions.ts
822
1120
  import { spawn as spawn3 } from "child_process";
823
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
824
- import { homedir as homedir3 } from "os";
825
- import { join as join3 } from "path";
1121
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
1122
+ import { homedir as homedir4 } from "os";
1123
+ import { join as join4 } from "path";
826
1124
  import { createInterface as createInterface2 } from "readline";
827
1125
  var ALIASES = {
828
1126
  claude: "anthropic",
@@ -900,12 +1198,12 @@ async function connectClaude() {
900
1198
  console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
901
1199
  }
902
1200
  async function connectChatgpt(opts) {
903
- const codexHome = process.env.CODEX_HOME || join3(homedir3(), ".codex");
904
- const path = join3(codexHome, "auth.json");
905
- if (!existsSync3(path)) {
1201
+ const codexHome = process.env.CODEX_HOME || join4(homedir4(), ".codex");
1202
+ const path = join4(codexHome, "auth.json");
1203
+ if (!existsSync4(path)) {
906
1204
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
907
1205
  }
908
- const raw = readFileSync5(path, "utf8");
1206
+ const raw = readFileSync6(path, "utf8");
909
1207
  let accountId;
910
1208
  try {
911
1209
  const parsed = JSON.parse(raw);
@@ -1147,7 +1445,7 @@ function renderResource(r) {
1147
1445
  const ref = r.checkout ? `@${r.checkout.type === "branch" ? r.checkout.name : r.checkout.sha}` : "";
1148
1446
  return `${r.id} repo ${r.mount_path} ${r.url}${ref}`;
1149
1447
  }
1150
- session.command("create <agent>").description("create a session (the pod is provisioned now)").option("--title <title>", "session title").option("--message <text>", "seed the session and start 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(
1448
+ 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(
1151
1449
  run(
1152
1450
  async (agentRef, opts) => {
1153
1451
  const resources = await resourceParams(opts);
@@ -1156,6 +1454,7 @@ session.command("create <agent>").description("create a session (the pod is prov
1156
1454
  ...opts.title ? { title: opts.title } : {},
1157
1455
  ...opts.vcpus !== void 0 ? { vcpus: Number(opts.vcpus) } : {},
1158
1456
  ...opts.memory !== void 0 ? { memoryMb: Number(opts.memory) } : {},
1457
+ ...opts.deferred ? { provision: "deferred" } : {},
1159
1458
  ...resources.length ? { resources } : {},
1160
1459
  ...opts.message ? { initial_events: [{ type: "user.message", content: [{ type: "text", text: opts.message }] }] } : {}
1161
1460
  });
@@ -1277,6 +1576,43 @@ sessionResources.command("rotate <ref> <rid> <token>").description("replace a re
1277
1576
  console.log(`Rotated ${updated.id}`);
1278
1577
  })
1279
1578
  );
1579
+ var sessionPorts = session.command("ports").description("local servers in the session's pod, each exposed on an origin of its own");
1580
+ var portsPath = (id) => `/sessions/${encodeURIComponent(id)}/ports`;
1581
+ function renderPort(p) {
1582
+ return `${String(p.port).padEnd(5)} ${p.visibility.padEnd(7)} ${p.url}`;
1583
+ }
1584
+ sessionPorts.command("list <ref>").description("the session's exposed ports, oldest first").action(
1585
+ run(async (ref) => {
1586
+ const id = await resolveSessionRef(ref);
1587
+ const { ports } = await api("GET", portsPath(id));
1588
+ if (!ports.length) {
1589
+ console.log("No exposed ports. `sky session ports expose <session> <port>` exposes one.");
1590
+ return;
1591
+ }
1592
+ for (const p of ports) console.log(renderPort(p));
1593
+ })
1594
+ );
1595
+ sessionPorts.command("expose <ref> <port>").description("expose a port the agent's local server listens on; prints its address").option("--public", "anyone with the address may open it; without it the port is private (only entry links admit a browser), and an already public port is made private again").action(
1596
+ run(async (ref, port, opts) => {
1597
+ const id = await resolveSessionRef(ref);
1598
+ const exposed = await api("PUT", `${portsPath(id)}/${encodeURIComponent(port)}`, { visibility: opts.public ? "public" : "private" });
1599
+ console.log(renderPort(exposed));
1600
+ })
1601
+ );
1602
+ sessionPorts.command("open <ref> <port>").description("print an entry link for an exposed port \u2014 single-use and good for a minute on a private port").action(
1603
+ run(async (ref, port) => {
1604
+ const id = await resolveSessionRef(ref);
1605
+ const grant = await api("POST", `${portsPath(id)}/${encodeURIComponent(port)}/grants`, {});
1606
+ console.log(grant.url);
1607
+ })
1608
+ );
1609
+ sessionPorts.command("revoke <ref> <port>").description("stop exposing a port; its address answers 404 from then on").action(
1610
+ run(async (ref, port) => {
1611
+ const id = await resolveSessionRef(ref);
1612
+ await api("DELETE", `${portsPath(id)}/${encodeURIComponent(port)}`);
1613
+ console.log(`Revoked port ${port}`);
1614
+ })
1615
+ );
1280
1616
  async function uploadAttachments(opts) {
1281
1617
  const files2 = [];
1282
1618
  for (const path of [...opts.image ?? [], ...opts.file ?? []]) {
@@ -1387,7 +1723,7 @@ secrets.command("unset <id> <key>").action(
1387
1723
  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(
1388
1724
  run(async (opts) => {
1389
1725
  const path = resolve3(opts.file);
1390
- if (!existsSync4(path)) {
1726
+ if (!existsSync5(path)) {
1391
1727
  const name = basename3(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
1392
1728
  scaffoldAgentFile(path, name);
1393
1729
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
@@ -1453,6 +1789,103 @@ program.command("clone").description("clone your local agent (Claude Code, \u202
1453
1789
  async (opts) => clone(opts)
1454
1790
  )
1455
1791
  );
1792
+ function formatRelative(msAgo) {
1793
+ const seconds = Math.round(msAgo / 1e3);
1794
+ if (seconds < 90) return "just now";
1795
+ const minutes = Math.round(seconds / 60);
1796
+ if (minutes < 90) return `${minutes}m ago`;
1797
+ const hours = Math.round(minutes / 60);
1798
+ if (hours < 48) return `${hours}h ago`;
1799
+ return `${Math.round(hours / 24)}d ago`;
1800
+ }
1801
+ 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(
1802
+ run(
1803
+ async (opts) => {
1804
+ const limit = Math.max(1, Math.min(50, parseInt(opts.limit ?? "10", 10) || 10));
1805
+ if (opts.harness && !["claude_code", "codex", "all"].includes(opts.harness)) throw new Error("Use --harness claude_code, codex, or all.");
1806
+ const harness = opts.harness ?? "all";
1807
+ const base = opts.url ?? agentUseBase();
1808
+ const candidates = opts.session ? listRecentSessions(harness === "all" ? void 0 : harness, Number.POSITIVE_INFINITY).filter(
1809
+ (s) => s.sessionId === opts.session
1810
+ ) : listRecentSessions(harness === "all" ? void 0 : harness, limit);
1811
+ if (candidates.length === 0) {
1812
+ if (opts.session) {
1813
+ throw new Error(`Session "${opts.session}" not found in ~/.claude or ~/.codex.`);
1814
+ }
1815
+ console.log(
1816
+ "No recent sessions found. Make sure you have run Claude Code (sessions in ~/.claude) or Codex (sessions in ~/.codex)."
1817
+ );
1818
+ return;
1819
+ }
1820
+ let toImport;
1821
+ if (opts.all || opts.session || candidates.length === 1) {
1822
+ toImport = candidates;
1823
+ } else {
1824
+ if (!process.stdin.isTTY) throw new Error("Use --session or --all to select conversations in a non-interactive terminal.");
1825
+ console.log("\nRecent local sessions:\n");
1826
+ candidates.forEach((s, i) => {
1827
+ const label = s.title ? `"${s.title.slice(0, 70)}"` : "(no title)";
1828
+ const ago = formatRelative(Date.now() - s.mtimeMs);
1829
+ const harnessBadge = s.harness === "codex" ? "[Codex]" : "[Claude Code]";
1830
+ const ws = s.workspace.replace(process.env.HOME ?? "", "~").slice(0, 60);
1831
+ console.log(` ${i + 1}. ${harnessBadge} ${ago} ${label}`);
1832
+ console.log(` ${ws}`);
1833
+ });
1834
+ console.log();
1835
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
1836
+ const answer = await new Promise(
1837
+ (resolve4) => rl.question(
1838
+ `Import which session? [1-${candidates.length}, or comma-separated, or "all"]: `,
1839
+ (a) => {
1840
+ rl.close();
1841
+ resolve4(a.trim());
1842
+ }
1843
+ )
1844
+ );
1845
+ if (answer.toLowerCase() === "all") {
1846
+ toImport = candidates;
1847
+ } else {
1848
+ const picks = answer.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < candidates.length);
1849
+ if (picks.length === 0) {
1850
+ console.log("Nothing selected.");
1851
+ return;
1852
+ }
1853
+ toImport = picks.map((i) => candidates[i]);
1854
+ }
1855
+ }
1856
+ toImport = [...new Map(toImport.map((session2) => [`${session2.harness}:${session2.sessionId}`, session2])).values()];
1857
+ console.log(`Selected ${toImport.length} conversation(s), ${toImport.reduce((count, session2) => count + session2.files.length, 0)} transcript file(s). Destination: ${base}`);
1858
+ console.log("The recorded conversation is redacted locally. Local project files and login credentials are not transferred.");
1859
+ if (!opts.yes) {
1860
+ if (!process.stdin.isTTY) throw new Error("Use --yes to confirm this transfer in a non-interactive terminal.");
1861
+ const prompt = createInterface3({ input: process.stdin, output: process.stdout });
1862
+ const answer = await new Promise((resolve4) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
1863
+ prompt.close();
1864
+ resolve4(value);
1865
+ }));
1866
+ if (!/^y(?:es)?$/i.test(answer.trim())) {
1867
+ console.log("Cancelled. Nothing uploaded.");
1868
+ return;
1869
+ }
1870
+ }
1871
+ for (const session2 of toImport) {
1872
+ const label = session2.title ? `"${session2.title.slice(0, 60)}"` : session2.sessionId;
1873
+ process.stdout.write(`
1874
+ Importing ${label} \u2026
1875
+ `);
1876
+ const result = await importSession({
1877
+ session: session2,
1878
+ agentUseUrl: base,
1879
+ onProgress: (msg) => process.stdout.write(` ${msg}
1880
+ `)
1881
+ });
1882
+ console.log(` \u2713 Imported. Continue here:
1883
+ ${result.url}
1884
+ `);
1885
+ }
1886
+ }
1887
+ )
1888
+ );
1456
1889
  var files = program.command("files").description("files: upload once, reference by id from any message");
1457
1890
  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(
1458
1891
  run(async (path, opts) => {