@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/{chunk-2KQLWHHC.js → chunk-MHL5Z6IQ.js} +845 -10
- package/dist/index.js +652 -21
- package/dist/products.d.ts +30 -0
- package/dist/products.js +1 -1
- package/dist/{src-EGXYUSME.js → src-GLBJHVIJ.js} +3 -3
- package/package.json +1 -1
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-
|
|
19
|
+
} from "./chunk-MHL5Z6IQ.js";
|
|
17
20
|
|
|
18
21
|
// src/index.ts
|
|
19
|
-
import { existsSync as
|
|
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.
|
|
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-
|
|
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(
|
|
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
|
|
826
|
-
import { homedir as
|
|
827
|
-
import { join as
|
|
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 ||
|
|
906
|
-
const path =
|
|
907
|
-
if (!
|
|
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 =
|
|
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
|
|
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 =
|
|
953
|
-
const label = provider
|
|
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 =
|
|
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 (!
|
|
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
|
-
|
|
2162
|
+
writeFileSync5(opts.out, bytes);
|
|
1532
2163
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
1533
2164
|
})
|
|
1534
2165
|
);
|