@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.
- package/dist/{chunk-TMLNYYYY.js → chunk-FSAF6HIL.js} +826 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +451 -18
- package/dist/products.d.ts +615 -0
- package/dist/products.js +207 -0
- package/dist/{src-NNPV5GRJ.js → src-67OBSB2Q.js} +9 -4
- package/package.json +21 -6
package/dist/index.d.ts
ADDED
|
@@ -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
|
-
|
|
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-
|
|
17
|
+
} from "./chunk-FSAF6HIL.js";
|
|
16
18
|
|
|
17
19
|
// src/index.ts
|
|
18
|
-
import { existsSync as
|
|
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.
|
|
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-
|
|
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 && !
|
|
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(
|
|
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
|
|
824
|
-
import { homedir as
|
|
825
|
-
import { join as
|
|
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 ||
|
|
904
|
-
const path =
|
|
905
|
-
if (!
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
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) => {
|