@agentsky/sky 2.21.0 → 2.23.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-GHTAIKQ6.js → chunk-HDPDY77W.js} +73 -37
- package/dist/{chunk-ZZ6UF7QA.js → chunk-HQPOCQRQ.js} +2 -2
- package/dist/{chunk-6JMFB3CV.js → chunk-V5XLV37X.js} +1 -1
- package/dist/index.js +1501 -780
- package/dist/{src-QBXMNV2V.js → src-UMT6FHLG.js} +1 -1
- package/dist/{trace-NMBMU4GS.js → trace-ZRU624EY.js} +2 -2
- package/dist/{trace-library-OWYB75AR.js → trace-library-QOQUAL5H.js} +3 -3
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
resolveSessionRef,
|
|
10
10
|
streamEvents,
|
|
11
11
|
uploadFile
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-HQPOCQRQ.js";
|
|
13
13
|
import {
|
|
14
14
|
CLI_VERSION,
|
|
15
15
|
USER_AGENT,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
currentUniverse,
|
|
21
21
|
loadConfig,
|
|
22
22
|
saveConfig
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-V5XLV37X.js";
|
|
24
24
|
import {
|
|
25
25
|
AGENT_TYPES,
|
|
26
26
|
MODEL_SUBSCRIPTION_PLAN_PHRASE,
|
|
@@ -42,12 +42,12 @@ import {
|
|
|
42
42
|
skillRefSchema,
|
|
43
43
|
splitMcpServers,
|
|
44
44
|
supersededBy
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-HDPDY77W.js";
|
|
46
46
|
import "./chunk-MLKGABMK.js";
|
|
47
47
|
|
|
48
48
|
// src/index.ts
|
|
49
|
-
import { existsSync as
|
|
50
|
-
import { basename as
|
|
49
|
+
import { existsSync as existsSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
|
|
50
|
+
import { basename as basename9, resolve as resolve9 } from "path";
|
|
51
51
|
import { createInterface as createInterface6 } from "readline";
|
|
52
52
|
import { Command } from "commander";
|
|
53
53
|
|
|
@@ -410,6 +410,52 @@ function renderApiKey(key) {
|
|
|
410
410
|
return `${key.id} ${key.token_prefix}\u2026 ${key.status.padEnd(7)} ${key.scopes.join(",").padEnd(16)} ${where} ${used} ${key.name}${key.current ? " (this key)" : ""}`;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
+
// src/account-access.ts
|
|
414
|
+
function linkToken(ref) {
|
|
415
|
+
const fromUrl = /\/s\/([A-Za-z0-9_-]+)\/?(?:[?#].*)?$/.exec(ref.trim());
|
|
416
|
+
const token = fromUrl ? fromUrl[1] : ref.trim();
|
|
417
|
+
if (!/^[A-Za-z0-9_-]{16,64}$/.test(token)) throw new ApiError(400, "invalid_request", "a share link is a /s/<token> URL or its token");
|
|
418
|
+
return token;
|
|
419
|
+
}
|
|
420
|
+
async function sendViaLink(ref, text, idempotencyKey) {
|
|
421
|
+
await api("POST", `/links/${encodeURIComponent(linkToken(ref))}/messages`, { text }, { idempotencyKey });
|
|
422
|
+
}
|
|
423
|
+
async function stopViaLink(ref) {
|
|
424
|
+
await api("POST", `/links/${encodeURIComponent(linkToken(ref))}/interrupt`);
|
|
425
|
+
}
|
|
426
|
+
function inviteCode(ref) {
|
|
427
|
+
const fromUrl = /\/i\/([^/?#]+)\/?(?:[?#].*)?$/.exec(ref.trim());
|
|
428
|
+
const code = fromUrl ? decodeURIComponent(fromUrl[1]) : ref.trim();
|
|
429
|
+
if (!code) throw new ApiError(400, "invalid_request", "a credit invite is an /i/<code> URL or its code");
|
|
430
|
+
return code;
|
|
431
|
+
}
|
|
432
|
+
async function claimCreditInvite(ref) {
|
|
433
|
+
return api("POST", "/billing/credit-invites/claim", { code: inviteCode(ref) });
|
|
434
|
+
}
|
|
435
|
+
function renderCreditInviteClaim(claim) {
|
|
436
|
+
const amount = `$${claim.amount_usd.toFixed(2)}`;
|
|
437
|
+
return claim.status === "granted" ? `Added ${amount} of credit to your balance` : `Already claimed: this link added ${amount} to your balance`;
|
|
438
|
+
}
|
|
439
|
+
async function listAppGrants() {
|
|
440
|
+
return (await api("GET", "/oauth_grants")).oauth_grants;
|
|
441
|
+
}
|
|
442
|
+
async function revokeAppGrant(clientId) {
|
|
443
|
+
await api("DELETE", `/oauth_grants/${encodeURIComponent(clientId)}`);
|
|
444
|
+
}
|
|
445
|
+
function renderAppGrant(grant) {
|
|
446
|
+
const since = grant.allowed_at ? ` since ${grant.allowed_at.slice(0, 10)}` : "";
|
|
447
|
+
const review = grant.review_status === "approved" ? "" : " (unreviewed)";
|
|
448
|
+
return `${grant.client_id} ${grant.name || "(unnamed)"} ${grant.scopes.join(" ")}${since}${review}`;
|
|
449
|
+
}
|
|
450
|
+
async function unsubscribeMarketing() {
|
|
451
|
+
const { notification_preferences } = await api(
|
|
452
|
+
"PATCH",
|
|
453
|
+
"/notification-preferences",
|
|
454
|
+
{ email: { marketing: false } }
|
|
455
|
+
);
|
|
456
|
+
return { marketing: notification_preferences.email.marketing };
|
|
457
|
+
}
|
|
458
|
+
|
|
413
459
|
// src/context.ts
|
|
414
460
|
function memoryBody(operation, version) {
|
|
415
461
|
return { ...operation, expectedVersion: version };
|
|
@@ -682,9 +728,9 @@ function readClaudeCode() {
|
|
|
682
728
|
sources.push("global CLAUDE.md");
|
|
683
729
|
}
|
|
684
730
|
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
685
|
-
const
|
|
686
|
-
if (
|
|
687
|
-
parts.push({ label: `Project ${name}`, content:
|
|
731
|
+
const project2 = readIfExists(join3(process.cwd(), name));
|
|
732
|
+
if (project2?.trim()) {
|
|
733
|
+
parts.push({ label: `Project ${name}`, content: project2 });
|
|
688
734
|
sources.push(`project ${name}`);
|
|
689
735
|
break;
|
|
690
736
|
}
|
|
@@ -739,11 +785,11 @@ function linkPath(configRoot) {
|
|
|
739
785
|
return join3(configRoot, "agentsky-link.json");
|
|
740
786
|
}
|
|
741
787
|
function readLink(configRoot) {
|
|
742
|
-
const
|
|
743
|
-
return
|
|
788
|
+
const link2 = readJson(linkPath(configRoot));
|
|
789
|
+
return link2?.agentId ? link2 : void 0;
|
|
744
790
|
}
|
|
745
|
-
function writeLink(configRoot,
|
|
746
|
-
writeFileSync3(linkPath(configRoot), `${JSON.stringify(
|
|
791
|
+
function writeLink(configRoot, link2) {
|
|
792
|
+
writeFileSync3(linkPath(configRoot), `${JSON.stringify(link2, null, 2)}
|
|
747
793
|
`);
|
|
748
794
|
}
|
|
749
795
|
var DEFAULT_VAULT_NAME = "default";
|
|
@@ -909,14 +955,14 @@ async function createClone(local, opts) {
|
|
|
909
955
|
await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
|
|
910
956
|
if (opts.open) openBrowser(url);
|
|
911
957
|
}
|
|
912
|
-
async function syncClone(local,
|
|
913
|
-
const since =
|
|
958
|
+
async function syncClone(local, link2, opts) {
|
|
959
|
+
const since = link2.lastSyncedAt ?? link2.clonedAt;
|
|
914
960
|
console.log(
|
|
915
|
-
`Syncing ${
|
|
961
|
+
`Syncing ${link2.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
|
|
916
962
|
);
|
|
917
963
|
if (opts.dryRun) {
|
|
918
964
|
console.log(
|
|
919
|
-
` would push prompt (${kb(local.prompt)}) to ${
|
|
965
|
+
` would push prompt (${kb(local.prompt)}) to ${link2.agentId}. Dry run \u2014 nothing sent.`
|
|
920
966
|
);
|
|
921
967
|
return;
|
|
922
968
|
}
|
|
@@ -924,24 +970,24 @@ async function syncClone(local, link, opts) {
|
|
|
924
970
|
try {
|
|
925
971
|
({ agent: agent2 } = await api(
|
|
926
972
|
"GET",
|
|
927
|
-
`/agents/${encodeURIComponent(
|
|
973
|
+
`/agents/${encodeURIComponent(link2.agentId)}`
|
|
928
974
|
));
|
|
929
975
|
} catch (error) {
|
|
930
976
|
if (error instanceof ApiError && error.status === 404) {
|
|
931
977
|
throw new Error(
|
|
932
|
-
`linked agent ${
|
|
978
|
+
`linked agent ${link2.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
|
|
933
979
|
);
|
|
934
980
|
}
|
|
935
981
|
throw error;
|
|
936
982
|
}
|
|
937
983
|
if (agent2.agentType !== local.harness) {
|
|
938
984
|
throw new Error(
|
|
939
|
-
`linked agent ${
|
|
985
|
+
`linked agent ${link2.agentId} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
|
|
940
986
|
);
|
|
941
987
|
}
|
|
942
988
|
const result = await api(
|
|
943
989
|
"PUT",
|
|
944
|
-
`/agents/${encodeURIComponent(
|
|
990
|
+
`/agents/${encodeURIComponent(link2.agentId)}/prompt`,
|
|
945
991
|
{ prompt: local.prompt }
|
|
946
992
|
);
|
|
947
993
|
console.log(
|
|
@@ -949,11 +995,11 @@ async function syncClone(local, link, opts) {
|
|
|
949
995
|
);
|
|
950
996
|
const drift = [];
|
|
951
997
|
const localModel = local.model ?? null;
|
|
952
|
-
const linkedModel =
|
|
998
|
+
const linkedModel = link2.llm ?? agent2.llm;
|
|
953
999
|
if (localModel && linkedModel && localModel !== linkedModel) {
|
|
954
1000
|
drift.push(`model changed ${linkedModel} \u2192 ${localModel}`);
|
|
955
1001
|
}
|
|
956
|
-
const before = new Set(
|
|
1002
|
+
const before = new Set(link2.mcpServers ?? []);
|
|
957
1003
|
const nowKept = Object.keys(local.keptServers);
|
|
958
1004
|
const newServers = nowKept.filter((n) => !before.has(n));
|
|
959
1005
|
const goneServers = [...before].filter((n) => !nowKept.includes(n));
|
|
@@ -968,16 +1014,16 @@ async function syncClone(local, link, opts) {
|
|
|
968
1014
|
` left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
|
|
969
1015
|
);
|
|
970
1016
|
}
|
|
971
|
-
writeLink(local.configRoot, { ...
|
|
972
|
-
console.log(`Synced ${
|
|
1017
|
+
writeLink(local.configRoot, { ...link2, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1018
|
+
console.log(`Synced ${link2.agentId}.`);
|
|
973
1019
|
await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
|
|
974
|
-
if (opts.open) openBrowser(`${apiBase()}/agents/${
|
|
1020
|
+
if (opts.open) openBrowser(`${apiBase()}/agents/${link2.agentId}`);
|
|
975
1021
|
}
|
|
976
1022
|
async function clone(opts) {
|
|
977
1023
|
const harness2 = detectHarness(opts.harness);
|
|
978
1024
|
const local = readLocal(harness2);
|
|
979
|
-
const
|
|
980
|
-
if (
|
|
1025
|
+
const link2 = opts.new ? void 0 : readLink(local.configRoot);
|
|
1026
|
+
if (link2) await syncClone(local, link2, opts);
|
|
981
1027
|
else await createClone(local, opts);
|
|
982
1028
|
}
|
|
983
1029
|
|
|
@@ -1000,7 +1046,7 @@ import { homedir as homedir2 } from "os";
|
|
|
1000
1046
|
import { basename as basename4, dirname as dirname3, join as join4, relative, sep } from "path";
|
|
1001
1047
|
import { parse as parseToml2 } from "smol-toml";
|
|
1002
1048
|
var MAX_TREE = 5e3;
|
|
1003
|
-
var SKILL_LINE = /Base directory for this skill: (\/[^\
|
|
1049
|
+
var SKILL_LINE = /Base directory for this skill: (\/[^\n\r"'\\<>]+)/g;
|
|
1004
1050
|
function git(cwd, ...args) {
|
|
1005
1051
|
const run2 = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 1e4, maxBuffer: 32 * 1024 * 1024 });
|
|
1006
1052
|
return run2.status === 0 ? run2.stdout : null;
|
|
@@ -1046,8 +1092,7 @@ function skillRoots(cwd, home) {
|
|
|
1046
1092
|
];
|
|
1047
1093
|
}
|
|
1048
1094
|
function pluginSkill(plugin, skill, home) {
|
|
1049
|
-
const
|
|
1050
|
-
const walk = (dir, depth) => {
|
|
1095
|
+
const walk = (dir, depth, underPlugin) => {
|
|
1051
1096
|
if (depth > 7) return null;
|
|
1052
1097
|
let entries;
|
|
1053
1098
|
try {
|
|
@@ -1058,13 +1103,13 @@ function pluginSkill(plugin, skill, home) {
|
|
|
1058
1103
|
for (const entry2 of entries) {
|
|
1059
1104
|
if (!entry2.isDirectory() || entry2.name === "node_modules" || entry2.name === ".git") continue;
|
|
1060
1105
|
const path = join4(dir, entry2.name);
|
|
1061
|
-
if (entry2.name === skill && basename4(dir) === "skills" && path.includes(`${sep}${plugin}${sep}`) && isFile(join4(path, "SKILL.md"))) return path;
|
|
1062
|
-
const found = walk(path, depth + 1);
|
|
1106
|
+
if (entry2.name === skill && basename4(dir) === "skills" && (underPlugin || path.includes(`${sep}${plugin}${sep}`)) && isFile(join4(path, "SKILL.md"))) return realpathSync(path);
|
|
1107
|
+
const found = walk(path, depth + 1, underPlugin);
|
|
1063
1108
|
if (found) return found;
|
|
1064
1109
|
}
|
|
1065
1110
|
return null;
|
|
1066
1111
|
};
|
|
1067
|
-
return walk(
|
|
1112
|
+
return walk(join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "plugins"), 0, false) ?? walk(join4(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions", "skills-plugin"), 0, true);
|
|
1068
1113
|
}
|
|
1069
1114
|
function findSkill(name, cwd, home) {
|
|
1070
1115
|
const [scope, skill] = name.includes(":") ? name.split(":", 2) : [null, name];
|
|
@@ -1084,23 +1129,24 @@ function loadedSkills(records, touched, cwd, home = homedir2()) {
|
|
|
1084
1129
|
if (![...dirs.values()].includes(real)) dirs.set(name, real);
|
|
1085
1130
|
return true;
|
|
1086
1131
|
};
|
|
1132
|
+
const called = [];
|
|
1087
1133
|
for (const record of records) {
|
|
1088
1134
|
const texts = [];
|
|
1089
1135
|
strings(record, texts);
|
|
1090
|
-
for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].replace(/[.,;:)]+$/, ""));
|
|
1136
|
+
for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].trim().replace(/[.,;:)]+$/, ""));
|
|
1091
1137
|
const content = record?.message?.content;
|
|
1092
1138
|
if (!Array.isArray(content)) continue;
|
|
1093
1139
|
for (const block of content) {
|
|
1094
1140
|
const b = block;
|
|
1095
|
-
if (b?.type
|
|
1096
|
-
const name = b.input.skill.replace(/^\//, "");
|
|
1097
|
-
if (!name || dirs.has(name) || [...dirs.keys()].some((key) => key.endsWith(`:${name}`) || name.endsWith(`:${key}`))) continue;
|
|
1098
|
-
const dir = findSkill(name, cwd, home);
|
|
1099
|
-
if (!dir || !add(dir, name)) missing.add(name);
|
|
1141
|
+
if (b?.type === "tool_use" && b.name === "Skill" && typeof b.input?.skill === "string") called.push(b.input.skill.replace(/^\//, ""));
|
|
1100
1142
|
}
|
|
1101
1143
|
}
|
|
1102
1144
|
for (const path of touched) if (basename4(path) === "SKILL.md") add(dirname3(path));
|
|
1103
|
-
for (const name of
|
|
1145
|
+
for (const name of called) {
|
|
1146
|
+
if (!name || dirs.has(name) || [...dirs.keys()].some((key) => key.endsWith(`:${name}`) || name.endsWith(`:${key}`))) continue;
|
|
1147
|
+
const dir = findSkill(name, cwd, home);
|
|
1148
|
+
if (!dir || !add(dir, name)) missing.add(name);
|
|
1149
|
+
}
|
|
1104
1150
|
return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
|
|
1105
1151
|
}
|
|
1106
1152
|
function readJson2(path) {
|
|
@@ -1697,7 +1743,7 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
|
|
|
1697
1743
|
const scratch = mkdtempSync(join6(tmpdir(), "sky-environment-"));
|
|
1698
1744
|
const path = join6(scratch, "environment.tar.gz");
|
|
1699
1745
|
const leftOut = [];
|
|
1700
|
-
const
|
|
1746
|
+
const written2 = /* @__PURE__ */ new Set();
|
|
1701
1747
|
const hash = createHash2("sha256");
|
|
1702
1748
|
let bytes = 0;
|
|
1703
1749
|
let redacted2 = 0;
|
|
@@ -1764,9 +1810,9 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
|
|
|
1764
1810
|
}
|
|
1765
1811
|
await write(tarPadding(stat.size));
|
|
1766
1812
|
}
|
|
1767
|
-
|
|
1813
|
+
written2.add(rel);
|
|
1768
1814
|
}
|
|
1769
|
-
const layout = importLayout(resources, cwd, platform, { files:
|
|
1815
|
+
const layout = importLayout(resources, cwd, platform, { files: written2.size, paths: written2 });
|
|
1770
1816
|
for (const [entry2, data] of layout?.entries ?? []) {
|
|
1771
1817
|
await write(tarHeader(entry2, data.length));
|
|
1772
1818
|
await write(data);
|
|
@@ -1781,7 +1827,7 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
|
|
|
1781
1827
|
folder: layout?.folder ?? "imported-session",
|
|
1782
1828
|
bytes,
|
|
1783
1829
|
sha256: hash.digest("hex"),
|
|
1784
|
-
files:
|
|
1830
|
+
files: written2.size + (layout?.entries.size ?? 0),
|
|
1785
1831
|
redacted: redacted2,
|
|
1786
1832
|
leftOut,
|
|
1787
1833
|
dispose: () => rmSync2(scratch, { recursive: true, force: true })
|
|
@@ -2294,507 +2340,92 @@ import { createHash as createHash4, randomBytes } from "crypto";
|
|
|
2294
2340
|
import { createServer } from "http";
|
|
2295
2341
|
import { hostname } from "os";
|
|
2296
2342
|
import { spawn as spawn2 } from "child_process";
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2343
|
+
|
|
2344
|
+
// src/mcp-register.ts
|
|
2345
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
2346
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2347
|
+
import { homedir as homedir6 } from "os";
|
|
2348
|
+
import { join as join10 } from "path";
|
|
2349
|
+
import { parse as parseToml3 } from "smol-toml";
|
|
2350
|
+
import { isMap, parseDocument } from "yaml";
|
|
2351
|
+
|
|
2352
|
+
// src/local-agents.ts
|
|
2353
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2354
|
+
import { homedir as homedir5 } from "os";
|
|
2355
|
+
import { join as join8 } from "path";
|
|
2356
|
+
function localAgents(env = process.env, home = homedir5()) {
|
|
2357
|
+
const claude = env.CLAUDE_CONFIG_DIR || join8(home, ".claude");
|
|
2358
|
+
const codex = env.CODEX_HOME || join8(home, ".codex");
|
|
2359
|
+
const cursor = join8(home, ".cursor");
|
|
2360
|
+
return [
|
|
2361
|
+
{ id: "claude_code", name: "Claude Code", root: claude, skillsDir: join8(claude, "skills") },
|
|
2362
|
+
{ id: "codex", name: "Codex", root: codex, skillsDir: join8(codex, "skills") },
|
|
2363
|
+
{ id: "cursor", name: "Cursor", root: cursor, skillsDir: join8(cursor, "skills") }
|
|
2364
|
+
];
|
|
2306
2365
|
}
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
const challenge = b64url(createHash4("sha256").update(verifier).digest());
|
|
2310
|
-
const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
|
|
2311
|
-
saveConfig({
|
|
2312
|
-
...loadConfig(),
|
|
2313
|
-
apiBase: apiBase(),
|
|
2314
|
-
token: result.token,
|
|
2315
|
-
email: result.user.email ?? void 0,
|
|
2316
|
-
currentUniverse: void 0
|
|
2317
|
-
// personal by default
|
|
2318
|
-
});
|
|
2319
|
-
console.log(
|
|
2320
|
-
`Logged in as ${result.user.email ?? "you"}${result.universe ? ` (universe: ${result.universe.slug})` : ""}`
|
|
2321
|
-
);
|
|
2366
|
+
function detectLocalAgents(env = process.env, home = homedir5()) {
|
|
2367
|
+
return localAgents(env, home).filter((agent2) => existsSync6(agent2.root));
|
|
2322
2368
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
const
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
return;
|
|
2331
|
-
}
|
|
2332
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
2333
|
-
res.end("<p>Logged in \u2014 you can close this tab and return to your terminal.</p>");
|
|
2334
|
-
const got = url.searchParams.get("code");
|
|
2335
|
-
if (url.searchParams.get("state") !== state || !got) {
|
|
2336
|
-
reject(new Error("login callback state mismatch"));
|
|
2337
|
-
} else {
|
|
2338
|
-
resolve10(got);
|
|
2339
|
-
}
|
|
2340
|
-
server.close();
|
|
2341
|
-
});
|
|
2342
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2343
|
-
const address = server.address();
|
|
2344
|
-
const port = typeof address === "object" && address ? address.port : 0;
|
|
2345
|
-
const authorizeUrl = `${consoleBase()}/cli/authorize?state=${state}&port=${port}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
|
|
2346
|
-
console.log("Opening your browser to authorize this machine\u2026");
|
|
2347
|
-
console.log(`If it doesn't open, visit:
|
|
2348
|
-
${authorizeUrl}`);
|
|
2349
|
-
openBrowser2(authorizeUrl);
|
|
2350
|
-
});
|
|
2351
|
-
setTimeout(() => {
|
|
2352
|
-
server.close();
|
|
2353
|
-
reject(new Error("login timed out after 10 minutes"));
|
|
2354
|
-
}, 10 * 60 * 1e3).unref();
|
|
2369
|
+
function selectLocalAgents(ids, env = process.env, home = homedir5()) {
|
|
2370
|
+
if (!ids?.length) return detectLocalAgents(env, home);
|
|
2371
|
+
const known = localAgents(env, home);
|
|
2372
|
+
return ids.map((id) => {
|
|
2373
|
+
const agent2 = known.find((candidate) => candidate.id === id);
|
|
2374
|
+
if (!agent2) throw new Error(`unknown agent ${id} \u2014 expected ${known.map((candidate) => candidate.id).join(" | ")}`);
|
|
2375
|
+
return agent2;
|
|
2355
2376
|
});
|
|
2356
|
-
return api(
|
|
2357
|
-
"POST",
|
|
2358
|
-
"/cli/token-exchange",
|
|
2359
|
-
{ code, verifier, clientName: `cli ${hostname()}` },
|
|
2360
|
-
{ auth: false }
|
|
2361
|
-
);
|
|
2362
|
-
}
|
|
2363
|
-
async function headlessLogin(verifier, challenge) {
|
|
2364
|
-
const request = b64url(randomBytes(24));
|
|
2365
|
-
const authorizeUrl = `${consoleBase()}/cli/authorize?request=${request}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
|
|
2366
|
-
console.log(`Visit this URL on any device to authorize:
|
|
2367
|
-
${authorizeUrl}
|
|
2368
|
-
`);
|
|
2369
|
-
process.stdout.write("Waiting for approval\u2026");
|
|
2370
|
-
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
2371
|
-
while (Date.now() < deadline) {
|
|
2372
|
-
await new Promise((r) => setTimeout(r, 3e3));
|
|
2373
|
-
const res = await fetch(
|
|
2374
|
-
`${apiBase()}/cli/poll?request=${request}&verifier=${encodeURIComponent(verifier)}&clientName=${encodeURIComponent(`cli ${hostname()}`)}`,
|
|
2375
|
-
{ headers: { "user-agent": USER_AGENT } }
|
|
2376
|
-
);
|
|
2377
|
-
if (!res.ok) {
|
|
2378
|
-
const body2 = await res.json().catch(() => null);
|
|
2379
|
-
throw new Error(body2?.error?.message ?? `poll failed (${res.status})`);
|
|
2380
|
-
}
|
|
2381
|
-
const body = await res.json();
|
|
2382
|
-
if (body.status === "approved") {
|
|
2383
|
-
console.log(" approved.");
|
|
2384
|
-
return body;
|
|
2385
|
-
}
|
|
2386
|
-
process.stdout.write(".");
|
|
2387
|
-
}
|
|
2388
|
-
throw new Error("login timed out after 10 minutes");
|
|
2389
2377
|
}
|
|
2390
2378
|
|
|
2391
|
-
// src/
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
const page = await api("GET", `/connectors/apps${qs ? `?${qs}` : ""}`);
|
|
2401
|
-
apps.push(...page.apps);
|
|
2402
|
-
cursor = page.next_cursor;
|
|
2403
|
-
} while (cursor && apps.length < limit);
|
|
2404
|
-
return apps.slice(0, limit);
|
|
2405
|
-
}
|
|
2406
|
-
function connectionLine2(c) {
|
|
2407
|
-
return `${c.id} ${c.app_name} (${c.app}) ${c.label} since ${c.created_at.slice(0, 10)}`;
|
|
2379
|
+
// src/skill.ts
|
|
2380
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
2381
|
+
import { join as join9 } from "path";
|
|
2382
|
+
|
|
2383
|
+
// src/catalog-file.ts
|
|
2384
|
+
var CATALOG_FILE = "catalog.md";
|
|
2385
|
+
var CATEGORY_ORDER = ["websites", "content", "video", "code", "research", "general"];
|
|
2386
|
+
function categoryOf(agent2) {
|
|
2387
|
+
return CATEGORY_ORDER.find((category) => agent2.categories.includes(category)) ?? agent2.categories[0] ?? "general";
|
|
2408
2388
|
}
|
|
2409
|
-
function
|
|
2410
|
-
const
|
|
2411
|
-
return
|
|
2389
|
+
function describe(value) {
|
|
2390
|
+
const parts = Object.entries(value).filter(([, item]) => item !== null && item !== void 0 && item !== false && item !== "").map(([key, item]) => item === true ? key : `${key}: ${typeof item === "string" ? item : JSON.stringify(item)}`);
|
|
2391
|
+
return parts.length ? parts.join(", ") : null;
|
|
2412
2392
|
}
|
|
2413
|
-
function
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
if (ok) resolve10(url.searchParams.get("account_id") ?? void 0);
|
|
2426
|
-
else reject(new ApiError(400, "connect_failed", "the app was not connected"));
|
|
2427
|
-
});
|
|
2428
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2429
|
-
const address = server.address();
|
|
2430
|
-
const port = typeof address === "object" && address ? address.port : 0;
|
|
2431
|
-
const base = `http://127.0.0.1:${port}`;
|
|
2432
|
-
start(`${base}/done`, `${base}/failed`).then(
|
|
2433
|
-
(authorizeUrl) => {
|
|
2434
|
-
console.log(`Authorize access in your browser:
|
|
2435
|
-
${authorizeUrl}`);
|
|
2436
|
-
if (opts.open !== false) openBrowser2(authorizeUrl);
|
|
2437
|
-
},
|
|
2438
|
-
(err) => {
|
|
2439
|
-
server.close();
|
|
2440
|
-
reject(err);
|
|
2441
|
-
}
|
|
2442
|
-
);
|
|
2443
|
-
});
|
|
2444
|
-
setTimeout(() => {
|
|
2445
|
-
server.close();
|
|
2446
|
-
reject(new ApiError(408, "connect_timeout", "nobody finished the authorization within 10 minutes"));
|
|
2447
|
-
}, opts.timeoutMs ?? 10 * 6e4).unref();
|
|
2448
|
-
});
|
|
2393
|
+
function entry(agent2) {
|
|
2394
|
+
const engine = agent2.engine_type ? ` \xB7 engine ${agent2.engine_type}` : "";
|
|
2395
|
+
const lines = [`### @${agent2.handle} \u2014 ${agent2.name}${engine}`, "", agent2.tagline];
|
|
2396
|
+
if (agent2.does.length) lines.push("", `Does: ${agent2.does.join("; ")}`);
|
|
2397
|
+
if (agent2.does_not.length) lines.push(`Does not: ${agent2.does_not.join("; ")}`);
|
|
2398
|
+
const { mcp_servers: mcpServers2, ...rest } = agent2.requirements;
|
|
2399
|
+
if (typeof mcpServers2 === "boolean") lines.push(mcpServers2 ? "Can use MCP servers." : "Cannot use MCP servers.");
|
|
2400
|
+
const details = describe(rest);
|
|
2401
|
+
if (details) lines.push(`Details: ${details}`);
|
|
2402
|
+
const pricing = describe(agent2.pricing);
|
|
2403
|
+
if (pricing) lines.push(`Pricing: ${pricing}`);
|
|
2404
|
+
return lines.join("\n");
|
|
2449
2405
|
}
|
|
2450
|
-
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2406
|
+
function renderCatalog(agents, generatedAt) {
|
|
2407
|
+
const header2 = [
|
|
2408
|
+
"# AgentSky agent catalog",
|
|
2409
|
+
"",
|
|
2410
|
+
`Every agent you can hire, as of ${generatedAt.toISOString()}. Start one with \`start_session\` and its handle, written with the @.`,
|
|
2411
|
+
"Run `npx @agentsky/sky setup` again to refresh this file; `search_catalog` always has the current list."
|
|
2412
|
+
];
|
|
2413
|
+
if (!agents.length) return [...header2, "", "The catalog was empty when this file was written. Use `search_catalog`.", ""].join("\n");
|
|
2414
|
+
const own = agents.filter((agent2) => agent2.visibility === "workspace");
|
|
2415
|
+
const shared = agents.filter((agent2) => agent2.visibility !== "workspace");
|
|
2416
|
+
const sections = [];
|
|
2417
|
+
if (own.length) sections.push(["## Your workspace's agents", ...own.map(entry)].join("\n\n"));
|
|
2418
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2419
|
+
for (const agent2 of shared) groups.set(categoryOf(agent2), [...groups.get(categoryOf(agent2)) ?? [], agent2]);
|
|
2420
|
+
const ordered = [...groups.keys()].sort((a, b) => {
|
|
2421
|
+
const rank = (category) => CATEGORY_ORDER.includes(category) ? CATEGORY_ORDER.indexOf(category) : CATEGORY_ORDER.length;
|
|
2422
|
+
return rank(a) - rank(b) || a.localeCompare(b);
|
|
2459
2423
|
});
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
function inboundLine(u) {
|
|
2466
|
-
if (!u) return "no inbound URL \u2014 make one with --create";
|
|
2467
|
-
return u.url ?? `inbound URL ${u.prefix}\u2026 made ${u.created_at.slice(0, 10)}${u.last_used_at ? `, last used ${u.last_used_at.slice(0, 16).replace("T", " ")}` : ", never used"}`;
|
|
2468
|
-
}
|
|
2469
|
-
function precheckLine(p) {
|
|
2470
|
-
if (!p) return "no precheck: every scheduled fire runs";
|
|
2471
|
-
const last = p.last_result ? `last check ${p.last_result}${p.last_error ? ` (${p.last_error.type}: ${p.last_error.message})` : ""}` : "not checked yet";
|
|
2472
|
-
return `fires only when ${p.url} changes \xB7 ${last}`;
|
|
2473
|
-
}
|
|
2474
|
-
function triggerLine(t, available = true) {
|
|
2475
|
-
if (!t) return "no chat trigger";
|
|
2476
|
-
const where = t.thread_id ? `thread ${t.thread_id} on ${t.connection_id}` : `any conversation on ${t.connection_id}`;
|
|
2477
|
-
const what = t.text_prefix ? `messages starting "${t.text_prefix}"` : "every message";
|
|
2478
|
-
return `${what} in ${where} start a run${available ? "" : " (chat messages are not offered to routines in this deployment)"}`;
|
|
2479
|
-
}
|
|
2480
|
-
function deliverLine(d) {
|
|
2481
|
-
return d ? `answers post to thread ${d.thread_id} on ${d.connection_id}` : "answers are posted nowhere";
|
|
2482
|
-
}
|
|
2483
|
-
function triggerBody(opts) {
|
|
2484
|
-
if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
|
|
2485
|
-
return { connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {}, ...opts.prefix ? { text_prefix: opts.prefix } : {} };
|
|
2486
|
-
}
|
|
2487
|
-
function deliverBody(opts) {
|
|
2488
|
-
if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
|
|
2489
|
-
return { type: "channel_thread", connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {} };
|
|
2490
|
-
}
|
|
2491
|
-
async function triggerRequest(id, resource, change) {
|
|
2492
|
-
const path = triggerPath(id, resource);
|
|
2493
|
-
if (change.remove) return api("DELETE", path);
|
|
2494
|
-
if (change.create) return api("POST", path);
|
|
2495
|
-
if (change.body) return api("PUT", path, change.body);
|
|
2496
|
-
return api("GET", path);
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
// src/session-workspace.ts
|
|
2500
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
2501
|
-
import { createHash as createHash5, randomUUID } from "crypto";
|
|
2502
|
-
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
2503
|
-
import { basename as basename6, join as join8, relative as relative3, resolve as resolve5 } from "path";
|
|
2504
|
-
var SINGLE_IMPORT_MAX = 25 * 1024 * 1024;
|
|
2505
|
-
var PART_SIZE = 8 * 1024 * 1024;
|
|
2506
|
-
var SKIPPED2 = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2507
|
-
var workspacePath = (id) => `/sessions/${encodeURIComponent(id)}/workspace`;
|
|
2508
|
-
async function listFiles2(id, path, hidden) {
|
|
2509
|
-
const query = new URLSearchParams();
|
|
2510
|
-
if (path) query.set("path", path);
|
|
2511
|
-
if (hidden) query.set("hidden", "true");
|
|
2512
|
-
const qs = query.toString();
|
|
2513
|
-
return api("GET", `${workspacePath(id)}/files${qs ? `?${qs}` : ""}`);
|
|
2514
|
-
}
|
|
2515
|
-
function entryLine(e) {
|
|
2516
|
-
const size = e.type === "directory" ? "-" : String(e.size ?? 0);
|
|
2517
|
-
return `${e.type === "directory" ? "d" : "-"} ${size.padStart(10)} ${(e.mtime ?? "").slice(0, 16).replace("T", " ")} ${e.name}${e.type === "directory" ? "/" : ""}`;
|
|
2518
|
-
}
|
|
2519
|
-
async function readFile(id, path) {
|
|
2520
|
-
return (await apiBytes("GET", `${workspacePath(id)}/files/content?path=${encodeURIComponent(path)}`)).bytes;
|
|
2521
|
-
}
|
|
2522
|
-
function renderChanges(changes) {
|
|
2523
|
-
if (!changes.repos.length) return [`no git repository under ${changes.root}`];
|
|
2524
|
-
return changes.repos.flatMap((r) => [
|
|
2525
|
-
`${r.path === "." ? changes.root : r.path}: ${r.status ? "changed" : "clean"}`,
|
|
2526
|
-
...(r.stat ?? r.status ?? "").split("\n").filter(Boolean).map((line) => ` ${line}`)
|
|
2527
|
-
]);
|
|
2528
|
-
}
|
|
2529
|
-
async function workspacePatch(id, repo) {
|
|
2530
|
-
const { patch, ...meta } = await api("POST", `${workspacePath(id)}/patch`, { repo });
|
|
2531
|
-
return { meta, patch: Buffer.from(patch, "base64") };
|
|
2532
|
-
}
|
|
2533
|
-
function folderFiles(dir) {
|
|
2534
|
-
const git2 = spawnSync3("git", ["-C", dir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8" });
|
|
2535
|
-
const top = spawnSync3("git", ["-C", dir, "rev-parse", "--show-prefix"], { encoding: "utf8" });
|
|
2536
|
-
if (git2.status === 0 && top.status === 0) {
|
|
2537
|
-
return git2.stdout.split("\0").filter((path) => path && statSync6(join8(dir, path), { throwIfNoEntry: false })?.isFile());
|
|
2538
|
-
}
|
|
2539
|
-
const files2 = [];
|
|
2540
|
-
const walk = (at) => {
|
|
2541
|
-
for (const entry2 of readdirSync5(at, { withFileTypes: true })) {
|
|
2542
|
-
if (SKIPPED2.has(entry2.name)) continue;
|
|
2543
|
-
const full = join8(at, entry2.name);
|
|
2544
|
-
if (entry2.isDirectory()) walk(full);
|
|
2545
|
-
else if (entry2.isFile()) files2.push(relative3(dir, full));
|
|
2546
|
-
}
|
|
2547
|
-
};
|
|
2548
|
-
walk(dir);
|
|
2549
|
-
return files2;
|
|
2550
|
-
}
|
|
2551
|
-
function folderArchive(dir, files2) {
|
|
2552
|
-
const tar = spawnSync3("tar", ["--format=ustar", "-czf", "-", "-C", dir, "-T", "-"], {
|
|
2553
|
-
input: files2.join("\n"),
|
|
2554
|
-
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
2555
|
-
maxBuffer: 2 * 1024 * 1024 * 1024
|
|
2556
|
-
});
|
|
2557
|
-
if (tar.status !== 0) throw new ApiError(400, "archive_failed", `tar failed: ${tar.stderr.toString().trim()}`);
|
|
2558
|
-
return tar.stdout;
|
|
2559
|
-
}
|
|
2560
|
-
async function importFolder(id, dir, opts = {}) {
|
|
2561
|
-
const root = resolve5(dir);
|
|
2562
|
-
const name = opts.name ?? basename6(root);
|
|
2563
|
-
const archive = opts.archive ?? folderArchive(root, folderFiles(root));
|
|
2564
|
-
if (archive.length <= SINGLE_IMPORT_MAX) {
|
|
2565
|
-
const res = await apiBytes("POST", `${workspacePath(id)}/import?name=${encodeURIComponent(name)}`, { bytes: archive, mediaType: "application/gzip" });
|
|
2566
|
-
return res.json();
|
|
2567
|
-
}
|
|
2568
|
-
const sha256 = createHash5("sha256").update(archive).digest("hex");
|
|
2569
|
-
let upload = await api("POST", `${workspacePath(id)}/imports`, { name, size: archive.length, sha256, part_size: PART_SIZE });
|
|
2570
|
-
const uploadPath = `${workspacePath(id)}/imports/${encodeURIComponent(upload.id)}`;
|
|
2571
|
-
for (let part = 0; part < upload.parts; part += 1) {
|
|
2572
|
-
if (upload.received.includes(part)) continue;
|
|
2573
|
-
const bytes = archive.subarray(part * upload.part_size, (part + 1) * upload.part_size);
|
|
2574
|
-
await apiBytes("PUT", `${uploadPath}/parts/${part}`, { bytes, mediaType: "application/octet-stream" });
|
|
2575
|
-
}
|
|
2576
|
-
upload = await api("POST", `${uploadPath}/complete`);
|
|
2577
|
-
while (upload.state !== "imported" && upload.state !== "failed") {
|
|
2578
|
-
await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
|
|
2579
|
-
upload = await api("GET", uploadPath);
|
|
2580
|
-
}
|
|
2581
|
-
if (upload.state === "failed") throw new ApiError(422, upload.error?.code ?? "import_failed", upload.error?.message ?? "the import failed");
|
|
2582
|
-
return { path: upload.path ?? name, files: upload.files ?? 0 };
|
|
2583
|
-
}
|
|
2584
|
-
function renderAccess(a) {
|
|
2585
|
-
const lines = [
|
|
2586
|
-
`${a.session_id}${a.title ? ` \u2014 ${a.title}` : ""}`,
|
|
2587
|
-
` ${a.visibility === "private" ? "private to its owner" : `visible to ${a.workspace.name} (${a.workspace.slug})`} \xB7 you are its ${a.access}`,
|
|
2588
|
-
` owner: ${a.owner.name ?? a.owner.email ?? "unknown"}`
|
|
2589
|
-
];
|
|
2590
|
-
if (a.viewers) lines.push(a.viewers.length ? ` viewers: ${a.viewers.map((v) => v.email).join(", ")}` : " viewers: none");
|
|
2591
|
-
return lines;
|
|
2592
|
-
}
|
|
2593
|
-
function accessPatch(opts) {
|
|
2594
|
-
if (opts.private && opts.members) throw new ApiError(400, "invalid_request", "pass --private or --members, not both");
|
|
2595
|
-
return {
|
|
2596
|
-
...opts.private ? { visibility: "private" } : opts.members ? { visibility: "workspace" } : {},
|
|
2597
|
-
...opts.addViewer?.length ? { add_viewers: opts.addViewer } : {},
|
|
2598
|
-
...opts.removeViewer?.length ? { remove_viewers: opts.removeViewer } : {}
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
|
-
function engineChange(model, thinking) {
|
|
2602
|
-
if (!model && !thinking) return null;
|
|
2603
|
-
return {
|
|
2604
|
-
...model ? { llm: model } : {},
|
|
2605
|
-
...thinking ? { reasoningEffort: thinking === "default" ? null : thinking } : {}
|
|
2606
|
-
};
|
|
2607
|
-
}
|
|
2608
|
-
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
2609
|
-
async function handoff(id, targetAgent, opts = {}) {
|
|
2610
|
-
let { handoff: h } = await api("POST", `/sessions/${encodeURIComponent(id)}/handoff`, {
|
|
2611
|
-
target_agent: targetAgent,
|
|
2612
|
-
...opts.fromMessage ? { last_message_id: opts.fromMessage } : {}
|
|
2613
|
-
}, { idempotencyKey: opts.idempotencyKey ?? randomUUID() });
|
|
2614
|
-
opts.onStatus?.(h);
|
|
2615
|
-
while (opts.wait && !TERMINAL.has(h.status)) {
|
|
2616
|
-
await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
|
|
2617
|
-
const next = (await api("GET", `/session-handoffs/${encodeURIComponent(h.id)}`)).handoff;
|
|
2618
|
-
if (next.status !== h.status) opts.onStatus?.(next);
|
|
2619
|
-
h = next;
|
|
2620
|
-
}
|
|
2621
|
-
return h;
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
|
-
// src/local-agents.ts
|
|
2625
|
-
import { existsSync as existsSync6 } from "fs";
|
|
2626
|
-
import { homedir as homedir5 } from "os";
|
|
2627
|
-
import { join as join9 } from "path";
|
|
2628
|
-
function localAgents(env = process.env, home = homedir5()) {
|
|
2629
|
-
const claude = env.CLAUDE_CONFIG_DIR || join9(home, ".claude");
|
|
2630
|
-
const codex = env.CODEX_HOME || join9(home, ".codex");
|
|
2631
|
-
const cursor = join9(home, ".cursor");
|
|
2632
|
-
return [
|
|
2633
|
-
{ id: "claude_code", name: "Claude Code", root: claude, skillsDir: join9(claude, "skills") },
|
|
2634
|
-
{ id: "codex", name: "Codex", root: codex, skillsDir: join9(codex, "skills") },
|
|
2635
|
-
{ id: "cursor", name: "Cursor", root: cursor, skillsDir: join9(cursor, "skills") }
|
|
2636
|
-
];
|
|
2637
|
-
}
|
|
2638
|
-
function detectLocalAgents(env = process.env, home = homedir5()) {
|
|
2639
|
-
return localAgents(env, home).filter((agent2) => existsSync6(agent2.root));
|
|
2640
|
-
}
|
|
2641
|
-
function selectLocalAgents(ids, env = process.env, home = homedir5()) {
|
|
2642
|
-
if (!ids?.length) return detectLocalAgents(env, home);
|
|
2643
|
-
const known = localAgents(env, home);
|
|
2644
|
-
return ids.map((id) => {
|
|
2645
|
-
const agent2 = known.find((candidate) => candidate.id === id);
|
|
2646
|
-
if (!agent2) throw new Error(`unknown agent ${id} \u2014 expected ${known.map((candidate) => candidate.id).join(" | ")}`);
|
|
2647
|
-
return agent2;
|
|
2648
|
-
});
|
|
2649
|
-
}
|
|
2650
|
-
|
|
2651
|
-
// src/render.ts
|
|
2652
|
-
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
2653
|
-
var red = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
2654
|
-
function clip(value, max = 120) {
|
|
2655
|
-
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
2656
|
-
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
2657
|
-
}
|
|
2658
|
-
function renderEvent(event, data) {
|
|
2659
|
-
const part = data.part;
|
|
2660
|
-
switch (event) {
|
|
2661
|
-
case "agent.message":
|
|
2662
|
-
console.log(String(data.text ?? ""));
|
|
2663
|
-
break;
|
|
2664
|
-
case "agent.reasoning":
|
|
2665
|
-
console.log(dim(`\xB7 ${clip(part?.text ?? "")}`));
|
|
2666
|
-
break;
|
|
2667
|
-
case "agent.tool_use":
|
|
2668
|
-
console.log(dim(`\u2192 ${String(part?.tool_name ?? "tool")}(${clip(part?.args ?? {}, 80)})`));
|
|
2669
|
-
break;
|
|
2670
|
-
case "agent.tool_result":
|
|
2671
|
-
console.log(dim(`\u2190 ${String(part?.tool_name ?? "tool")} ${String(part?.status ?? "")}`));
|
|
2672
|
-
break;
|
|
2673
|
-
case "agent.status":
|
|
2674
|
-
console.log(dim(`\u2026 ${String(part?.level ?? "")} ${String(part?.text ?? "")}`.trim()));
|
|
2675
|
-
break;
|
|
2676
|
-
case "user.interrupt":
|
|
2677
|
-
console.log(dim("\u23F9 interrupting"));
|
|
2678
|
-
break;
|
|
2679
|
-
case "session.error": {
|
|
2680
|
-
const error = data.error;
|
|
2681
|
-
const message2 = String(error?.message ?? error?.type ?? "unknown");
|
|
2682
|
-
if (error?.retry_status === "retrying") console.error(dim(`retrying: ${message2}`));
|
|
2683
|
-
else console.error(red(`error: ${message2}`));
|
|
2684
|
-
break;
|
|
2685
|
-
}
|
|
2686
|
-
case "session.status_terminated":
|
|
2687
|
-
console.log(dim("session terminated"));
|
|
2688
|
-
break;
|
|
2689
|
-
default:
|
|
2690
|
-
break;
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
// src/session-read.ts
|
|
2695
|
-
var TURN_TYPES = ["user.message", "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
|
|
2696
|
-
function latestTurn(events) {
|
|
2697
|
-
let start = 0;
|
|
2698
|
-
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
2699
|
-
if (events[index].type === "user.message") {
|
|
2700
|
-
start = index + 1;
|
|
2701
|
-
break;
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
const reading = { finished: false, messages: [], errors: [] };
|
|
2705
|
-
for (const event of events.slice(start)) {
|
|
2706
|
-
if (event.type === "agent.message" && event.text?.trim()) reading.messages.push(event.text);
|
|
2707
|
-
else if (event.type === "session.error" && event.error?.retry_status !== "retrying") {
|
|
2708
|
-
reading.errors.push(event.error?.message ?? event.error?.type ?? "unknown error");
|
|
2709
|
-
} else if (event.type === "session.status_terminated") reading.finished = true;
|
|
2710
|
-
else if (event.type === "session.status_idle" && event.stop_reason?.type !== "requires_action") reading.finished = true;
|
|
2711
|
-
}
|
|
2712
|
-
return reading;
|
|
2713
|
-
}
|
|
2714
|
-
async function history2(sessionId) {
|
|
2715
|
-
const events = [];
|
|
2716
|
-
let cursor = null;
|
|
2717
|
-
for (; ; ) {
|
|
2718
|
-
const query = new URLSearchParams({ limit: "500" });
|
|
2719
|
-
for (const type of TURN_TYPES) query.append("types[]", type);
|
|
2720
|
-
if (cursor) query.set("cursor", cursor);
|
|
2721
|
-
const page = await api(
|
|
2722
|
-
"GET",
|
|
2723
|
-
`/sessions/${encodeURIComponent(sessionId)}/events?${query}`
|
|
2724
|
-
);
|
|
2725
|
-
events.push(...page.events);
|
|
2726
|
-
if (!page.hasMore || !page.cursor) return events;
|
|
2727
|
-
cursor = page.cursor;
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
async function readTurn(sessionId, opts = {}) {
|
|
2731
|
-
const deadline = Date.now() + (opts.timeoutSeconds ?? 600) * 1e3;
|
|
2732
|
-
for (; ; ) {
|
|
2733
|
-
const reading = latestTurn(await history2(sessionId));
|
|
2734
|
-
if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
|
|
2735
|
-
await new Promise((resolve10) => setTimeout(resolve10, opts.intervalMs ?? 3e3));
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
// src/setup.ts
|
|
2740
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
|
|
2741
|
-
import { dirname as dirname6, join as join13 } from "path";
|
|
2742
|
-
import { createInterface as createInterface3 } from "readline";
|
|
2743
|
-
|
|
2744
|
-
// src/mcp-register.ts
|
|
2745
|
-
import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
2746
|
-
import { spawnSync as spawnSync4 } from "child_process";
|
|
2747
|
-
|
|
2748
|
-
// src/skill.ts
|
|
2749
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
2750
|
-
import { join as join10 } from "path";
|
|
2751
|
-
|
|
2752
|
-
// src/catalog-file.ts
|
|
2753
|
-
var CATALOG_FILE = "catalog.md";
|
|
2754
|
-
var CATEGORY_ORDER = ["websites", "content", "video", "code", "research", "general"];
|
|
2755
|
-
function categoryOf(agent2) {
|
|
2756
|
-
return CATEGORY_ORDER.find((category) => agent2.categories.includes(category)) ?? agent2.categories[0] ?? "general";
|
|
2757
|
-
}
|
|
2758
|
-
function describe(value) {
|
|
2759
|
-
const parts = Object.entries(value).filter(([, item]) => item !== null && item !== void 0 && item !== false && item !== "").map(([key, item]) => item === true ? key : `${key}: ${typeof item === "string" ? item : JSON.stringify(item)}`);
|
|
2760
|
-
return parts.length ? parts.join(", ") : null;
|
|
2761
|
-
}
|
|
2762
|
-
function entry(agent2) {
|
|
2763
|
-
const engine = agent2.engine_type ? ` \xB7 engine ${agent2.engine_type}` : "";
|
|
2764
|
-
const lines = [`### @${agent2.handle} \u2014 ${agent2.name}${engine}`, "", agent2.tagline];
|
|
2765
|
-
if (agent2.does.length) lines.push("", `Does: ${agent2.does.join("; ")}`);
|
|
2766
|
-
if (agent2.does_not.length) lines.push(`Does not: ${agent2.does_not.join("; ")}`);
|
|
2767
|
-
const { mcp_servers: mcpServers2, ...rest } = agent2.requirements;
|
|
2768
|
-
if (typeof mcpServers2 === "boolean") lines.push(mcpServers2 ? "Can use MCP servers." : "Cannot use MCP servers.");
|
|
2769
|
-
const details = describe(rest);
|
|
2770
|
-
if (details) lines.push(`Details: ${details}`);
|
|
2771
|
-
const pricing = describe(agent2.pricing);
|
|
2772
|
-
if (pricing) lines.push(`Pricing: ${pricing}`);
|
|
2773
|
-
return lines.join("\n");
|
|
2774
|
-
}
|
|
2775
|
-
function renderCatalog(agents, generatedAt) {
|
|
2776
|
-
const header2 = [
|
|
2777
|
-
"# AgentSky agent catalog",
|
|
2778
|
-
"",
|
|
2779
|
-
`Every agent you can hire, as of ${generatedAt.toISOString()}. Start one with \`start_session\` and its handle, written with the @.`,
|
|
2780
|
-
"Run `npx @agentsky/sky setup` again to refresh this file; `search_catalog` always has the current list."
|
|
2781
|
-
];
|
|
2782
|
-
if (!agents.length) return [...header2, "", "The catalog was empty when this file was written. Use `search_catalog`.", ""].join("\n");
|
|
2783
|
-
const own = agents.filter((agent2) => agent2.visibility === "workspace");
|
|
2784
|
-
const shared = agents.filter((agent2) => agent2.visibility !== "workspace");
|
|
2785
|
-
const sections = [];
|
|
2786
|
-
if (own.length) sections.push(["## Your workspace's agents", ...own.map(entry)].join("\n\n"));
|
|
2787
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2788
|
-
for (const agent2 of shared) groups.set(categoryOf(agent2), [...groups.get(categoryOf(agent2)) ?? [], agent2]);
|
|
2789
|
-
const ordered = [...groups.keys()].sort((a, b) => {
|
|
2790
|
-
const rank = (category) => CATEGORY_ORDER.includes(category) ? CATEGORY_ORDER.indexOf(category) : CATEGORY_ORDER.length;
|
|
2791
|
-
return rank(a) - rank(b) || a.localeCompare(b);
|
|
2792
|
-
});
|
|
2793
|
-
for (const category of ordered) {
|
|
2794
|
-
const title = category.charAt(0).toUpperCase() + category.slice(1);
|
|
2795
|
-
sections.push([`## ${title}`, ...groups.get(category).map(entry)].join("\n\n"));
|
|
2796
|
-
}
|
|
2797
|
-
return [...header2, "", sections.join("\n\n"), ""].join("\n");
|
|
2424
|
+
for (const category of ordered) {
|
|
2425
|
+
const title = category.charAt(0).toUpperCase() + category.slice(1);
|
|
2426
|
+
sections.push([`## ${title}`, ...groups.get(category).map(entry)].join("\n\n"));
|
|
2427
|
+
}
|
|
2428
|
+
return [...header2, "", sections.join("\n\n"), ""].join("\n");
|
|
2798
2429
|
}
|
|
2799
2430
|
|
|
2800
2431
|
// src/skill.ts
|
|
@@ -3145,185 +2776,765 @@ your application has checked who they are. By hand: \`sky task show <id>\` and
|
|
|
3145
2776
|
\`sky task answer <id> <request> --text "..."\` (MCP: \`get_task\`,
|
|
3146
2777
|
\`answer_request\`).
|
|
3147
2778
|
|
|
3148
|
-
## 7. What the application keeps
|
|
2779
|
+
## 7. What the application keeps
|
|
2780
|
+
|
|
2781
|
+
The agent decides; the application acts. Keep these in the application's own
|
|
2782
|
+
database:
|
|
2783
|
+
|
|
2784
|
+
- **One job per event**, with a unique event key. The same key with the same
|
|
2785
|
+
payload returns the job it made; with a different payload it is a conflict.
|
|
2786
|
+
- **The session id**, saved before you start polling, so a restart picks the
|
|
2787
|
+
job up where it was.
|
|
2788
|
+
- **A confirmation receipt** from the signed-in customer before any side
|
|
2789
|
+
effect.
|
|
2790
|
+
- **A unique action key per job**, so the side effect happens once. Agent
|
|
2791
|
+
retries do not make it happen once; the key does.
|
|
2792
|
+
- **Webhooks to the customer** with an event id, retried until acknowledged.
|
|
2793
|
+
|
|
2794
|
+
## Before launch
|
|
2795
|
+
|
|
2796
|
+
Check one real session:
|
|
2797
|
+
|
|
2798
|
+
- \`GET /v1/sessions/{id}\` lists only the upload under \`resources\`.
|
|
2799
|
+
- \`GET /v1/hook_rules\` and \`GET /v1/universes/{slug}/policies\` show the rules
|
|
2800
|
+
above.
|
|
2801
|
+
- Ask the agent directly to run a shell command. The answer has to be the
|
|
2802
|
+
platform's refusal.
|
|
2803
|
+
- A file id from another customer's workspace is refused at session create.
|
|
2804
|
+
`;
|
|
2805
|
+
}
|
|
2806
|
+
function workspacesReferenceDoc() {
|
|
2807
|
+
return `# Workspaces in AgentSky
|
|
2808
|
+
|
|
2809
|
+
A workspace (also called a universe) groups agents and sessions. Every API call
|
|
2810
|
+
and MCP tool accepts an optional workspace slug.
|
|
2811
|
+
|
|
2812
|
+
## How to choose
|
|
2813
|
+
|
|
2814
|
+
1. The person named one \u2192 use it.
|
|
2815
|
+
2. Check the git root for \`.agentsky.json\` with a \`"workspace"\` key \u2192 use that.
|
|
2816
|
+
3. Neither \u2192 use the account default (personal workspace). Never ask.
|
|
2817
|
+
|
|
2818
|
+
## Applying it
|
|
2819
|
+
|
|
2820
|
+
- MCP tools: pass \`workspace: "<slug>"\` on tools that accept it.
|
|
2821
|
+
- sky CLI: \`--workspace <slug>\` on any command, or set \`SKY_WORKSPACE=<slug>\`.
|
|
2822
|
+
- Tools that take a \`task_id\` or \`session_id\` inherit that task's workspace
|
|
2823
|
+
automatically; one task never spans workspaces.
|
|
2824
|
+
|
|
2825
|
+
## Listing workspaces
|
|
2826
|
+
|
|
2827
|
+
\`sky workspace list\` (or \`sky universe list\`) prints your workspaces, with
|
|
2828
|
+
\`*\` marking the current default.
|
|
2829
|
+
|
|
2830
|
+
## .agentsky.json
|
|
2831
|
+
|
|
2832
|
+
To set a per-repo default, create \`.agentsky.json\` at the git root:
|
|
2833
|
+
|
|
2834
|
+
\`\`\`json
|
|
2835
|
+
{"workspace": "my-team-slug"}
|
|
2836
|
+
\`\`\`
|
|
2837
|
+
`;
|
|
2838
|
+
}
|
|
2839
|
+
function installedVersion(dir) {
|
|
2840
|
+
try {
|
|
2841
|
+
const marker = JSON.parse(readFileSync9(join9(dir, SKILL_MARKER), "utf8"));
|
|
2842
|
+
return typeof marker.version === "string" ? marker.version : null;
|
|
2843
|
+
} catch {
|
|
2844
|
+
return null;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
function isInstalledSkill(dir) {
|
|
2848
|
+
return installedVersion(dir) !== null;
|
|
2849
|
+
}
|
|
2850
|
+
function installSkill(agent2, opts = {}) {
|
|
2851
|
+
const dir = join9(agent2.skillsDir, SKILL_NAME);
|
|
2852
|
+
const previous = installedVersion(dir);
|
|
2853
|
+
const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
|
|
2854
|
+
if (opts.dryRun) return { agent: agent2.id, path: dir, outcome };
|
|
2855
|
+
if (outcome !== "current") {
|
|
2856
|
+
mkdirSync2(dir, { recursive: true });
|
|
2857
|
+
writeFileSync4(join9(dir, "SKILL.md"), skillDocument());
|
|
2858
|
+
writeFileSync4(join9(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
|
|
2859
|
+
`);
|
|
2860
|
+
}
|
|
2861
|
+
if (opts.catalog) {
|
|
2862
|
+
mkdirSync2(dir, { recursive: true });
|
|
2863
|
+
writeFileSync4(join9(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
|
|
2864
|
+
}
|
|
2865
|
+
const refsDir = join9(dir, REFERENCES_DIR);
|
|
2866
|
+
mkdirSync2(refsDir, { recursive: true });
|
|
2867
|
+
writeFileSync4(join9(refsDir, "import.md"), importReferenceDoc());
|
|
2868
|
+
writeFileSync4(join9(refsDir, "workspaces.md"), workspacesReferenceDoc());
|
|
2869
|
+
writeFileSync4(join9(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
|
|
2870
|
+
return { agent: agent2.id, path: dir, outcome };
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/mcp-register.ts
|
|
2874
|
+
var MCP_SERVER_NAME = SKILL_NAME;
|
|
2875
|
+
function detectHermes(env = process.env, home = homedir6()) {
|
|
2876
|
+
const root = env.HERMES_HOME || join10(home, ".hermes");
|
|
2877
|
+
return existsSync7(root) ? { id: "hermes", name: "Hermes", root } : null;
|
|
2878
|
+
}
|
|
2879
|
+
function detectMcpClients(env = process.env, home = homedir6()) {
|
|
2880
|
+
const hermes = detectHermes(env, home);
|
|
2881
|
+
return [...detectLocalAgents(env, home), ...hermes ? [hermes] : []];
|
|
2882
|
+
}
|
|
2883
|
+
function systemCommandRunner() {
|
|
2884
|
+
return {
|
|
2885
|
+
has: (command) => spawnSync3(command, ["--version"], { stdio: "ignore" }).status === 0,
|
|
2886
|
+
run: (command, args) => {
|
|
2887
|
+
const result = spawnSync3(command, args, { encoding: "utf8" });
|
|
2888
|
+
return { status: result.status, stderr: result.stderr ?? "" };
|
|
2889
|
+
}
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
function mcpUrlFor(apiBase2) {
|
|
2893
|
+
return `${apiBase2.replace(/\/$/, "")}/mcp`;
|
|
2894
|
+
}
|
|
2895
|
+
function mcpCommands(agent2, opts) {
|
|
2896
|
+
if (agent2 === "claude_code") {
|
|
2897
|
+
return {
|
|
2898
|
+
command: "claude",
|
|
2899
|
+
remove: ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME],
|
|
2900
|
+
add: ["mcp", "add", "--scope", "user", "--transport", "http", MCP_SERVER_NAME, opts.mcpUrl, "--header", `Authorization: Bearer ${opts.token}`]
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
return null;
|
|
2904
|
+
}
|
|
2905
|
+
function redact(text, token) {
|
|
2906
|
+
return text.split(token).join(`${token.slice(0, 10)}\u2026`);
|
|
2907
|
+
}
|
|
2908
|
+
function registerMcpServer(client, opts, runner = systemCommandRunner()) {
|
|
2909
|
+
if (client.id === "codex") return registerCodex(client, opts);
|
|
2910
|
+
if (client.id === "cursor") return registerCursor(client, opts);
|
|
2911
|
+
if (client.id === "hermes") return registerHermes(client, opts);
|
|
2912
|
+
const commands = mcpCommands(client.id, opts);
|
|
2913
|
+
if (!commands) return { agent: client.id, outcome: "unsupported" };
|
|
2914
|
+
if (!runner.has(commands.command)) return { agent: client.id, outcome: "no_command", detail: `${commands.command} is not on PATH` };
|
|
2915
|
+
if (opts.dryRun) return { agent: client.id, outcome: "planned", detail: redact(`run ${[commands.command, ...commands.add].join(" ")}`, opts.token) };
|
|
2916
|
+
runner.run(commands.command, commands.remove);
|
|
2917
|
+
const added = runner.run(commands.command, commands.add);
|
|
2918
|
+
if (added.status !== 0) return { agent: client.id, outcome: "failed", detail: redact(added.stderr.trim() || `exit ${added.status}`, opts.token) };
|
|
2919
|
+
return { agent: client.id, outcome: "registered" };
|
|
2920
|
+
}
|
|
2921
|
+
function mcpConfigPath(client, env = process.env, home = homedir6()) {
|
|
2922
|
+
switch (client.id) {
|
|
2923
|
+
case "claude_code":
|
|
2924
|
+
return join10(env.CLAUDE_CONFIG_DIR || home, ".claude.json");
|
|
2925
|
+
case "codex":
|
|
2926
|
+
return join10(client.root, "config.toml");
|
|
2927
|
+
case "cursor":
|
|
2928
|
+
return join10(client.root, "mcp.json");
|
|
2929
|
+
case "hermes":
|
|
2930
|
+
return join10(client.root, "config.yaml");
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
function readText(path) {
|
|
2934
|
+
try {
|
|
2935
|
+
return readFileSync10(path, "utf8");
|
|
2936
|
+
} catch {
|
|
2937
|
+
return null;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
function hasMcpServer(client, env = process.env, home = homedir6()) {
|
|
2941
|
+
const raw = readText(mcpConfigPath(client, env, home));
|
|
2942
|
+
if (raw === null) return false;
|
|
2943
|
+
try {
|
|
2944
|
+
if (client.id === "codex") return Boolean(parseToml3(raw).mcp_servers?.[MCP_SERVER_NAME]);
|
|
2945
|
+
if (client.id === "hermes") return parseDocument(raw).hasIn(["mcp_servers", MCP_SERVER_NAME]);
|
|
2946
|
+
return Boolean(JSON.parse(raw).mcpServers?.[MCP_SERVER_NAME]);
|
|
2947
|
+
} catch {
|
|
2948
|
+
return false;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
function planned(client, path, entry2, opts) {
|
|
2952
|
+
return { agent: client.id, outcome: "planned", detail: `write to ${path}: ${redact(entry2, opts.token)}` };
|
|
2953
|
+
}
|
|
2954
|
+
function written(client, path, text) {
|
|
2955
|
+
try {
|
|
2956
|
+
writeFileSync5(path, text, { encoding: "utf8", mode: 384 });
|
|
2957
|
+
} catch (err) {
|
|
2958
|
+
return { agent: client.id, outcome: "failed", detail: String(err) };
|
|
2959
|
+
}
|
|
2960
|
+
return { agent: client.id, outcome: "registered" };
|
|
2961
|
+
}
|
|
2962
|
+
function tomlTableKey(name) {
|
|
2963
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2964
|
+
return `(?:${escaped}|"${escaped}"|'${escaped}')`;
|
|
2965
|
+
}
|
|
2966
|
+
function withoutTomlServer(text, name) {
|
|
2967
|
+
const ours = new RegExp(`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*${tomlTableKey(name)}\\s*(?:\\.[^\\]]*)?\\]\\s*(?:#.*)?$`);
|
|
2968
|
+
let skipping = false;
|
|
2969
|
+
const kept = [];
|
|
2970
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2971
|
+
if (/^\s*\[/.test(line)) skipping = ours.test(line);
|
|
2972
|
+
if (!skipping) kept.push(line);
|
|
2973
|
+
}
|
|
2974
|
+
return kept.join("\n").replace(/\n+$/, "");
|
|
2975
|
+
}
|
|
2976
|
+
function registerCodex(client, opts) {
|
|
2977
|
+
const path = mcpConfigPath(client);
|
|
2978
|
+
const block = [
|
|
2979
|
+
`[mcp_servers.${MCP_SERVER_NAME}]`,
|
|
2980
|
+
`url = ${JSON.stringify(opts.mcpUrl)}`,
|
|
2981
|
+
`http_headers = { Authorization = ${JSON.stringify(`Bearer ${opts.token}`)} }`
|
|
2982
|
+
].join("\n");
|
|
2983
|
+
if (opts.dryRun) return planned(client, path, block, opts);
|
|
2984
|
+
const raw = readText(path) ?? "";
|
|
2985
|
+
let before;
|
|
2986
|
+
try {
|
|
2987
|
+
before = parseToml3(raw);
|
|
2988
|
+
} catch (err) {
|
|
2989
|
+
return { agent: client.id, outcome: "failed", detail: `${path} is not valid TOML, left as it was (${String(err).split("\n")[0]})` };
|
|
2990
|
+
}
|
|
2991
|
+
const rest = withoutTomlServer(raw, MCP_SERVER_NAME);
|
|
2992
|
+
const text = `${rest ? `${rest}
|
|
3149
2993
|
|
|
3150
|
-
|
|
3151
|
-
|
|
2994
|
+
` : ""}${block}
|
|
2995
|
+
`;
|
|
2996
|
+
try {
|
|
2997
|
+
const after = parseToml3(text);
|
|
2998
|
+
const others = Object.keys(before.mcp_servers ?? {}).filter((name) => name !== MCP_SERVER_NAME);
|
|
2999
|
+
if (after.mcp_servers?.[MCP_SERVER_NAME]?.url !== opts.mcpUrl || others.some((name) => !after.mcp_servers?.[name])) throw new Error("the edited file lost an entry");
|
|
3000
|
+
} catch (err) {
|
|
3001
|
+
return { agent: client.id, outcome: "failed", detail: `could not edit ${path} safely, left as it was (${String(err).split("\n")[0]})` };
|
|
3002
|
+
}
|
|
3003
|
+
return written(client, path, text);
|
|
3004
|
+
}
|
|
3005
|
+
function registerCursor(client, opts) {
|
|
3006
|
+
const path = mcpConfigPath(client);
|
|
3007
|
+
const entry2 = { url: opts.mcpUrl, headers: { Authorization: `Bearer ${opts.token}` } };
|
|
3008
|
+
if (opts.dryRun) return planned(client, path, JSON.stringify({ [MCP_SERVER_NAME]: entry2 }, null, 2), opts);
|
|
3009
|
+
let existing = {};
|
|
3010
|
+
try {
|
|
3011
|
+
const parsed = JSON.parse(readText(path) ?? "");
|
|
3012
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
|
|
3013
|
+
} catch {
|
|
3014
|
+
existing = {};
|
|
3015
|
+
}
|
|
3016
|
+
const updated = { ...existing, mcpServers: { ...existing.mcpServers ?? {}, [MCP_SERVER_NAME]: entry2 } };
|
|
3017
|
+
return written(client, path, `${JSON.stringify(updated, null, 2)}
|
|
3018
|
+
`);
|
|
3019
|
+
}
|
|
3020
|
+
function registerHermes(client, opts) {
|
|
3021
|
+
const path = mcpConfigPath(client);
|
|
3022
|
+
const entry2 = { url: opts.mcpUrl, headers: { Authorization: `Bearer ${opts.token}` } };
|
|
3023
|
+
if (opts.dryRun) return planned(client, path, `mcp_servers.${MCP_SERVER_NAME}: ${JSON.stringify(entry2)}`, opts);
|
|
3024
|
+
const doc = parseDocument(readText(path) ?? "");
|
|
3025
|
+
if (doc.errors.length) return { agent: client.id, outcome: "failed", detail: `${path} is not valid YAML, left as it was (${doc.errors[0].message.split("\n")[0]})` };
|
|
3026
|
+
if (doc.contents !== null && !isMap(doc.contents)) {
|
|
3027
|
+
return { agent: client.id, outcome: "failed", detail: `${path} is not a YAML mapping, left as it was` };
|
|
3028
|
+
}
|
|
3029
|
+
doc.setIn(["mcp_servers", MCP_SERVER_NAME], doc.createNode(entry2));
|
|
3030
|
+
return written(client, path, doc.toString());
|
|
3031
|
+
}
|
|
3032
|
+
function refreshMcpRegistrations(opts, clients = detectMcpClients(), runner = systemCommandRunner()) {
|
|
3033
|
+
return clients.filter((client) => hasMcpServer(client)).map((client) => registerMcpServer(client, opts, runner));
|
|
3034
|
+
}
|
|
3152
3035
|
|
|
3153
|
-
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3036
|
+
// src/shell-env.ts
|
|
3037
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
3038
|
+
import { homedir as homedir7 } from "os";
|
|
3039
|
+
import { basename as basename6, dirname as dirname5, join as join11 } from "path";
|
|
3040
|
+
var TOKEN_ENV = "AGENTSKY_API_KEY";
|
|
3041
|
+
var HOOK_MARK = "# AgentSky API token, kept current by sky";
|
|
3042
|
+
function shellEnvFiles() {
|
|
3043
|
+
const dir = dirname5(configPath());
|
|
3044
|
+
return { sh: join11(dir, "env.sh"), fish: join11(dir, "env.fish") };
|
|
3045
|
+
}
|
|
3046
|
+
function quoted(value) {
|
|
3047
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
3048
|
+
}
|
|
3049
|
+
function shellStartupFiles(env = process.env, home = homedir7(), platform = process.platform) {
|
|
3050
|
+
const shell = basename6(env.SHELL ?? "");
|
|
3051
|
+
const files2 = [];
|
|
3052
|
+
const zshenv = join11(env.ZDOTDIR || home, ".zshenv");
|
|
3053
|
+
if (shell === "zsh" || platform === "darwin" || existsSync8(zshenv)) files2.push({ path: zshenv, kind: "sh" });
|
|
3054
|
+
const bashrc = join11(home, ".bashrc");
|
|
3055
|
+
if (shell === "bash" || existsSync8(bashrc)) files2.push({ path: bashrc, kind: "sh" });
|
|
3056
|
+
const bashProfile = join11(home, ".bash_profile");
|
|
3057
|
+
if (existsSync8(bashProfile)) files2.push({ path: bashProfile, kind: "sh" });
|
|
3058
|
+
const fish = join11(env.XDG_CONFIG_HOME || join11(home, ".config"), "fish");
|
|
3059
|
+
if (shell === "fish" || existsSync8(fish)) files2.push({ path: join11(fish, "conf.d", "agentsky.fish"), kind: "fish" });
|
|
3060
|
+
return files2;
|
|
3061
|
+
}
|
|
3062
|
+
function writeShellEnv(token, opts = {}) {
|
|
3063
|
+
const { sh, fish } = shellEnvFiles();
|
|
3064
|
+
const startup = shellStartupFiles(opts.env, opts.home, opts.platform);
|
|
3065
|
+
if (opts.dryRun) return { outcome: "planned", files: [sh, fish, ...startup.map((file) => file.path)] };
|
|
3066
|
+
const header2 = "# Written by sky. `sky auth login` and `sky auth logout` rewrite this file.\n";
|
|
3067
|
+
const hooked = [];
|
|
3068
|
+
try {
|
|
3069
|
+
mkdirSync3(dirname5(sh), { recursive: true });
|
|
3070
|
+
writeFileSync6(sh, `${header2}${token ? `export ${TOKEN_ENV}=${quoted(token)}` : `unset ${TOKEN_ENV}`}
|
|
3071
|
+
`, { mode: 384 });
|
|
3072
|
+
writeFileSync6(fish, `${header2}${token ? `set -gx ${TOKEN_ENV} ${quoted(token)}` : `set -e ${TOKEN_ENV}`}
|
|
3073
|
+
`, { mode: 384 });
|
|
3074
|
+
for (const file of startup) {
|
|
3075
|
+
const existing = existsSync8(file.path) ? readFileSync11(file.path, "utf8") : "";
|
|
3076
|
+
if (existing.includes(HOOK_MARK)) continue;
|
|
3077
|
+
const line = file.kind === "fish" ? `${HOOK_MARK}
|
|
3078
|
+
test -f ${quoted(fish)}; and source ${quoted(fish)}
|
|
3079
|
+
` : `${HOOK_MARK}
|
|
3080
|
+
[ -f ${quoted(sh)} ] && . ${quoted(sh)}
|
|
3081
|
+
`;
|
|
3082
|
+
mkdirSync3(dirname5(file.path), { recursive: true });
|
|
3083
|
+
appendFileSync(file.path, `${existing && !existing.endsWith("\n") ? "\n" : ""}${line}`);
|
|
3084
|
+
hooked.push(file.path);
|
|
3085
|
+
}
|
|
3086
|
+
} catch (err) {
|
|
3087
|
+
return { outcome: "failed", files: [sh, fish, ...hooked], detail: String(err) };
|
|
3088
|
+
}
|
|
3089
|
+
return { outcome: token ? "written" : "cleared", files: [sh, fish, ...hooked] };
|
|
3090
|
+
}
|
|
3162
3091
|
|
|
3163
|
-
|
|
3092
|
+
// src/local-access.ts
|
|
3093
|
+
function syncLocalAccess(token, clients = detectMcpClients()) {
|
|
3094
|
+
const lines = [];
|
|
3095
|
+
const shell = writeShellEnv(token);
|
|
3096
|
+
if (shell.outcome === "failed") lines.push(` ${TOKEN_ENV} not updated for new shells: ${shell.detail}`);
|
|
3097
|
+
else lines.push(token ? ` new shells get ${TOKEN_ENV} for this login` : ` new shells no longer get ${TOKEN_ENV}`);
|
|
3098
|
+
if (token) {
|
|
3099
|
+
for (const registered of refreshMcpRegistrations({ token, mcpUrl: mcpUrlFor(apiBase()) }, clients)) {
|
|
3100
|
+
const name = clients.find((client) => client.id === registered.agent)?.name ?? registered.agent;
|
|
3101
|
+
lines.push(registered.outcome === "registered" ? ` agentsky MCP server for ${name} now uses this login` : ` agentsky MCP server for ${name} not updated: ${registered.detail ?? registered.outcome} \u2014 run \`sky setup skill\``);
|
|
3102
|
+
}
|
|
3103
|
+
return lines;
|
|
3104
|
+
}
|
|
3105
|
+
const holding = clients.filter((client) => hasMcpServer(client)).map((client) => client.name);
|
|
3106
|
+
if (holding.length) lines.push(` the agentsky MCP server in ${holding.join(", ")} still uses the old token: \`sky auth login\` replaces it, or revoke the key in Developer \u2192 API keys`);
|
|
3107
|
+
return lines;
|
|
3108
|
+
}
|
|
3164
3109
|
|
|
3165
|
-
|
|
3110
|
+
// src/login.ts
|
|
3111
|
+
function b64url(buf) {
|
|
3112
|
+
return buf.toString("base64url");
|
|
3113
|
+
}
|
|
3114
|
+
function openBrowser2(url) {
|
|
3115
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3116
|
+
try {
|
|
3117
|
+
spawn2(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
3118
|
+
} catch {
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
async function login(opts) {
|
|
3122
|
+
const verifier = b64url(randomBytes(32));
|
|
3123
|
+
const challenge = b64url(createHash4("sha256").update(verifier).digest());
|
|
3124
|
+
const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
|
|
3125
|
+
saveConfig({
|
|
3126
|
+
...loadConfig(),
|
|
3127
|
+
apiBase: apiBase(),
|
|
3128
|
+
token: result.token,
|
|
3129
|
+
email: result.user.email ?? void 0,
|
|
3130
|
+
currentUniverse: void 0
|
|
3131
|
+
// personal by default
|
|
3132
|
+
});
|
|
3133
|
+
console.log(
|
|
3134
|
+
`Logged in as ${result.user.email ?? "you"}${result.universe ? ` (universe: ${result.universe.slug})` : ""}`
|
|
3135
|
+
);
|
|
3136
|
+
for (const line of syncLocalAccess(result.token)) console.log(line);
|
|
3137
|
+
}
|
|
3138
|
+
async function browserLogin(verifier, challenge) {
|
|
3139
|
+
const state = b64url(randomBytes(16));
|
|
3140
|
+
const code = await new Promise((resolve10, reject) => {
|
|
3141
|
+
const server = createServer((req, res) => {
|
|
3142
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
3143
|
+
if (url.pathname !== "/callback") {
|
|
3144
|
+
res.writeHead(404).end();
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3148
|
+
res.end("<p>Logged in \u2014 you can close this tab and return to your terminal.</p>");
|
|
3149
|
+
const got = url.searchParams.get("code");
|
|
3150
|
+
if (url.searchParams.get("state") !== state || !got) {
|
|
3151
|
+
reject(new Error("login callback state mismatch"));
|
|
3152
|
+
} else {
|
|
3153
|
+
resolve10(got);
|
|
3154
|
+
}
|
|
3155
|
+
server.close();
|
|
3156
|
+
});
|
|
3157
|
+
server.listen(0, "127.0.0.1", () => {
|
|
3158
|
+
const address = server.address();
|
|
3159
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
3160
|
+
const authorizeUrl = `${consoleBase()}/cli/authorize?state=${state}&port=${port}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
|
|
3161
|
+
console.log("Opening your browser to authorize this machine\u2026");
|
|
3162
|
+
console.log(`If it doesn't open, visit:
|
|
3163
|
+
${authorizeUrl}`);
|
|
3164
|
+
openBrowser2(authorizeUrl);
|
|
3165
|
+
});
|
|
3166
|
+
setTimeout(() => {
|
|
3167
|
+
server.close();
|
|
3168
|
+
reject(new Error("login timed out after 10 minutes"));
|
|
3169
|
+
}, 10 * 60 * 1e3).unref();
|
|
3170
|
+
});
|
|
3171
|
+
return api(
|
|
3172
|
+
"POST",
|
|
3173
|
+
"/cli/token-exchange",
|
|
3174
|
+
{ code, verifier, clientName: `cli ${hostname()}` },
|
|
3175
|
+
{ auth: false }
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
async function headlessLogin(verifier, challenge) {
|
|
3179
|
+
const request = b64url(randomBytes(24));
|
|
3180
|
+
const authorizeUrl = `${consoleBase()}/cli/authorize?request=${request}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
|
|
3181
|
+
console.log(`Visit this URL on any device to authorize:
|
|
3182
|
+
${authorizeUrl}
|
|
3183
|
+
`);
|
|
3184
|
+
process.stdout.write("Waiting for approval\u2026");
|
|
3185
|
+
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
3186
|
+
while (Date.now() < deadline) {
|
|
3187
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
3188
|
+
const res = await fetch(
|
|
3189
|
+
`${apiBase()}/cli/poll?request=${request}&verifier=${encodeURIComponent(verifier)}&clientName=${encodeURIComponent(`cli ${hostname()}`)}`,
|
|
3190
|
+
{ headers: { "user-agent": USER_AGENT } }
|
|
3191
|
+
);
|
|
3192
|
+
if (!res.ok) {
|
|
3193
|
+
const body2 = await res.json().catch(() => null);
|
|
3194
|
+
throw new Error(body2?.error?.message ?? `poll failed (${res.status})`);
|
|
3195
|
+
}
|
|
3196
|
+
const body = await res.json();
|
|
3197
|
+
if (body.status === "approved") {
|
|
3198
|
+
console.log(" approved.");
|
|
3199
|
+
return body;
|
|
3200
|
+
}
|
|
3201
|
+
process.stdout.write(".");
|
|
3202
|
+
}
|
|
3203
|
+
throw new Error("login timed out after 10 minutes");
|
|
3204
|
+
}
|
|
3166
3205
|
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3206
|
+
// src/connectors.ts
|
|
3207
|
+
async function listApps(query, limit = 50) {
|
|
3208
|
+
const apps = [];
|
|
3209
|
+
let cursor = null;
|
|
3210
|
+
do {
|
|
3211
|
+
const params = new URLSearchParams();
|
|
3212
|
+
if (query) params.set("q", query);
|
|
3213
|
+
if (cursor) params.set("cursor", cursor);
|
|
3214
|
+
const qs = params.toString();
|
|
3215
|
+
const page = await api("GET", `/connectors/apps${qs ? `?${qs}` : ""}`);
|
|
3216
|
+
apps.push(...page.apps);
|
|
3217
|
+
cursor = page.next_cursor;
|
|
3218
|
+
} while (cursor && apps.length < limit);
|
|
3219
|
+
return apps.slice(0, limit);
|
|
3220
|
+
}
|
|
3221
|
+
function connectionLine2(c) {
|
|
3222
|
+
return `${c.id} ${c.app_name} (${c.app}) ${c.label} since ${c.created_at.slice(0, 10)}`;
|
|
3223
|
+
}
|
|
3224
|
+
function workspaceConnectionLine(c) {
|
|
3225
|
+
const owner = c.workspace_owned ? "the workspace" : c.mine ? "you" : c.shared_by?.name ?? c.shared_by?.user_id ?? "someone";
|
|
3226
|
+
return `${c.connection_id} ${c.app_name} (${c.app}) ${c.label} shared by ${owner}`;
|
|
3227
|
+
}
|
|
3228
|
+
function browserReturn(start, opts = {}) {
|
|
3229
|
+
return new Promise((resolve10, reject) => {
|
|
3230
|
+
const server = createServer2((req, res) => {
|
|
3231
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
3232
|
+
if (url.pathname !== "/done" && url.pathname !== "/failed") {
|
|
3233
|
+
res.writeHead(404).end();
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
const ok = url.pathname === "/done";
|
|
3237
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3238
|
+
res.end(ok ? "<p>Connected \u2014 you can close this tab and return to your terminal.</p>" : "<p>The app was not connected. Return to your terminal.</p>");
|
|
3239
|
+
server.close();
|
|
3240
|
+
if (ok) resolve10(url.searchParams.get("account_id") ?? void 0);
|
|
3241
|
+
else reject(new ApiError(400, "connect_failed", "the app was not connected"));
|
|
3242
|
+
});
|
|
3243
|
+
server.listen(0, "127.0.0.1", () => {
|
|
3244
|
+
const address = server.address();
|
|
3245
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
3246
|
+
const base = `http://127.0.0.1:${port}`;
|
|
3247
|
+
start(`${base}/done`, `${base}/failed`).then(
|
|
3248
|
+
(authorizeUrl) => {
|
|
3249
|
+
console.log(`Authorize access in your browser:
|
|
3250
|
+
${authorizeUrl}`);
|
|
3251
|
+
if (opts.open !== false) openBrowser2(authorizeUrl);
|
|
3252
|
+
},
|
|
3253
|
+
(err) => {
|
|
3254
|
+
server.close();
|
|
3255
|
+
reject(err);
|
|
3256
|
+
}
|
|
3257
|
+
);
|
|
3258
|
+
});
|
|
3259
|
+
setTimeout(() => {
|
|
3260
|
+
server.close();
|
|
3261
|
+
reject(new ApiError(408, "connect_timeout", "nobody finished the authorization within 10 minutes"));
|
|
3262
|
+
}, opts.timeoutMs ?? 10 * 6e4).unref();
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
async function connectApp(app, opts = {}) {
|
|
3266
|
+
const base = opts.workspace ? `/universes/${encodeURIComponent(opts.workspace)}/connections` : "/connectors/connections";
|
|
3267
|
+
const accountId = await browserReturn(async (success_url, error_url) => {
|
|
3268
|
+
const { url } = await api("POST", base, { app, success_url, error_url });
|
|
3269
|
+
return url;
|
|
3270
|
+
}, { open: opts.open });
|
|
3271
|
+
const done = await api("POST", `${base}/complete`, {
|
|
3272
|
+
app,
|
|
3273
|
+
...accountId ? { account_id: accountId } : {}
|
|
3274
|
+
});
|
|
3275
|
+
return done.connection;
|
|
3174
3276
|
}
|
|
3175
|
-
function workspacesReferenceDoc() {
|
|
3176
|
-
return `# Workspaces in AgentSky
|
|
3177
|
-
|
|
3178
|
-
A workspace (also called a universe) groups agents and sessions. Every API call
|
|
3179
|
-
and MCP tool accepts an optional workspace slug.
|
|
3180
|
-
|
|
3181
|
-
## How to choose
|
|
3182
|
-
|
|
3183
|
-
1. The person named one \u2192 use it.
|
|
3184
|
-
2. Check the git root for \`.agentsky.json\` with a \`"workspace"\` key \u2192 use that.
|
|
3185
|
-
3. Neither \u2192 use the account default (personal workspace). Never ask.
|
|
3186
|
-
|
|
3187
|
-
## Applying it
|
|
3188
|
-
|
|
3189
|
-
- MCP tools: pass \`workspace: "<slug>"\` on tools that accept it.
|
|
3190
|
-
- sky CLI: \`--workspace <slug>\` on any command, or set \`SKY_WORKSPACE=<slug>\`.
|
|
3191
|
-
- Tools that take a \`task_id\` or \`session_id\` inherit that task's workspace
|
|
3192
|
-
automatically; one task never spans workspaces.
|
|
3193
|
-
|
|
3194
|
-
## Listing workspaces
|
|
3195
|
-
|
|
3196
|
-
\`sky workspace list\` (or \`sky universe list\`) prints your workspaces, with
|
|
3197
|
-
\`*\` marking the current default.
|
|
3198
|
-
|
|
3199
|
-
## .agentsky.json
|
|
3200
3277
|
|
|
3201
|
-
|
|
3278
|
+
// src/routine-triggers.ts
|
|
3279
|
+
var triggerPath = (id, resource) => `/routines/${encodeURIComponent(id)}/${resource}`;
|
|
3280
|
+
function inboundLine(u) {
|
|
3281
|
+
if (!u) return "no inbound URL \u2014 make one with --create";
|
|
3282
|
+
return u.url ?? `inbound URL ${u.prefix}\u2026 made ${u.created_at.slice(0, 10)}${u.last_used_at ? `, last used ${u.last_used_at.slice(0, 16).replace("T", " ")}` : ", never used"}`;
|
|
3283
|
+
}
|
|
3284
|
+
function precheckLine(p) {
|
|
3285
|
+
if (!p) return "no precheck: every scheduled fire runs";
|
|
3286
|
+
const last = p.last_result ? `last check ${p.last_result}${p.last_error ? ` (${p.last_error.type}: ${p.last_error.message})` : ""}` : "not checked yet";
|
|
3287
|
+
return `fires only when ${p.url} changes \xB7 ${last}`;
|
|
3288
|
+
}
|
|
3289
|
+
function triggerLine(t, available = true) {
|
|
3290
|
+
if (!t) return "no chat trigger";
|
|
3291
|
+
const where = t.thread_id ? `thread ${t.thread_id} on ${t.connection_id}` : `any conversation on ${t.connection_id}`;
|
|
3292
|
+
const what = t.text_prefix ? `messages starting "${t.text_prefix}"` : "every message";
|
|
3293
|
+
return `${what} in ${where} start a run${available ? "" : " (chat messages are not offered to routines in this deployment)"}`;
|
|
3294
|
+
}
|
|
3295
|
+
function deliverLine(d) {
|
|
3296
|
+
return d ? `answers post to thread ${d.thread_id} on ${d.connection_id}` : "answers are posted nowhere";
|
|
3297
|
+
}
|
|
3298
|
+
function triggerBody(opts) {
|
|
3299
|
+
if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
|
|
3300
|
+
return { connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {}, ...opts.prefix ? { text_prefix: opts.prefix } : {} };
|
|
3301
|
+
}
|
|
3302
|
+
function deliverBody(opts) {
|
|
3303
|
+
if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
|
|
3304
|
+
return { type: "channel_thread", connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {} };
|
|
3305
|
+
}
|
|
3306
|
+
async function triggerRequest(id, resource, change) {
|
|
3307
|
+
const path = triggerPath(id, resource);
|
|
3308
|
+
if (change.remove) return api("DELETE", path);
|
|
3309
|
+
if (change.create) return api("POST", path);
|
|
3310
|
+
if (change.body) return api("PUT", path, change.body);
|
|
3311
|
+
return api("GET", path);
|
|
3312
|
+
}
|
|
3202
3313
|
|
|
3203
|
-
|
|
3204
|
-
{
|
|
3205
|
-
|
|
3206
|
-
|
|
3314
|
+
// src/session-workspace.ts
|
|
3315
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
3316
|
+
import { createHash as createHash5, randomUUID } from "crypto";
|
|
3317
|
+
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
3318
|
+
import { basename as basename7, join as join12, relative as relative3, resolve as resolve5 } from "path";
|
|
3319
|
+
var SINGLE_IMPORT_MAX = 25 * 1024 * 1024;
|
|
3320
|
+
var PART_SIZE = 8 * 1024 * 1024;
|
|
3321
|
+
var SKIPPED2 = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
3322
|
+
var workspacePath = (id) => `/sessions/${encodeURIComponent(id)}/workspace`;
|
|
3323
|
+
async function listFiles2(id, path, hidden) {
|
|
3324
|
+
const query = new URLSearchParams();
|
|
3325
|
+
if (path) query.set("path", path);
|
|
3326
|
+
if (hidden) query.set("hidden", "true");
|
|
3327
|
+
const qs = query.toString();
|
|
3328
|
+
return api("GET", `${workspacePath(id)}/files${qs ? `?${qs}` : ""}`);
|
|
3207
3329
|
}
|
|
3208
|
-
function
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3330
|
+
function entryLine(e) {
|
|
3331
|
+
const size = e.type === "directory" ? "-" : String(e.size ?? 0);
|
|
3332
|
+
return `${e.type === "directory" ? "d" : "-"} ${size.padStart(10)} ${(e.mtime ?? "").slice(0, 16).replace("T", " ")} ${e.name}${e.type === "directory" ? "/" : ""}`;
|
|
3333
|
+
}
|
|
3334
|
+
async function readFile(id, path) {
|
|
3335
|
+
return (await apiBytes("GET", `${workspacePath(id)}/files/content?path=${encodeURIComponent(path)}`)).bytes;
|
|
3336
|
+
}
|
|
3337
|
+
function renderChanges(changes) {
|
|
3338
|
+
if (!changes.repos.length) return [`no git repository under ${changes.root}`];
|
|
3339
|
+
return changes.repos.flatMap((r) => [
|
|
3340
|
+
`${r.path === "." ? changes.root : r.path}: ${r.status ? "changed" : "clean"}`,
|
|
3341
|
+
...(r.stat ?? r.status ?? "").split("\n").filter(Boolean).map((line) => ` ${line}`)
|
|
3342
|
+
]);
|
|
3343
|
+
}
|
|
3344
|
+
async function workspacePatch(id, repo) {
|
|
3345
|
+
const { patch, ...meta } = await api("POST", `${workspacePath(id)}/patch`, { repo });
|
|
3346
|
+
return { meta, patch: Buffer.from(patch, "base64") };
|
|
3347
|
+
}
|
|
3348
|
+
function folderFiles(dir) {
|
|
3349
|
+
const git2 = spawnSync4("git", ["-C", dir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8" });
|
|
3350
|
+
const top = spawnSync4("git", ["-C", dir, "rev-parse", "--show-prefix"], { encoding: "utf8" });
|
|
3351
|
+
if (git2.status === 0 && top.status === 0) {
|
|
3352
|
+
return git2.stdout.split("\0").filter((path) => path && statSync6(join12(dir, path), { throwIfNoEntry: false })?.isFile());
|
|
3214
3353
|
}
|
|
3354
|
+
const files2 = [];
|
|
3355
|
+
const walk = (at) => {
|
|
3356
|
+
for (const entry2 of readdirSync5(at, { withFileTypes: true })) {
|
|
3357
|
+
if (SKIPPED2.has(entry2.name)) continue;
|
|
3358
|
+
const full = join12(at, entry2.name);
|
|
3359
|
+
if (entry2.isDirectory()) walk(full);
|
|
3360
|
+
else if (entry2.isFile()) files2.push(relative3(dir, full));
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
walk(dir);
|
|
3364
|
+
return files2;
|
|
3215
3365
|
}
|
|
3216
|
-
function
|
|
3217
|
-
|
|
3366
|
+
function folderArchive(dir, files2) {
|
|
3367
|
+
const tar = spawnSync4("tar", ["--format=ustar", "-czf", "-", "-C", dir, "-T", "-"], {
|
|
3368
|
+
input: files2.join("\n"),
|
|
3369
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
3370
|
+
maxBuffer: 2 * 1024 * 1024 * 1024
|
|
3371
|
+
});
|
|
3372
|
+
if (tar.status !== 0) throw new ApiError(400, "archive_failed", `tar failed: ${tar.stderr.toString().trim()}`);
|
|
3373
|
+
return tar.stdout;
|
|
3218
3374
|
}
|
|
3219
|
-
function
|
|
3220
|
-
const
|
|
3221
|
-
const
|
|
3222
|
-
const
|
|
3223
|
-
if (
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
writeFileSync4(join10(dir, "SKILL.md"), skillDocument());
|
|
3227
|
-
writeFileSync4(join10(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
|
|
3228
|
-
`);
|
|
3375
|
+
async function importFolder(id, dir, opts = {}) {
|
|
3376
|
+
const root = resolve5(dir);
|
|
3377
|
+
const name = opts.name ?? basename7(root);
|
|
3378
|
+
const archive = opts.archive ?? folderArchive(root, folderFiles(root));
|
|
3379
|
+
if (archive.length <= SINGLE_IMPORT_MAX) {
|
|
3380
|
+
const res = await apiBytes("POST", `${workspacePath(id)}/import?name=${encodeURIComponent(name)}`, { bytes: archive, mediaType: "application/gzip" });
|
|
3381
|
+
return res.json();
|
|
3229
3382
|
}
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3383
|
+
const sha256 = createHash5("sha256").update(archive).digest("hex");
|
|
3384
|
+
let upload = await api("POST", `${workspacePath(id)}/imports`, { name, size: archive.length, sha256, part_size: PART_SIZE });
|
|
3385
|
+
const uploadPath = `${workspacePath(id)}/imports/${encodeURIComponent(upload.id)}`;
|
|
3386
|
+
for (let part = 0; part < upload.parts; part += 1) {
|
|
3387
|
+
if (upload.received.includes(part)) continue;
|
|
3388
|
+
const bytes = archive.subarray(part * upload.part_size, (part + 1) * upload.part_size);
|
|
3389
|
+
await apiBytes("PUT", `${uploadPath}/parts/${part}`, { bytes, mediaType: "application/octet-stream" });
|
|
3233
3390
|
}
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3391
|
+
upload = await api("POST", `${uploadPath}/complete`);
|
|
3392
|
+
while (upload.state !== "imported" && upload.state !== "failed") {
|
|
3393
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
|
|
3394
|
+
upload = await api("GET", uploadPath);
|
|
3395
|
+
}
|
|
3396
|
+
if (upload.state === "failed") throw new ApiError(422, upload.error?.code ?? "import_failed", upload.error?.message ?? "the import failed");
|
|
3397
|
+
return { path: upload.path ?? name, files: upload.files ?? 0 };
|
|
3240
3398
|
}
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3399
|
+
function renderAccess(a) {
|
|
3400
|
+
const lines = [
|
|
3401
|
+
`${a.session_id}${a.title ? ` \u2014 ${a.title}` : ""}`,
|
|
3402
|
+
` ${a.visibility === "private" ? "private to its owner" : `visible to ${a.workspace.name} (${a.workspace.slug})`} \xB7 you are its ${a.access}`,
|
|
3403
|
+
` owner: ${a.owner.name ?? a.owner.email ?? "unknown"}`
|
|
3404
|
+
];
|
|
3405
|
+
if (a.viewers) lines.push(a.viewers.length ? ` viewers: ${a.viewers.map((v) => v.email).join(", ")}` : " viewers: none");
|
|
3406
|
+
return lines;
|
|
3407
|
+
}
|
|
3408
|
+
function accessPatch(opts) {
|
|
3409
|
+
if (opts.private && opts.members) throw new ApiError(400, "invalid_request", "pass --private or --members, not both");
|
|
3246
3410
|
return {
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
return { status: result.status, stderr: result.stderr ?? "" };
|
|
3251
|
-
}
|
|
3411
|
+
...opts.private ? { visibility: "private" } : opts.members ? { visibility: "workspace" } : {},
|
|
3412
|
+
...opts.addViewer?.length ? { add_viewers: opts.addViewer } : {},
|
|
3413
|
+
...opts.removeViewer?.length ? { remove_viewers: opts.removeViewer } : {}
|
|
3252
3414
|
};
|
|
3253
3415
|
}
|
|
3254
|
-
function
|
|
3255
|
-
|
|
3416
|
+
function engineChange(model, thinking) {
|
|
3417
|
+
if (!model && !thinking) return null;
|
|
3418
|
+
return {
|
|
3419
|
+
...model ? { llm: model } : {},
|
|
3420
|
+
...thinking ? { reasoningEffort: thinking === "default" ? null : thinking } : {}
|
|
3421
|
+
};
|
|
3256
3422
|
}
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3423
|
+
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
3424
|
+
async function handoff(id, targetAgent, opts = {}) {
|
|
3425
|
+
let { handoff: h } = await api("POST", `/sessions/${encodeURIComponent(id)}/handoff`, {
|
|
3426
|
+
target_agent: targetAgent,
|
|
3427
|
+
...opts.fromMessage ? { last_message_id: opts.fromMessage } : {}
|
|
3428
|
+
}, { idempotencyKey: opts.idempotencyKey ?? randomUUID() });
|
|
3429
|
+
opts.onStatus?.(h);
|
|
3430
|
+
while (opts.wait && !TERMINAL.has(h.status)) {
|
|
3431
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
|
|
3432
|
+
const next = (await api("GET", `/session-handoffs/${encodeURIComponent(h.id)}`)).handoff;
|
|
3433
|
+
if (next.status !== h.status) opts.onStatus?.(next);
|
|
3434
|
+
h = next;
|
|
3264
3435
|
}
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3436
|
+
return h;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// src/render.ts
|
|
3440
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
3441
|
+
var red = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
3442
|
+
function clip(value, max = 120) {
|
|
3443
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
3444
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
3445
|
+
}
|
|
3446
|
+
function renderEvent(event, data) {
|
|
3447
|
+
const part = data.part;
|
|
3448
|
+
switch (event) {
|
|
3449
|
+
case "agent.message":
|
|
3450
|
+
console.log(String(data.text ?? ""));
|
|
3451
|
+
break;
|
|
3452
|
+
case "agent.reasoning":
|
|
3453
|
+
console.log(dim(`\xB7 ${clip(part?.text ?? "")}`));
|
|
3454
|
+
break;
|
|
3455
|
+
case "agent.tool_use":
|
|
3456
|
+
console.log(dim(`\u2192 ${String(part?.tool_name ?? "tool")}(${clip(part?.args ?? {}, 80)})`));
|
|
3457
|
+
break;
|
|
3458
|
+
case "agent.tool_result":
|
|
3459
|
+
console.log(dim(`\u2190 ${String(part?.tool_name ?? "tool")} ${String(part?.status ?? "")}`));
|
|
3460
|
+
break;
|
|
3461
|
+
case "agent.status":
|
|
3462
|
+
console.log(dim(`\u2026 ${String(part?.level ?? "")} ${String(part?.text ?? "")}`.trim()));
|
|
3463
|
+
break;
|
|
3464
|
+
case "user.interrupt":
|
|
3465
|
+
console.log(dim("\u23F9 interrupting"));
|
|
3466
|
+
break;
|
|
3467
|
+
case "session.error": {
|
|
3468
|
+
const error = data.error;
|
|
3469
|
+
const message2 = String(error?.message ?? error?.type ?? "unknown");
|
|
3470
|
+
if (error?.retry_status === "retrying") console.error(dim(`retrying: ${message2}`));
|
|
3471
|
+
else console.error(red(`error: ${message2}`));
|
|
3472
|
+
break;
|
|
3473
|
+
}
|
|
3474
|
+
case "session.status_terminated":
|
|
3475
|
+
console.log(dim("session terminated"));
|
|
3476
|
+
break;
|
|
3477
|
+
default:
|
|
3478
|
+
break;
|
|
3271
3479
|
}
|
|
3272
|
-
return null;
|
|
3273
|
-
}
|
|
3274
|
-
function redact(text, token) {
|
|
3275
|
-
return text.split(token).join(`${token.slice(0, 10)}\u2026`);
|
|
3276
3480
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3481
|
+
|
|
3482
|
+
// src/session-read.ts
|
|
3483
|
+
var TURN_TYPES = ["user.message", "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
|
|
3484
|
+
function latestTurn(events) {
|
|
3485
|
+
let start = 0;
|
|
3486
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
3487
|
+
if (events[index].type === "user.message") {
|
|
3488
|
+
start = index + 1;
|
|
3489
|
+
break;
|
|
3490
|
+
}
|
|
3280
3491
|
}
|
|
3281
|
-
const
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
return agent2.id === "codex" ? { agent: agent2.id, outcome: "registered", detail: `set ${CODEX_TOKEN_ENV} to an AgentSky API token in the shell that runs codex` } : { agent: agent2.id, outcome: "registered" };
|
|
3289
|
-
}
|
|
3290
|
-
function registerCursorMcpServer(agent2, opts) {
|
|
3291
|
-
const configPath2 = `${agent2.root}/mcp.json`;
|
|
3292
|
-
const entry2 = {
|
|
3293
|
-
url: opts.mcpUrl,
|
|
3294
|
-
headers: { Authorization: `Bearer ${opts.token}` }
|
|
3295
|
-
};
|
|
3296
|
-
if (opts.dryRun) {
|
|
3297
|
-
const preview = JSON.stringify({ agentsky: { url: opts.mcpUrl, headers: { Authorization: `Bearer ${opts.token.slice(0, 10)}\u2026` } } }, null, 2);
|
|
3298
|
-
return { agent: agent2.id, outcome: "planned", detail: `would write to ${configPath2}: ${preview}` };
|
|
3492
|
+
const reading = { finished: false, messages: [], errors: [] };
|
|
3493
|
+
for (const event of events.slice(start)) {
|
|
3494
|
+
if (event.type === "agent.message" && event.text?.trim()) reading.messages.push(event.text);
|
|
3495
|
+
else if (event.type === "session.error" && event.error?.retry_status !== "retrying") {
|
|
3496
|
+
reading.errors.push(event.error?.message ?? event.error?.type ?? "unknown error");
|
|
3497
|
+
} else if (event.type === "session.status_terminated") reading.finished = true;
|
|
3498
|
+
else if (event.type === "session.status_idle" && event.stop_reason?.type !== "requires_action") reading.finished = true;
|
|
3299
3499
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3500
|
+
return reading;
|
|
3501
|
+
}
|
|
3502
|
+
async function history2(sessionId) {
|
|
3503
|
+
const events = [];
|
|
3504
|
+
let cursor = null;
|
|
3505
|
+
for (; ; ) {
|
|
3506
|
+
const query = new URLSearchParams({ limit: "500" });
|
|
3507
|
+
for (const type of TURN_TYPES) query.append("types[]", type);
|
|
3508
|
+
if (cursor) query.set("cursor", cursor);
|
|
3509
|
+
const page = await api(
|
|
3510
|
+
"GET",
|
|
3511
|
+
`/sessions/${encodeURIComponent(sessionId)}/events?${query}`
|
|
3512
|
+
);
|
|
3513
|
+
events.push(...page.events);
|
|
3514
|
+
if (!page.hasMore || !page.cursor) return events;
|
|
3515
|
+
cursor = page.cursor;
|
|
3308
3516
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
return { agent: agent2.id, outcome: "failed", detail: String(err) };
|
|
3517
|
+
}
|
|
3518
|
+
async function readTurn(sessionId, opts = {}) {
|
|
3519
|
+
const deadline = Date.now() + (opts.timeoutSeconds ?? 600) * 1e3;
|
|
3520
|
+
for (; ; ) {
|
|
3521
|
+
const reading = latestTurn(await history2(sessionId));
|
|
3522
|
+
if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
|
|
3523
|
+
await new Promise((resolve10) => setTimeout(resolve10, opts.intervalMs ?? 3e3));
|
|
3317
3524
|
}
|
|
3318
|
-
return { agent: agent2.id, outcome: "registered" };
|
|
3319
3525
|
}
|
|
3320
3526
|
|
|
3527
|
+
// src/setup.ts
|
|
3528
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
|
|
3529
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
3530
|
+
import { createInterface as createInterface3 } from "readline";
|
|
3531
|
+
|
|
3321
3532
|
// src/setup-local.ts
|
|
3322
3533
|
import { isUtf8 as isUtf83 } from "buffer";
|
|
3323
|
-
import { existsSync as
|
|
3324
|
-
import { homedir as
|
|
3325
|
-
import { dirname as
|
|
3326
|
-
import { parse as
|
|
3534
|
+
import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
|
|
3535
|
+
import { homedir as homedir8 } from "os";
|
|
3536
|
+
import { dirname as dirname6, isAbsolute as isAbsolute2, join as join13, resolve as resolve6 } from "path";
|
|
3537
|
+
import { parse as parseToml4 } from "smol-toml";
|
|
3327
3538
|
var MAX_SKILL_FILE_BYTES = 1024 * 1024;
|
|
3328
3539
|
var MAX_IMPORT_DEPTH = 5;
|
|
3329
3540
|
var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
@@ -3347,11 +3558,11 @@ function redacted(data) {
|
|
|
3347
3558
|
function skillFiles2(root, prefix) {
|
|
3348
3559
|
const files2 = [];
|
|
3349
3560
|
for (const entry2 of readdirSync6(root, { withFileTypes: true })) {
|
|
3350
|
-
const path =
|
|
3561
|
+
const path = join13(root, entry2.name);
|
|
3351
3562
|
if (entry2.isDirectory()) {
|
|
3352
3563
|
if (!SKIPPED_DIRS.has(entry2.name)) files2.push(...skillFiles2(path, `${prefix}/${entry2.name}`));
|
|
3353
3564
|
} else if (entry2.isFile() && !sensitive(entry2.name) && statSync7(path).size <= MAX_SKILL_FILE_BYTES) {
|
|
3354
|
-
const bytes = redacted(
|
|
3565
|
+
const bytes = redacted(readFileSync12(path));
|
|
3355
3566
|
if (bytes) files2.push({ path: `${prefix}/${entry2.name}`, bytes });
|
|
3356
3567
|
}
|
|
3357
3568
|
}
|
|
@@ -3361,9 +3572,9 @@ function readLocalSkills(agents) {
|
|
|
3361
3572
|
const skills2 = [];
|
|
3362
3573
|
const ignored = [];
|
|
3363
3574
|
for (const agent2 of agents) {
|
|
3364
|
-
if (!
|
|
3575
|
+
if (!existsSync9(agent2.skillsDir)) continue;
|
|
3365
3576
|
for (const entry2 of readdirSync6(agent2.skillsDir, { withFileTypes: true })) {
|
|
3366
|
-
const dir =
|
|
3577
|
+
const dir = join13(agent2.skillsDir, entry2.name);
|
|
3367
3578
|
let isDir2 = entry2.isDirectory();
|
|
3368
3579
|
if (entry2.isSymbolicLink()) {
|
|
3369
3580
|
try {
|
|
@@ -3372,8 +3583,8 @@ function readLocalSkills(agents) {
|
|
|
3372
3583
|
isDir2 = false;
|
|
3373
3584
|
}
|
|
3374
3585
|
}
|
|
3375
|
-
if (!isDir2 || !
|
|
3376
|
-
const name = frontmatterName(
|
|
3586
|
+
if (!isDir2 || !existsSync9(join13(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
|
|
3587
|
+
const name = frontmatterName(readFileSync12(join13(dir, "SKILL.md"), "utf8"));
|
|
3377
3588
|
if (!name) {
|
|
3378
3589
|
ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
|
|
3379
3590
|
continue;
|
|
@@ -3401,11 +3612,11 @@ function inlineImports(content, from, home, depth, seen) {
|
|
|
3401
3612
|
}
|
|
3402
3613
|
const target = fence ? void 0 : line.match(/^@(\S+\.(?:md|markdown|txt))$/i)?.[1];
|
|
3403
3614
|
if (!target || depth >= MAX_IMPORT_DEPTH) return line;
|
|
3404
|
-
const path = target.startsWith("~/") ?
|
|
3615
|
+
const path = target.startsWith("~/") ? join13(home, target.slice(2)) : isAbsolute2(target) ? target : resolve6(dirname6(from), target);
|
|
3405
3616
|
if (seen.includes(path)) return "";
|
|
3406
3617
|
let imported;
|
|
3407
3618
|
try {
|
|
3408
|
-
imported =
|
|
3619
|
+
imported = readFileSync12(path, "utf8");
|
|
3409
3620
|
} catch {
|
|
3410
3621
|
return line;
|
|
3411
3622
|
}
|
|
@@ -3413,15 +3624,15 @@ function inlineImports(content, from, home, depth, seen) {
|
|
|
3413
3624
|
return inlineImports(imported, path, home, depth + 1, seen).trim();
|
|
3414
3625
|
}).join("\n");
|
|
3415
3626
|
}
|
|
3416
|
-
function readGlobalInstructions(agents, home =
|
|
3627
|
+
function readGlobalInstructions(agents, home = homedir8()) {
|
|
3417
3628
|
const found = [];
|
|
3418
3629
|
for (const agent2 of agents) {
|
|
3419
3630
|
const file = INSTRUCTION_FILES2[agent2.id];
|
|
3420
3631
|
if (!file) continue;
|
|
3421
|
-
const path =
|
|
3632
|
+
const path = join13(agent2.root, file);
|
|
3422
3633
|
let raw;
|
|
3423
3634
|
try {
|
|
3424
|
-
raw =
|
|
3635
|
+
raw = readFileSync12(path, "utf8");
|
|
3425
3636
|
} catch {
|
|
3426
3637
|
continue;
|
|
3427
3638
|
}
|
|
@@ -3440,7 +3651,7 @@ ${item.content.trim()}`).join("\n\n");
|
|
|
3440
3651
|
}
|
|
3441
3652
|
function readJson3(path) {
|
|
3442
3653
|
try {
|
|
3443
|
-
return JSON.parse(
|
|
3654
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
3444
3655
|
} catch {
|
|
3445
3656
|
return void 0;
|
|
3446
3657
|
}
|
|
@@ -3454,18 +3665,19 @@ function bearerOf(def) {
|
|
|
3454
3665
|
return match?.[1] ?? null;
|
|
3455
3666
|
}
|
|
3456
3667
|
function serversOf(agent2, home) {
|
|
3457
|
-
if (agent2.id === "claude_code") return readJson3(
|
|
3458
|
-
if (agent2.id === "cursor") return readJson3(
|
|
3668
|
+
if (agent2.id === "claude_code") return readJson3(join13(home, ".claude.json"))?.mcpServers ?? {};
|
|
3669
|
+
if (agent2.id === "cursor") return readJson3(join13(agent2.root, "mcp.json"))?.mcpServers ?? {};
|
|
3459
3670
|
try {
|
|
3460
|
-
return
|
|
3671
|
+
return parseToml4(readFileSync12(join13(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
|
|
3461
3672
|
} catch {
|
|
3462
3673
|
return {};
|
|
3463
3674
|
}
|
|
3464
3675
|
}
|
|
3465
|
-
function findLocalSecrets(agents, home =
|
|
3676
|
+
function findLocalSecrets(agents, home = homedir8()) {
|
|
3466
3677
|
const secrets2 = [];
|
|
3467
3678
|
for (const agent2 of agents) {
|
|
3468
3679
|
for (const [name, def] of Object.entries(serversOf(agent2, home))) {
|
|
3680
|
+
if (name === SKILL_NAME) continue;
|
|
3469
3681
|
if (!def || typeof def !== "object" || typeof def.url !== "string" || !/^https:\/\//i.test(def.url)) continue;
|
|
3470
3682
|
const token = bearerOf(def);
|
|
3471
3683
|
if (!token || /^\$\{?[A-Za-z_]/.test(token)) continue;
|
|
@@ -3478,9 +3690,9 @@ function findLocalSecrets(agents, home = homedir6()) {
|
|
|
3478
3690
|
|
|
3479
3691
|
// src/subscriptions.ts
|
|
3480
3692
|
import { spawn as spawn3 } from "child_process";
|
|
3481
|
-
import { existsSync as
|
|
3482
|
-
import { homedir as
|
|
3483
|
-
import { join as
|
|
3693
|
+
import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
|
|
3694
|
+
import { homedir as homedir9 } from "os";
|
|
3695
|
+
import { join as join14 } from "path";
|
|
3484
3696
|
import { createInterface as createInterface2 } from "readline";
|
|
3485
3697
|
var ALIASES = {
|
|
3486
3698
|
claude: "anthropic",
|
|
@@ -3575,12 +3787,12 @@ async function connectClaude() {
|
|
|
3575
3787
|
const expiry = sub.expiresAt ? ` (expires ~${sub.expiresAt.slice(0, 10)})` : "";
|
|
3576
3788
|
console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
|
|
3577
3789
|
}
|
|
3578
|
-
function codexSignIn(env = process.env, home =
|
|
3579
|
-
const path =
|
|
3580
|
-
if (!
|
|
3790
|
+
function codexSignIn(env = process.env, home = homedir9()) {
|
|
3791
|
+
const path = join14(env.CODEX_HOME || join14(home, ".codex"), "auth.json");
|
|
3792
|
+
if (!existsSync10(path)) {
|
|
3581
3793
|
throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
|
|
3582
3794
|
}
|
|
3583
|
-
const raw =
|
|
3795
|
+
const raw = readFileSync13(path, "utf8");
|
|
3584
3796
|
try {
|
|
3585
3797
|
const parsed = JSON.parse(raw);
|
|
3586
3798
|
return { path, raw, account: parsed.tokens?.account_id };
|
|
@@ -3588,12 +3800,12 @@ function codexSignIn(env = process.env, home = homedir7()) {
|
|
|
3588
3800
|
throw new ApiError(400, "invalid_codex_auth", `${path} isn't valid JSON \u2014 re-run \`codex login\``);
|
|
3589
3801
|
}
|
|
3590
3802
|
}
|
|
3591
|
-
function cursorSignIn(env = process.env, home =
|
|
3592
|
-
const path =
|
|
3593
|
-
if (!
|
|
3803
|
+
function cursorSignIn(env = process.env, home = homedir9()) {
|
|
3804
|
+
const path = join14(env.XDG_CONFIG_HOME || join14(home, ".config"), "cursor", "auth.json");
|
|
3805
|
+
if (!existsSync10(path)) {
|
|
3594
3806
|
throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
|
|
3595
3807
|
}
|
|
3596
|
-
const raw =
|
|
3808
|
+
const raw = readFileSync13(path, "utf8");
|
|
3597
3809
|
try {
|
|
3598
3810
|
const parsed = JSON.parse(raw);
|
|
3599
3811
|
if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") throw new Error("shape");
|
|
@@ -3752,11 +3964,11 @@ function planProviders(names2) {
|
|
|
3752
3964
|
}));
|
|
3753
3965
|
}
|
|
3754
3966
|
function reportPath() {
|
|
3755
|
-
return
|
|
3967
|
+
return join15(dirname7(configPath()), "setup.json");
|
|
3756
3968
|
}
|
|
3757
3969
|
function loadReport() {
|
|
3758
3970
|
try {
|
|
3759
|
-
const saved = JSON.parse(
|
|
3971
|
+
const saved = JSON.parse(readFileSync14(reportPath(), "utf8"));
|
|
3760
3972
|
return { agents: saved.agents ?? [], plans: saved.plans ?? [], ...saved.secrets ? { secrets: saved.secrets } : {} };
|
|
3761
3973
|
} catch {
|
|
3762
3974
|
return { agents: [], plans: [] };
|
|
@@ -3769,8 +3981,8 @@ function mergeReport(base, next) {
|
|
|
3769
3981
|
return { agents, plans, ...secrets2 ? { secrets: secrets2 } : {} };
|
|
3770
3982
|
}
|
|
3771
3983
|
function saveReport(report) {
|
|
3772
|
-
|
|
3773
|
-
|
|
3984
|
+
mkdirSync4(dirname7(reportPath()), { recursive: true });
|
|
3985
|
+
writeFileSync7(reportPath(), `${JSON.stringify(report, null, 2)}
|
|
3774
3986
|
`, { mode: 384 });
|
|
3775
3987
|
}
|
|
3776
3988
|
function confirm3(question) {
|
|
@@ -3788,7 +4000,7 @@ function kb2(bytes) {
|
|
|
3788
4000
|
function describe2(event) {
|
|
3789
4001
|
switch (event.type) {
|
|
3790
4002
|
case "agents":
|
|
3791
|
-
return event.agents.length ? `Found on this computer: ${event.agents.map((agent2) => agent2.name).join(", ")}` : "No agent found on this computer (looked for Claude Code, Codex and
|
|
4003
|
+
return event.agents.length ? `Found on this computer: ${event.agents.map((agent2) => agent2.name).join(", ")}` : "No agent found on this computer (looked for Claude Code, Codex, Cursor and Hermes).";
|
|
3792
4004
|
case "catalog":
|
|
3793
4005
|
return event.outcome === "read" ? ` agent catalog: ${event.agents} agent${event.agents === 1 ? "" : "s"}` : ` agent catalog: not read${event.detail ? ` (${event.detail})` : ""}; the skill looks agents up with search_catalog`;
|
|
3794
4006
|
case "skill":
|
|
@@ -3798,12 +4010,23 @@ function describe2(event) {
|
|
|
3798
4010
|
case "registered":
|
|
3799
4011
|
return ` agentsky MCP server registered for ${event.agent}${event.detail ? ` (${event.detail})` : ""}`;
|
|
3800
4012
|
case "planned":
|
|
3801
|
-
return ` would
|
|
4013
|
+
return ` ${event.agent}: would ${event.detail}`;
|
|
3802
4014
|
case "unsupported":
|
|
3803
4015
|
return null;
|
|
3804
4016
|
default:
|
|
3805
4017
|
return ` agentsky MCP server not registered for ${event.agent}: ${event.detail ?? event.outcome}`;
|
|
3806
4018
|
}
|
|
4019
|
+
case "env":
|
|
4020
|
+
switch (event.outcome) {
|
|
4021
|
+
case "written":
|
|
4022
|
+
return ` new shells get ${event.variable} for this login (open a new terminal, or run: . ${event.files[0]})`;
|
|
4023
|
+
case "planned":
|
|
4024
|
+
return ` would write ${event.variable} to ${event.files.join(", ")}`;
|
|
4025
|
+
case "cleared":
|
|
4026
|
+
return null;
|
|
4027
|
+
default:
|
|
4028
|
+
return ` ${event.variable} not set for new shells: ${event.detail ?? event.outcome}`;
|
|
4029
|
+
}
|
|
3807
4030
|
case "plan":
|
|
3808
4031
|
return ` ${event.provider} plan: ${event.outcome === "already" ? "already connected" : event.outcome}${event.detail ? ` (${event.detail})` : ""}`;
|
|
3809
4032
|
case "found": {
|
|
@@ -3893,8 +4116,11 @@ async function runSetup(opts) {
|
|
|
3893
4116
|
};
|
|
3894
4117
|
const agreed = async (question) => !interactive || confirm3(question);
|
|
3895
4118
|
const has = (step) => steps.includes(step);
|
|
3896
|
-
const
|
|
3897
|
-
|
|
4119
|
+
const named = opts.agents?.filter((id) => id !== "hermes");
|
|
4120
|
+
const agents = opts.agents?.length && !named?.length ? [] : selectLocalAgents(named);
|
|
4121
|
+
const hermes = !opts.agents?.length || opts.agents.includes("hermes") ? detectHermes() : null;
|
|
4122
|
+
const mcpOnly = hermes ? [hermes] : [];
|
|
4123
|
+
emit({ type: "agents", agents: [...agents, ...mcpOnly].map((agent2) => ({ id: agent2.id, name: agent2.name })) });
|
|
3898
4124
|
const needsToken = !opts.dryRun && (has("skill") || has("logins") || has("secrets") || has("import") && !opts.key);
|
|
3899
4125
|
if (needsToken && !apiToken()) {
|
|
3900
4126
|
if (opts.json) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
|
|
@@ -3916,10 +4142,14 @@ async function runSetup(opts) {
|
|
|
3916
4142
|
held = new Map((claimed.body.skills ?? []).map((skill) => [skill.name, skill.fingerprint]));
|
|
3917
4143
|
};
|
|
3918
4144
|
await tell();
|
|
3919
|
-
if (has("skill") && agents.length && await agreed("Add the AgentSky skill and MCP server to these agents, so they can hand work to others?")) {
|
|
4145
|
+
if (has("skill") && (agents.length || mcpOnly.length) && await agreed("Add the AgentSky skill and MCP server to these agents, so they can hand work to others?")) {
|
|
3920
4146
|
const { catalog: catalog2, detail } = await readCatalog();
|
|
3921
4147
|
emit(catalog2 ? { type: "catalog", outcome: "read", agents: catalog2.length } : { type: "catalog", outcome: "unavailable", ...detail ? { detail } : {} });
|
|
3922
4148
|
const token = apiToken();
|
|
4149
|
+
const registerMcp = (client) => {
|
|
4150
|
+
const registered = token ? registerMcpServer(client, { token, mcpUrl: mcpUrlFor(apiBase()), dryRun: opts.dryRun }) : { agent: client.id, outcome: "skipped", detail: "not logged in" };
|
|
4151
|
+
emit({ type: "mcp", agent: client.id, outcome: registered.outcome, ...registered.detail ? { detail: registered.detail } : {} });
|
|
4152
|
+
};
|
|
3923
4153
|
for (const agent2 of agents) {
|
|
3924
4154
|
try {
|
|
3925
4155
|
const installed = installSkill(agent2, { dryRun: opts.dryRun, catalog: catalog2 });
|
|
@@ -3931,8 +4161,12 @@ async function runSetup(opts) {
|
|
|
3931
4161
|
run2.agents.push({ id: agent2.id, skill: "failed" });
|
|
3932
4162
|
continue;
|
|
3933
4163
|
}
|
|
3934
|
-
|
|
3935
|
-
|
|
4164
|
+
registerMcp(agent2);
|
|
4165
|
+
}
|
|
4166
|
+
for (const client of mcpOnly) registerMcp(client);
|
|
4167
|
+
if (token) {
|
|
4168
|
+
const shell = writeShellEnv(token, { dryRun: opts.dryRun });
|
|
4169
|
+
emit({ type: "env", variable: TOKEN_ENV, outcome: shell.outcome, files: shell.files, ...shell.detail ? { detail: shell.detail } : {} });
|
|
3936
4170
|
}
|
|
3937
4171
|
await tell();
|
|
3938
4172
|
}
|
|
@@ -4119,17 +4353,17 @@ function renderArtifact(artifact2) {
|
|
|
4119
4353
|
}
|
|
4120
4354
|
|
|
4121
4355
|
// src/skills-store.ts
|
|
4122
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
4123
|
-
import { basename as
|
|
4356
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync15, statSync as statSync8 } from "fs";
|
|
4357
|
+
import { basename as basename8, join as join16, relative as relative4, resolve as resolve7 } from "path";
|
|
4124
4358
|
function skillBundle(dir) {
|
|
4125
4359
|
const root = resolve7(dir);
|
|
4126
|
-
const top =
|
|
4360
|
+
const top = basename8(root);
|
|
4127
4361
|
const files2 = [];
|
|
4128
4362
|
const walk = (path) => {
|
|
4129
4363
|
for (const name of readdirSync7(path).sort()) {
|
|
4130
|
-
const full =
|
|
4364
|
+
const full = join16(path, name);
|
|
4131
4365
|
if (statSync8(full).isDirectory()) walk(full);
|
|
4132
|
-
else files2.push({ path: `${top}/${relative4(root, full)}`, bytes:
|
|
4366
|
+
else files2.push({ path: `${top}/${relative4(root, full)}`, bytes: readFileSync15(full) });
|
|
4133
4367
|
}
|
|
4134
4368
|
};
|
|
4135
4369
|
walk(root);
|
|
@@ -4139,7 +4373,7 @@ function skillBundle(dir) {
|
|
|
4139
4373
|
async function uploadSkill(dir, displayName) {
|
|
4140
4374
|
const form = new FormData();
|
|
4141
4375
|
for (const file of skillBundle(dir)) form.append("files", new Blob([new Uint8Array(file.bytes)]), file.path);
|
|
4142
|
-
form.append("displayName", displayName ??
|
|
4376
|
+
form.append("displayName", displayName ?? basename8(resolve7(dir)));
|
|
4143
4377
|
return apiForm("POST", "/skills", form);
|
|
4144
4378
|
}
|
|
4145
4379
|
async function listSkills() {
|
|
@@ -4168,8 +4402,8 @@ async function getSkillUsage(since) {
|
|
|
4168
4402
|
|
|
4169
4403
|
// src/task-export.ts
|
|
4170
4404
|
import { createHash as createHash6 } from "crypto";
|
|
4171
|
-
import { mkdirSync as
|
|
4172
|
-
import { dirname as
|
|
4405
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
4406
|
+
import { dirname as dirname8, join as join17 } from "path";
|
|
4173
4407
|
import { stringify as stringifyToml2 } from "smol-toml";
|
|
4174
4408
|
var TEXT_MAX = 2e4;
|
|
4175
4409
|
function scrub(text, names2) {
|
|
@@ -4457,14 +4691,14 @@ async function exportTaskHarbor(taskRef, outDir, opts) {
|
|
|
4457
4691
|
fileBytes.set(file.path, bytes);
|
|
4458
4692
|
}
|
|
4459
4693
|
const files2 = buildHarborTask(record, { ...opts, fileBytes });
|
|
4460
|
-
const
|
|
4694
|
+
const written2 = [];
|
|
4461
4695
|
for (const [path, content] of files2) {
|
|
4462
|
-
const target =
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4696
|
+
const target = join17(outDir, path);
|
|
4697
|
+
mkdirSync5(dirname8(target), { recursive: true });
|
|
4698
|
+
writeFileSync8(target, content, typeof content === "string" ? { mode: path.endsWith(".sh") || path.endsWith(".py") ? 493 : 420 } : void 0);
|
|
4699
|
+
written2.push(path);
|
|
4466
4700
|
}
|
|
4467
|
-
return { written, record };
|
|
4701
|
+
return { written: written2, record };
|
|
4468
4702
|
}
|
|
4469
4703
|
function parseRedactNames(values) {
|
|
4470
4704
|
return (values ?? []).map((value) => {
|
|
@@ -4773,6 +5007,44 @@ function renderBrowserProfile(profile) {
|
|
|
4773
5007
|
return lines.join("\n");
|
|
4774
5008
|
}
|
|
4775
5009
|
|
|
5010
|
+
// src/mac.ts
|
|
5011
|
+
var REACH_KINDS = ["folder", "browser", "screen", "host"];
|
|
5012
|
+
var OPEN = /* @__PURE__ */ new Set(["pending", "asking"]);
|
|
5013
|
+
var MAX_WAIT_SECONDS = 25;
|
|
5014
|
+
async function listMacs() {
|
|
5015
|
+
return (await api("GET", "/desktop/machines")).machines;
|
|
5016
|
+
}
|
|
5017
|
+
async function askMac(machine, kind, reason) {
|
|
5018
|
+
if (!REACH_KINDS.includes(kind)) throw new Error(`kind must be one of ${REACH_KINDS.join(", ")}`);
|
|
5019
|
+
return api("POST", `/desktop/machines/${encodeURIComponent(machine)}/reach-requests`, { kind, reason });
|
|
5020
|
+
}
|
|
5021
|
+
async function readReachRequest(id, waitSeconds = 0) {
|
|
5022
|
+
const wait = Math.min(Math.max(Math.floor(waitSeconds), 0), MAX_WAIT_SECONDS);
|
|
5023
|
+
return (await api("GET", `/desktop/reach-requests/${encodeURIComponent(id)}${wait ? `?wait=${wait}` : ""}`)).reach_request;
|
|
5024
|
+
}
|
|
5025
|
+
async function waitForAnswer(id, budgetSeconds) {
|
|
5026
|
+
const until = Date.now() + budgetSeconds * 1e3;
|
|
5027
|
+
for (; ; ) {
|
|
5028
|
+
const left = Math.ceil((until - Date.now()) / 1e3);
|
|
5029
|
+
const request = await readReachRequest(id, Math.max(left, 0));
|
|
5030
|
+
if (!OPEN.has(request.status) || left <= 0) return request;
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
async function takeBack(mount) {
|
|
5034
|
+
await api("DELETE", `/desktop/mounts/${encodeURIComponent(mount)}`);
|
|
5035
|
+
}
|
|
5036
|
+
function renderMac(mac2) {
|
|
5037
|
+
const lends = mac2.reach ? mac2.reach.kinds.join(", ") || "nothing" : "unknown (update the Mac app)";
|
|
5038
|
+
const screen = mac2.reach && !mac2.reach.kinds.includes("screen") ? ` screen: ${mac2.reach.screen}` : "";
|
|
5039
|
+
const lines = [`${mac2.id} ${mac2.name} ${mac2.online ? "online" : "offline"} can lend: ${lends}${screen}`];
|
|
5040
|
+
for (const mount of mac2.mounts ?? []) lines.push(` ${mount.id} ${mount.kind.padEnd(7)} ${mount.root ?? mount.label}`);
|
|
5041
|
+
return lines.join("\n");
|
|
5042
|
+
}
|
|
5043
|
+
function renderReachRequest(request) {
|
|
5044
|
+
if (request.status === "granted" && request.mount) return `${request.id} granted ${request.mount.id} (${request.mount.kind}${request.mount.root ? ` ${request.mount.root}` : ""})`;
|
|
5045
|
+
return `${request.id} ${request.status}${request.error ? ` ${request.error.code}: ${request.error.message}` : ""}`;
|
|
5046
|
+
}
|
|
5047
|
+
|
|
4776
5048
|
// src/compare.ts
|
|
4777
5049
|
async function listComparisons(opts = {}) {
|
|
4778
5050
|
const qs = opts.archived ? "?archived=true" : "";
|
|
@@ -4793,6 +5065,53 @@ async function listThreads2(opts = {}) {
|
|
|
4793
5065
|
const { threads } = await api("GET", `/threads${qs}`);
|
|
4794
5066
|
return threads;
|
|
4795
5067
|
}
|
|
5068
|
+
async function startThread(body) {
|
|
5069
|
+
return api("POST", "/threads", body);
|
|
5070
|
+
}
|
|
5071
|
+
var threadPath = (id) => `/threads/${encodeURIComponent(id)}`;
|
|
5072
|
+
async function getThread(id) {
|
|
5073
|
+
return api("GET", threadPath(id));
|
|
5074
|
+
}
|
|
5075
|
+
async function organizeThread(id, change) {
|
|
5076
|
+
return api("PATCH", threadPath(id), change);
|
|
5077
|
+
}
|
|
5078
|
+
async function deleteThread(id) {
|
|
5079
|
+
return api("DELETE", threadPath(id));
|
|
5080
|
+
}
|
|
5081
|
+
async function sendThreadMessage(id, body) {
|
|
5082
|
+
return api("POST", `${threadPath(id)}/messages`, body);
|
|
5083
|
+
}
|
|
5084
|
+
async function addThreadAgent(id, body) {
|
|
5085
|
+
return api("POST", `${threadPath(id)}/agents`, body);
|
|
5086
|
+
}
|
|
5087
|
+
async function removeThreadAgent(id, columnId) {
|
|
5088
|
+
return api("DELETE", `${threadPath(id)}/agents/${encodeURIComponent(columnId)}`);
|
|
5089
|
+
}
|
|
5090
|
+
async function interruptThread(id, columnId) {
|
|
5091
|
+
return api("POST", `${threadPath(id)}/interrupt`, columnId ? { columnId } : {});
|
|
5092
|
+
}
|
|
5093
|
+
async function forkThread(id, body) {
|
|
5094
|
+
return api("POST", `${threadPath(id)}/fork`, body);
|
|
5095
|
+
}
|
|
5096
|
+
async function changeThreadModel(id, change, columnId) {
|
|
5097
|
+
return api("PATCH", `${threadPath(id)}/engine${columnId ? `?column=${encodeURIComponent(columnId)}` : ""}`, change);
|
|
5098
|
+
}
|
|
5099
|
+
async function searchThreads(q, opts = {}) {
|
|
5100
|
+
const params = new URLSearchParams({ q, ...opts.threads ? { threads: opts.threads } : {}, ...opts.cursor ? { cursor: opts.cursor } : {} });
|
|
5101
|
+
return api("GET", `/threads/search?${params}`);
|
|
5102
|
+
}
|
|
5103
|
+
async function threadOptions() {
|
|
5104
|
+
return api("GET", "/threads/options");
|
|
5105
|
+
}
|
|
5106
|
+
async function joinThread(sessionId) {
|
|
5107
|
+
return api("POST", "/threads/joinable", { sessionId });
|
|
5108
|
+
}
|
|
5109
|
+
async function listProjects() {
|
|
5110
|
+
return api("GET", "/projects");
|
|
5111
|
+
}
|
|
5112
|
+
async function rememberProject(project2) {
|
|
5113
|
+
return api("PUT", "/projects", { project: project2 });
|
|
5114
|
+
}
|
|
4796
5115
|
function renderComparisonSummary(c) {
|
|
4797
5116
|
const agents = c.agents.map((a) => `${a.agentType}/${a.llm}`).join(" vs ");
|
|
4798
5117
|
const status = c.statusLine ?? c.attention ?? (c.archived ? "archived" : "active");
|
|
@@ -4805,20 +5124,129 @@ function renderThreadSummary(t) {
|
|
|
4805
5124
|
return `${t.id.slice(0, 12)} ${flag}${t.title.slice(0, 40).padEnd(44)} [${agents}] ${status}`;
|
|
4806
5125
|
}
|
|
4807
5126
|
|
|
5127
|
+
// src/library.ts
|
|
5128
|
+
var artifactPath = (id) => `/library/artifacts/${encodeURIComponent(id)}`;
|
|
5129
|
+
async function listLibrary(query = {}) {
|
|
5130
|
+
const params = new URLSearchParams(Object.entries(query).filter((entry2) => Boolean(entry2[1])));
|
|
5131
|
+
const qs = params.size ? `?${params}` : "";
|
|
5132
|
+
return api("GET", `/library/artifacts${qs}`);
|
|
5133
|
+
}
|
|
5134
|
+
async function uploadToLibrary(file, requestId, workspace2) {
|
|
5135
|
+
const form = new FormData();
|
|
5136
|
+
form.append("file", new Blob([new Uint8Array(file.bytes)], { type: file.mediaType }), file.name);
|
|
5137
|
+
form.append("requestId", requestId);
|
|
5138
|
+
if (workspace2) form.append("workspace", workspace2);
|
|
5139
|
+
return apiForm("POST", "/library/artifacts", form);
|
|
5140
|
+
}
|
|
5141
|
+
async function getLibraryArtifact(id) {
|
|
5142
|
+
return api("GET", artifactPath(id));
|
|
5143
|
+
}
|
|
5144
|
+
async function deleteLibraryArtifact(id, revision) {
|
|
5145
|
+
return api("DELETE", artifactPath(id), { revision });
|
|
5146
|
+
}
|
|
5147
|
+
async function shareLibraryArtifact(id, enabled) {
|
|
5148
|
+
const { currentVersion } = await getLibraryArtifact(id);
|
|
5149
|
+
return api("PATCH", `${artifactPath(id)}/share`, { enabled, revision: currentVersion });
|
|
5150
|
+
}
|
|
5151
|
+
async function placeLibraryArtifact(id, workspace2) {
|
|
5152
|
+
return api("PATCH", `${artifactPath(id)}/workspace`, { workspace: workspace2 });
|
|
5153
|
+
}
|
|
5154
|
+
async function downloadLibraryArtifact(id, version) {
|
|
5155
|
+
return apiBytes("GET", `${artifactPath(id)}/download${version ? `?version=${version}` : ""}`);
|
|
5156
|
+
}
|
|
5157
|
+
function renderLibraryArtifact(artifact2) {
|
|
5158
|
+
const version = artifact2.currentVersion ? `v${artifact2.currentVersion}` : "";
|
|
5159
|
+
return `${artifact2.id} ${artifact2.name} ${version}${artifact2.shared ? " shared" : ""}`;
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
// src/billing-plans.ts
|
|
5163
|
+
var readCodingPlan = () => api("GET", "/billing/coding-plan");
|
|
5164
|
+
function joinCodingPlan(dollars2, requestId) {
|
|
5165
|
+
const amount = Number(dollars2.trim().replace(/^\$/, ""));
|
|
5166
|
+
if (!Number.isInteger(amount) || amount <= 0) throw new Error(`<dollars> must be whole dollars, got ${JSON.stringify(dollars2)}`);
|
|
5167
|
+
return api("POST", "/billing/coding-plan", { amount_usd: amount, request_id: requestId });
|
|
5168
|
+
}
|
|
5169
|
+
var cancelCodingPlan = () => api("DELETE", "/billing/coding-plan");
|
|
5170
|
+
function renderCodingPlan(plan) {
|
|
5171
|
+
if (!plan.active) {
|
|
5172
|
+
const lines2 = [`coding plan: not a member (enrollment ${plan.enrollment}; monthly amounts $${plan.amounts_usd.join(", $")})`];
|
|
5173
|
+
if (plan.charge_failure) lines2.push(` last charge failed: ${plan.charge_failure}`);
|
|
5174
|
+
return lines2.join("\n");
|
|
5175
|
+
}
|
|
5176
|
+
const until = plan.current_period_end?.slice(0, 10) ?? "?";
|
|
5177
|
+
const lines = [`coding plan: $${plan.monthly_credit_usd}/month, paid until ${until}, ${plan.renews ? "renews" : "does not renew"}`];
|
|
5178
|
+
if (plan.charge_failure) lines.push(` last charge failed: ${plan.charge_failure}`);
|
|
5179
|
+
return lines.join("\n");
|
|
5180
|
+
}
|
|
5181
|
+
function readRequestUsage(opts) {
|
|
5182
|
+
const query = usageQuery(opts);
|
|
5183
|
+
const cursor = opts.cursor ? `${query ? "&" : "?"}cursor=${encodeURIComponent(opts.cursor)}` : "";
|
|
5184
|
+
return api("GET", `/billing/usage/requests${query}${cursor}`);
|
|
5185
|
+
}
|
|
5186
|
+
function renderRequestUsage(page) {
|
|
5187
|
+
const lines = page.rows.map((row) => `${row.at.slice(0, 19).replace("T", " ")} ${(row.apiKeyName ?? "").padEnd(16).slice(0, 16)} ${(row.model ?? "-").padEnd(28).slice(0, 28)} ${String(row.httpStatus ?? "-").padStart(3)} ${String(row.inputTokens ?? 0).padStart(9)} in ${String(row.outputTokens ?? 0).padStart(8)} out ${`$${(row.billedCostUsd ?? 0).toFixed(6)}`.padStart(11)}`);
|
|
5188
|
+
if (!lines.length) lines.push("no API requests in this range");
|
|
5189
|
+
if (page.nextCursor) lines.push(`more: --cursor ${page.nextCursor}`);
|
|
5190
|
+
return lines.join("\n");
|
|
5191
|
+
}
|
|
5192
|
+
var readReferralLinks = () => api("GET", "/billing/referral-links");
|
|
5193
|
+
function referralLinkBody(opts) {
|
|
5194
|
+
if (opts.price === void 0 === !opts.sku?.length) throw new Error("pass --price for every model, or --sku id=multiplier for each SKU, not both");
|
|
5195
|
+
const body = {};
|
|
5196
|
+
if (opts.price !== void 0) body.customer_multiplier = multiplier("--price", opts.price);
|
|
5197
|
+
if (opts.sku?.length) {
|
|
5198
|
+
body.sku_prices = opts.sku.map((pair) => {
|
|
5199
|
+
const at = pair.lastIndexOf("=");
|
|
5200
|
+
if (at <= 0) throw new Error(`--sku must be id=multiplier, got ${JSON.stringify(pair)}`);
|
|
5201
|
+
return { sku_id: pair.slice(0, at), multiplier: multiplier("--sku", pair.slice(at + 1)) };
|
|
5202
|
+
});
|
|
5203
|
+
}
|
|
5204
|
+
if (opts.email) body.audience_email = opts.email;
|
|
5205
|
+
if (opts.uses) body.max_redemptions = wholeNumber2("--uses", opts.uses);
|
|
5206
|
+
if (opts.days) body.expires_in_days = wholeNumber2("--days", opts.days);
|
|
5207
|
+
return body;
|
|
5208
|
+
}
|
|
5209
|
+
var createReferralLink = (opts) => api("POST", "/billing/referral-links", referralLinkBody(opts));
|
|
5210
|
+
var disableReferralLink = (id) => api("POST", `/billing/referral-links/${encodeURIComponent(id)}/disable`);
|
|
5211
|
+
function renderReferralLinks(dashboard) {
|
|
5212
|
+
const lines = [`priced from ${dashboard.source_plan.name}: customers pay at least ${Math.round(dashboard.source_plan.multiplier * 100)}% of public price`];
|
|
5213
|
+
for (const link2 of dashboard.links) {
|
|
5214
|
+
const state = link2.disabled_at ? "disabled" : `expires ${link2.expires_at.slice(0, 10)}`;
|
|
5215
|
+
const audience = link2.audience_email ? ` for ${link2.audience_email} (${link2.email_status})` : "";
|
|
5216
|
+
lines.push(` ${link2.id} ${Math.round(link2.customer_multiplier * 100)}% ${link2.redeemed_count}/${link2.max_redemptions} used ${state}${audience}`);
|
|
5217
|
+
lines.push(` ${link2.url}`);
|
|
5218
|
+
}
|
|
5219
|
+
if (!dashboard.links.length) lines.push(" no links yet");
|
|
5220
|
+
const earned = dashboard.earnings.reduce((sum, earning) => sum + earning.amount_usd, 0);
|
|
5221
|
+
lines.push(`earned $${earned.toFixed(2)} from ${dashboard.earnings.length} customer${dashboard.earnings.length === 1 ? "" : "s"}`);
|
|
5222
|
+
return lines.join("\n");
|
|
5223
|
+
}
|
|
5224
|
+
function multiplier(flag, value) {
|
|
5225
|
+
const trimmed = value.trim();
|
|
5226
|
+
const percent = trimmed.endsWith("%");
|
|
5227
|
+
const number = Number(percent ? trimmed.slice(0, -1) : trimmed);
|
|
5228
|
+
if (!Number.isFinite(number) || number <= 0) throw new Error(`${flag} must be a share of public price like 0.8 or 80%, got ${JSON.stringify(value)}`);
|
|
5229
|
+
return percent ? number / 100 : number;
|
|
5230
|
+
}
|
|
5231
|
+
function wholeNumber2(flag, value) {
|
|
5232
|
+
if (!/^\d+$/.test(value.trim())) throw new Error(`${flag} must be a whole number, got ${JSON.stringify(value)}`);
|
|
5233
|
+
return Number(value.trim());
|
|
5234
|
+
}
|
|
5235
|
+
|
|
4808
5236
|
// src/harness.ts
|
|
4809
|
-
import { existsSync as
|
|
4810
|
-
import { dirname as
|
|
5237
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
|
|
5238
|
+
import { dirname as dirname9, join as join18, resolve as resolve8 } from "path";
|
|
4811
5239
|
var HARNESS_FILE = "harness.json";
|
|
4812
5240
|
function harnessFilePath(dir) {
|
|
4813
|
-
return
|
|
5241
|
+
return join18(resolve8(dir), HARNESS_FILE);
|
|
4814
5242
|
}
|
|
4815
5243
|
function readHarnessFile(dir) {
|
|
4816
5244
|
const path = harnessFilePath(dir);
|
|
4817
|
-
if (!
|
|
5245
|
+
if (!existsSync11(path)) {
|
|
4818
5246
|
throw new Error(`no ${HARNESS_FILE} found in ${resolve8(dir)} \u2014 run \`sky harness init\` first`);
|
|
4819
5247
|
}
|
|
4820
5248
|
try {
|
|
4821
|
-
return JSON.parse(
|
|
5249
|
+
return JSON.parse(readFileSync16(path, "utf8"));
|
|
4822
5250
|
} catch (err) {
|
|
4823
5251
|
throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4824
5252
|
}
|
|
@@ -4845,11 +5273,11 @@ var INIT_COMMENT = `# harness.json \u2014 workspace harness profile skeleton
|
|
|
4845
5273
|
`;
|
|
4846
5274
|
function harnessInit(dir) {
|
|
4847
5275
|
const path = harnessFilePath(dir);
|
|
4848
|
-
if (
|
|
5276
|
+
if (existsSync11(path)) {
|
|
4849
5277
|
throw new Error(`${path} already exists \u2014 delete it to re-init`);
|
|
4850
5278
|
}
|
|
4851
|
-
|
|
4852
|
-
|
|
5279
|
+
mkdirSync6(dirname9(path), { recursive: true });
|
|
5280
|
+
writeFileSync9(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
|
|
4853
5281
|
`);
|
|
4854
5282
|
console.log(`Created ${path}`);
|
|
4855
5283
|
console.log(INIT_COMMENT);
|
|
@@ -5010,6 +5438,7 @@ auth.command("logout").description("forget the stored token (revoke it in Develo
|
|
|
5010
5438
|
run(async () => {
|
|
5011
5439
|
saveConfig({ ...loadConfig(), token: void 0, email: void 0 });
|
|
5012
5440
|
console.log(`Logged out. (Token revocation: Developer \u2192 API keys.)`);
|
|
5441
|
+
for (const line of syncLocalAccess(void 0)) console.log(line);
|
|
5013
5442
|
})
|
|
5014
5443
|
);
|
|
5015
5444
|
auth.command("whoami").description("who the current token acts as").action(
|
|
@@ -5042,6 +5471,19 @@ auth.command("usage <provider>").description("plan allowance left on a connected
|
|
|
5042
5471
|
auth.command("switch-to-credits <provider>").description("move the agents on a model subscription onto platform credits (do this before disconnecting)").option("--account <id>", "an additional account (default: the provider's first account)").option("-y, --yes", "skip the confirmation prompt").action(run(async (name, opts) => switchToCredits(name, opts)));
|
|
5043
5472
|
auth.command("accounts").description("list connected model subscriptions and the agents they power").action(run(async () => listSubscriptions()));
|
|
5044
5473
|
auth.command("disconnect <provider>").description("disconnect a model subscription (agents on it fail until switched)").option("-y, --yes", "skip the confirmation prompt").action(run(async (name, opts) => disconnect(name, opts)));
|
|
5474
|
+
auth.command("apps").description("the registered apps you let into your account, with the scopes you allowed").action(
|
|
5475
|
+
run(async () => {
|
|
5476
|
+
const grants = await listAppGrants();
|
|
5477
|
+
if (!grants.length) console.log("No apps have access to your account.");
|
|
5478
|
+
for (const grant of grants) console.log(renderAppGrant(grant));
|
|
5479
|
+
})
|
|
5480
|
+
);
|
|
5481
|
+
auth.command("revoke-app <clientId>").description("remove an app's access to your account; its tokens stop working at once (admin scope)").action(
|
|
5482
|
+
run(async (clientId) => {
|
|
5483
|
+
await revokeAppGrant(clientId);
|
|
5484
|
+
console.log(`Removed ${clientId}'s access to your account`);
|
|
5485
|
+
})
|
|
5486
|
+
);
|
|
5045
5487
|
var universe = program.command("universe").description("grouping boundary for agents");
|
|
5046
5488
|
universe.command("list").action(
|
|
5047
5489
|
run(async () => {
|
|
@@ -5088,7 +5530,7 @@ universe.command("invite <emails...>").description("invite people by email; prin
|
|
|
5088
5530
|
run(async (emails, opts) => {
|
|
5089
5531
|
const result = await invite(await workspaceSlug(opts.universe), emails, opts);
|
|
5090
5532
|
for (const i of result.invites) console.log(`invited ${i.email} as ${i.role} ${i.id}`);
|
|
5091
|
-
for (const
|
|
5533
|
+
for (const link2 of result.links) console.log(` ${link2.email} joins with: sky universe join ${link2.token}`);
|
|
5092
5534
|
if (!result.invites.length) console.log("nobody new to invite \u2014 everyone listed is already a member");
|
|
5093
5535
|
})
|
|
5094
5536
|
);
|
|
@@ -5405,12 +5847,12 @@ session.command("share <ref>").description("print the session's share link, turn
|
|
|
5405
5847
|
console.log(`Sharing is off for ${id}`);
|
|
5406
5848
|
return;
|
|
5407
5849
|
}
|
|
5408
|
-
const
|
|
5850
|
+
const link2 = await api("POST", path, {
|
|
5409
5851
|
access: opts.view ? "view" : "prompt",
|
|
5410
5852
|
...opts.rotate ? { rotate: true } : {}
|
|
5411
5853
|
});
|
|
5412
|
-
if (
|
|
5413
|
-
else console.log(`view-only token: ${
|
|
5854
|
+
if (link2.shareUrl) console.log(link2.shareUrl);
|
|
5855
|
+
else console.log(`view-only token: ${link2.token} \u2014 read with GET /links/${link2.token}/events`);
|
|
5414
5856
|
})
|
|
5415
5857
|
);
|
|
5416
5858
|
session.command("usage <ref>").description("what the session has cost so far, and its newest billed entries").option("--limit <n>", "entries to list (default 20)").option("--json", "print the API's answer as JSON").action(
|
|
@@ -5558,7 +6000,7 @@ sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1
|
|
|
5558
6000
|
run(async (ref, path, opts) => {
|
|
5559
6001
|
const bytes = await readFile(await resolveSessionRef(ref), path);
|
|
5560
6002
|
if (opts.out) {
|
|
5561
|
-
|
|
6003
|
+
writeFileSync10(opts.out, bytes);
|
|
5562
6004
|
console.error(`saved ${bytes.length} bytes to ${opts.out}`);
|
|
5563
6005
|
} else process.stdout.write(bytes);
|
|
5564
6006
|
})
|
|
@@ -5575,7 +6017,7 @@ sessionWorkspace.command("patch <ref> [repo]").description("the work in a reposi
|
|
|
5575
6017
|
console.error(`${meta.repo}: ${meta.commits.length} commit${meta.commits.length === 1 ? "" : "s"} since ${meta.base_branch || "the imported copy"} (${meta.base_commit.slice(0, 12)})`);
|
|
5576
6018
|
if (!patch.length) console.error("nothing changed");
|
|
5577
6019
|
else if (opts.out) {
|
|
5578
|
-
|
|
6020
|
+
writeFileSync10(opts.out, patch);
|
|
5579
6021
|
console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
|
|
5580
6022
|
} else process.stdout.write(patch);
|
|
5581
6023
|
})
|
|
@@ -5720,7 +6162,7 @@ catalog.command("search [words...]").description("find an agent for a piece of w
|
|
|
5720
6162
|
Start one: sky session create @<handle> --message "<the job>"`);
|
|
5721
6163
|
})
|
|
5722
6164
|
);
|
|
5723
|
-
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(
|
|
6165
|
+
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 | hermes").option("--dry-run", "say what would be written; write nothing").option("--json", "print the outcome per agent as JSON").action(
|
|
5724
6166
|
run(async (opts) => {
|
|
5725
6167
|
const installs = selectLocalAgents(opts.agent).map((a) => installSkill(a, { dryRun: opts.dryRun }));
|
|
5726
6168
|
if (opts.json) {
|
|
@@ -5736,7 +6178,7 @@ program.command("skill").description("the AgentSky skill for the agents on this
|
|
|
5736
6178
|
})
|
|
5737
6179
|
);
|
|
5738
6180
|
program.command("setup [steps...]").description("bring this machine's agents to AgentSky: the skill and its MCP server, your plans, your skills and instructions, your tool keys").addHelpText("after", `
|
|
5739
|
-
Steps (all when none is named): ${SETUP_STEPS.join(" | ")}`).option("--key <key>", "a setup key from agentsky.dev/setup").option("--agent <id...>", "only these agents: claude_code | codex | cursor").option("--secret <name...>", "save these tool keys to your vault (asked one by one when omitted)").option("--no-claude", "leave the Claude plan alone").option("--plan <name...>", "connect only these plans: claude | chatgpt | cursor").option("--dry-run", "say what would be done; write and send nothing").option("-y, --yes", "do not ask before each step").option("--json", "print progress as one JSON object per line; never asks").action(
|
|
6181
|
+
Steps (all when none is named): ${SETUP_STEPS.join(" | ")}`).option("--key <key>", "a setup key from agentsky.dev/setup").option("--agent <id...>", "only these agents: claude_code | codex | cursor | hermes").option("--secret <name...>", "save these tool keys to your vault (asked one by one when omitted)").option("--no-claude", "leave the Claude plan alone").option("--plan <name...>", "connect only these plans: claude | chatgpt | cursor").option("--dry-run", "say what would be done; write and send nothing").option("-y, --yes", "do not ask before each step").option("--json", "print progress as one JSON object per line; never asks").action(
|
|
5740
6182
|
run(async (steps, opts) => {
|
|
5741
6183
|
const unknown = steps.find((step) => !SETUP_STEPS.includes(step));
|
|
5742
6184
|
if (unknown) throw new Error(`unknown step ${unknown} \u2014 expected ${SETUP_STEPS.join(" | ")}`);
|
|
@@ -5784,8 +6226,8 @@ secrets.command("unset <id> <key>").action(
|
|
|
5784
6226
|
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(
|
|
5785
6227
|
run(async (opts) => {
|
|
5786
6228
|
const path = resolve9(opts.file);
|
|
5787
|
-
if (!
|
|
5788
|
-
const name =
|
|
6229
|
+
if (!existsSync12(path)) {
|
|
6230
|
+
const name = basename9(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
|
|
5789
6231
|
scaffoldAgentFile(path, name);
|
|
5790
6232
|
console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
|
|
5791
6233
|
}
|
|
@@ -6067,8 +6509,8 @@ files.command("rm <id>").description("delete a file (a session that already rece
|
|
|
6067
6509
|
);
|
|
6068
6510
|
files.command("download <id>").description("download a session file's bytes").requiredOption("-o, --out <path>", "where to write the bytes").action(
|
|
6069
6511
|
run(async (id, opts) => {
|
|
6070
|
-
const
|
|
6071
|
-
console.log(`Wrote ${
|
|
6512
|
+
const written2 = await downloadFileTo(id, opts.out);
|
|
6513
|
+
console.log(`Wrote ${written2} bytes to ${opts.out}`);
|
|
6072
6514
|
})
|
|
6073
6515
|
);
|
|
6074
6516
|
var task = program.command("task").description("a task's shared files and open requests (the task is its first session's id)");
|
|
@@ -6083,14 +6525,14 @@ task.command("get <task> <path>").description("read one task file; prints it, or
|
|
|
6083
6525
|
run(async (taskId, path, opts) => {
|
|
6084
6526
|
const bytes = await readTaskFile(taskId, path);
|
|
6085
6527
|
if (opts.out) {
|
|
6086
|
-
|
|
6528
|
+
writeFileSync10(opts.out, bytes);
|
|
6087
6529
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
6088
6530
|
} else process.stdout.write(bytes);
|
|
6089
6531
|
})
|
|
6090
6532
|
);
|
|
6091
6533
|
task.command("put <task> <path> <local>").description("write a local file to <path> in the task's files (every session of the task sees it)").action(
|
|
6092
6534
|
run(async (taskId, path, local) => {
|
|
6093
|
-
const { file, conflict } = await writeTaskFile(taskId, path,
|
|
6535
|
+
const { file, conflict } = await writeTaskFile(taskId, path, readFileSync17(local), mediaTypeFor(local));
|
|
6094
6536
|
console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
|
|
6095
6537
|
})
|
|
6096
6538
|
);
|
|
@@ -6104,9 +6546,9 @@ task.command("export <task>").description("write the task's session record as a
|
|
|
6104
6546
|
run(async (taskId, opts) => {
|
|
6105
6547
|
const tier = opts.tier === "gold" || opts.tier === "exploratory" ? opts.tier : "silver";
|
|
6106
6548
|
const outDir = opts.out ?? `./harbor-${taskId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
6107
|
-
const { written, record } = await exportTaskHarbor(taskId, outDir, { tier, redactNames: parseRedactNames(opts.redact) });
|
|
6108
|
-
console.log(`Wrote ${
|
|
6109
|
-
if (!
|
|
6549
|
+
const { written: written2, record } = await exportTaskHarbor(taskId, outDir, { tier, redactNames: parseRedactNames(opts.redact) });
|
|
6550
|
+
console.log(`Wrote ${written2.length} files to ${outDir} (${record.threads.length} thread${record.threads.length === 1 ? "" : "s"}, ${record.files.length} task file${record.files.length === 1 ? "" : "s"}${record.goal ? ", goal rubric" : ", no goal: derived rubric"})`);
|
|
6551
|
+
if (!written2.includes("environment/Dockerfile")) console.log("environment/: not resettable from the record \u2014 see environment/README.md before running with Harbor");
|
|
6110
6552
|
})
|
|
6111
6553
|
);
|
|
6112
6554
|
task.command("show <task>").description("the task's sessions and the requests waiting on you (questions, approvals, apps to connect)").option("--json", "print the task as JSON").action(
|
|
@@ -6227,6 +6669,56 @@ reflections.command("list").description("the workspace's proposals, newest first
|
|
|
6227
6669
|
);
|
|
6228
6670
|
reflections.command("approve <id>").description("write the proposal through the skill or memory store").action(run(async (id) => decideReflection(id, "approve")));
|
|
6229
6671
|
reflections.command("reject <id>").description("decline the proposal; nothing is written").action(run(async (id) => decideReflection(id, "reject")));
|
|
6672
|
+
var library = program.command("library").description("your artifact Library on agentsky.dev: files and sites your agents saved or you uploaded");
|
|
6673
|
+
library.command("list").description("your Library, newest first").option("--search <text>", "only names that match").option("--cursor <cursor>", "the next page").option("--json", "print as JSON").action(
|
|
6674
|
+
run(async (opts) => {
|
|
6675
|
+
const page = await listLibrary({ q: opts.search, cursor: opts.cursor });
|
|
6676
|
+
if (opts.json) {
|
|
6677
|
+
console.log(JSON.stringify(page, null, 2));
|
|
6678
|
+
return;
|
|
6679
|
+
}
|
|
6680
|
+
if (!page.artifacts.length) console.log("no artifacts");
|
|
6681
|
+
for (const a of page.artifacts) console.log(renderLibraryArtifact(a));
|
|
6682
|
+
if (page.nextCursor) console.log(`more: --cursor ${page.nextCursor}`);
|
|
6683
|
+
})
|
|
6684
|
+
);
|
|
6685
|
+
library.command("upload <path>").description("save a file from this computer to your Library").option("--workspace <key>", "place it in this workspace").action(
|
|
6686
|
+
run(async (path, opts) => {
|
|
6687
|
+
const bytes = readFileSync17(path);
|
|
6688
|
+
const { artifact: artifact2 } = await uploadToLibrary({ bytes, name: basename9(path), mediaType: "application/octet-stream" }, `sky-upload-${crypto.randomUUID()}`, opts.workspace);
|
|
6689
|
+
console.log(renderLibraryArtifact(artifact2));
|
|
6690
|
+
})
|
|
6691
|
+
);
|
|
6692
|
+
library.command("show <id>").description("one artifact and its saved versions").action(run(async (id) => {
|
|
6693
|
+
console.log(JSON.stringify(await getLibraryArtifact(id), null, 2));
|
|
6694
|
+
}));
|
|
6695
|
+
library.command("delete <id>").description("delete your artifact with all its versions").action(
|
|
6696
|
+
run(async (id) => {
|
|
6697
|
+
const { currentVersion } = await getLibraryArtifact(id);
|
|
6698
|
+
await deleteLibraryArtifact(id, currentVersion ?? 0);
|
|
6699
|
+
console.log(`deleted ${id}`);
|
|
6700
|
+
})
|
|
6701
|
+
);
|
|
6702
|
+
for (const [verb, enabled, description] of [
|
|
6703
|
+
["share", true, "turn on the artifact's public link"],
|
|
6704
|
+
["unshare", false, "turn off the artifact's public link"]
|
|
6705
|
+
]) {
|
|
6706
|
+
library.command(`${verb} <id>`).description(description).action(run(async (id) => {
|
|
6707
|
+
const state = await shareLibraryArtifact(id, enabled);
|
|
6708
|
+
console.log(state.url ?? `${verb}: ${id}`);
|
|
6709
|
+
}));
|
|
6710
|
+
}
|
|
6711
|
+
library.command("place <id> <workspace>").description('place your artifact in a workspace, or "me" to take it back to you alone').action(run(async (id, workspace2) => {
|
|
6712
|
+
await placeLibraryArtifact(id, workspace2 === "me" ? null : workspace2);
|
|
6713
|
+
console.log(`placed ${id}`);
|
|
6714
|
+
}));
|
|
6715
|
+
library.command("download <id> <out>").description("save one version's bytes to a file").option("--version <n>", "a saved version (the current one when omitted)").action(
|
|
6716
|
+
run(async (id, out, opts) => {
|
|
6717
|
+
const { bytes } = await downloadLibraryArtifact(id, opts.version ? Number(opts.version) : void 0);
|
|
6718
|
+
writeFileSync10(out, bytes);
|
|
6719
|
+
console.log(`wrote ${bytes.length} bytes to ${out}`);
|
|
6720
|
+
})
|
|
6721
|
+
);
|
|
6230
6722
|
var artifact = program.command("artifact").description("artifacts: publish a task's files as a named, shareable result");
|
|
6231
6723
|
artifact.command("list").description("the workspace's artifacts, or one task's with --task").option("--task <task>", "only this task's artifacts").action(
|
|
6232
6724
|
run(async (opts) => {
|
|
@@ -6244,7 +6736,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
|
|
|
6244
6736
|
run(async (id, path, opts) => {
|
|
6245
6737
|
const bytes = await readArtifactFile(id, path);
|
|
6246
6738
|
if (opts.out) {
|
|
6247
|
-
|
|
6739
|
+
writeFileSync10(opts.out, bytes);
|
|
6248
6740
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
6249
6741
|
} else process.stdout.write(bytes);
|
|
6250
6742
|
})
|
|
@@ -6494,6 +6986,19 @@ program.command("config").description("show the CLI config location and current
|
|
|
6494
6986
|
console.log(` token: ${config.token ? `${config.token.slice(0, 10)}\u2026` : "(none)"}`);
|
|
6495
6987
|
console.log(` universe: ${config.currentUniverse ?? "(default)"}`);
|
|
6496
6988
|
});
|
|
6989
|
+
var link = program.command("link").description("a session someone shared with you by a guest link: send to it and stop it");
|
|
6990
|
+
link.command("send <link> <text...>").description("send a message through a guest link (/s/<token> URL or token); it runs and bills as the session's owner").option("--idempotency-key <key>", "repeat a send without sending twice (default: a new key)").action(
|
|
6991
|
+
run(async (ref, words, opts) => {
|
|
6992
|
+
await sendViaLink(ref, words.join(" "), opts.idempotencyKey ?? `sky-${crypto.randomUUID()}`);
|
|
6993
|
+
console.log("Sent. Read the reply with GET /links/<token>/events.");
|
|
6994
|
+
})
|
|
6995
|
+
);
|
|
6996
|
+
link.command("stop <link>").description("stop the turn running on the session a guest link opens").action(
|
|
6997
|
+
run(async (ref) => {
|
|
6998
|
+
await stopViaLink(ref);
|
|
6999
|
+
console.log("Stopped.");
|
|
7000
|
+
})
|
|
7001
|
+
);
|
|
6497
7002
|
var billing = program.command("billing").description("your balance, auto top-up, usage across sessions and API keys, and buying credit");
|
|
6498
7003
|
billing.command("show").description("balance, whether a card is saved, and auto top-up").action(
|
|
6499
7004
|
run(async () => {
|
|
@@ -6520,6 +7025,11 @@ billing.command("topup <dollars>").description("buy credit with your saved card:
|
|
|
6520
7025
|
console.log(renderTopup(result));
|
|
6521
7026
|
})
|
|
6522
7027
|
);
|
|
7028
|
+
billing.command("claim <link>").description("claim a credit invite link (agentsky.dev/i/<code>, or just the code) for your own balance").action(
|
|
7029
|
+
run(async (link2) => {
|
|
7030
|
+
console.log(renderCreditInviteClaim(await claimCreditInvite(link2)));
|
|
7031
|
+
})
|
|
7032
|
+
);
|
|
6523
7033
|
billing.command("auto-topup").description("show auto top-up, or change it: --on/--off, --threshold <5|10|20>, --amount <10|20|50|100>").option("--on", "charge the saved card when the balance falls under the threshold").option("--off", "stop charging automatically").option("--threshold <dollars>", "the balance that triggers a charge").option("--amount <dollars>", "the credit each charge adds").action(
|
|
6524
7034
|
run(async (opts) => {
|
|
6525
7035
|
const body = autoTopupBody(opts);
|
|
@@ -6542,13 +7052,42 @@ usageOptions(
|
|
|
6542
7052
|
run(async (opts) => {
|
|
6543
7053
|
const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
|
|
6544
7054
|
if (opts.out) {
|
|
6545
|
-
|
|
7055
|
+
writeFileSync10(opts.out, bytes);
|
|
6546
7056
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
6547
7057
|
} else {
|
|
6548
7058
|
process.stdout.write(bytes);
|
|
6549
7059
|
}
|
|
6550
7060
|
})
|
|
6551
7061
|
);
|
|
7062
|
+
billing.command("requests").description("one row per API request your developer API keys made, newest first, a page at a time").option("--from <day>", "first UTC day, YYYY-MM-DD (default: 29 days ago)").option("--to <day>", "first UTC day not included (default: tomorrow)").option("--key <id>", "only this API key").option("--cursor <cursor>", "the next page, as printed after the previous one").option("--json", "print the API's answer as JSON").action(
|
|
7063
|
+
run(async (opts) => {
|
|
7064
|
+
const page = await readRequestUsage(opts);
|
|
7065
|
+
console.log(opts.json ? JSON.stringify(page, null, 2) : renderRequestUsage(page));
|
|
7066
|
+
})
|
|
7067
|
+
);
|
|
7068
|
+
var codingPlan = billing.command("coding-plan").description("the coding plan: monthly credit on your saved card, API gateway calls billed at cost");
|
|
7069
|
+
codingPlan.command("show").description("whether you are a member, the monthly amount, and whether it renews").action(run(async () => console.log(renderCodingPlan(await readCodingPlan()))));
|
|
7070
|
+
codingPlan.command("join <dollars>").description("join with your saved card (charged now), or change your monthly amount from the next renewal").option("--request-id <id>", "repeat a request without charging twice (default: a new id)").action(
|
|
7071
|
+
run(async (dollars2, opts) => {
|
|
7072
|
+
console.log(renderCodingPlan(await joinCodingPlan(dollars2, opts.requestId ?? `sky-${crypto.randomUUID()}`)));
|
|
7073
|
+
})
|
|
7074
|
+
);
|
|
7075
|
+
codingPlan.command("cancel").description("stop the plan renewing; the paid month stays").action(run(async () => console.log(renderCodingPlan(await cancelCodingPlan()))));
|
|
7076
|
+
var referralLinks = billing.command("referral-links").description("customer discount links for the API gateway (accounts on a discount plan)");
|
|
7077
|
+
referralLinks.command("list").description("your links, their use, and what referred customers earned you").action(run(async () => console.log(renderReferralLinks(await readReferralLinks()))));
|
|
7078
|
+
referralLinks.command("create").description("create a link priced at no less than your own price; --email sends it to one customer").option("--price <share>", "what the customer pays for every model, as a share of public price: 0.8 or 80%").option("--sku <id=share>", "a price for one SKU; repeat for several", collect).option("--email <address>", "email the link to this customer; it then redeems once").option("--uses <n>", "how many customers can redeem it (default 100)").option("--days <n>", "days until it expires (default 30)").action(
|
|
7079
|
+
run(async (opts) => {
|
|
7080
|
+
const { url, email_status: emailStatus } = await createReferralLink(opts);
|
|
7081
|
+
console.log(emailStatus ? `${url}
|
|
7082
|
+
email: ${emailStatus}` : url);
|
|
7083
|
+
})
|
|
7084
|
+
);
|
|
7085
|
+
referralLinks.command("disable <id>").description("stop a link being redeemed; customers already enrolled keep their price").action(
|
|
7086
|
+
run(async (id) => {
|
|
7087
|
+
await disableReferralLink(id);
|
|
7088
|
+
console.log(`Disabled ${id}`);
|
|
7089
|
+
})
|
|
7090
|
+
);
|
|
6552
7091
|
var keys = program.command("keys").description("your own API keys: list, create, revoke, restore (admin scope)");
|
|
6553
7092
|
keys.command("list").action(
|
|
6554
7093
|
run(async () => {
|
|
@@ -6869,6 +7408,48 @@ vendor.command("usage <id>").description("the agents and tasks that used this ve
|
|
|
6869
7408
|
for (const t of tasks) console.log(` task ${t.session_id} ${t.title ?? "(untitled)"} ${t.last_used_at.slice(0, 10)}`);
|
|
6870
7409
|
})
|
|
6871
7410
|
);
|
|
7411
|
+
var mac = program.command("mac").description("your Macs: what each lends to agents now, and asking one for a folder, a browser window, its screen or its apps and files");
|
|
7412
|
+
mac.command("list").description("your Macs: online or not, what each can lend, and the folders, browser windows and screens lent now").option("--json", "print as JSON").action(
|
|
7413
|
+
run(async (opts) => {
|
|
7414
|
+
const macs = await listMacs();
|
|
7415
|
+
if (opts.json) {
|
|
7416
|
+
console.log(JSON.stringify(macs, null, 2));
|
|
7417
|
+
return;
|
|
7418
|
+
}
|
|
7419
|
+
if (!macs.length) console.log("no Macs connected; sign in to the AgentSky Mac app");
|
|
7420
|
+
for (const entry2 of macs) console.log(renderMac(entry2));
|
|
7421
|
+
})
|
|
7422
|
+
);
|
|
7423
|
+
mac.command("ask <machine> <kind>").description("ask a Mac to lend a folder, a browser window, its screen or its apps and files (host: local chats, call notes, uploads, saves, local git); the person answers on the Mac. Prints the MCP URL a session attaches once it is granted").requiredOption("--reason <text>", "why, in one sentence the person reads on their Mac").option("--no-wait", "return as soon as the Mac has the ask").option("--json", "print as JSON").action(
|
|
7424
|
+
run(async (machine, kind, opts) => {
|
|
7425
|
+
const asked = await askMac(machine, kind, opts.reason);
|
|
7426
|
+
const answer = opts.wait ? await waitForAnswer(asked.reach_request.id, 240) : asked.reach_request;
|
|
7427
|
+
if (opts.json) {
|
|
7428
|
+
console.log(JSON.stringify({ reach_request: answer, mcp_url: asked.mcp_url }, null, 2));
|
|
7429
|
+
return;
|
|
7430
|
+
}
|
|
7431
|
+
console.log(renderReachRequest(answer));
|
|
7432
|
+
console.log(`mcp_url: ${asked.mcp_url}`);
|
|
7433
|
+
if (answer.status === "granted") console.log("attach it as an MCP server: mcp_servers on POST /v1/sessions (MCP start_session: sources), or PATCH /v1/sessions/{id}");
|
|
7434
|
+
else if (!["pending", "asking"].includes(answer.status)) process.exitCode = 1;
|
|
7435
|
+
})
|
|
7436
|
+
);
|
|
7437
|
+
mac.command("ask-status <id>").description("where an ask stands: pending, asking, granted, denied, unavailable or expired").option("--wait <seconds>", "wait up to this long (at most 25) for the person to answer", "0").option("--json", "print as JSON").action(
|
|
7438
|
+
run(async (id, opts) => {
|
|
7439
|
+
const answer = await readReachRequest(id, Number(opts.wait) || 0);
|
|
7440
|
+
if (opts.json) {
|
|
7441
|
+
console.log(JSON.stringify(answer, null, 2));
|
|
7442
|
+
return;
|
|
7443
|
+
}
|
|
7444
|
+
console.log(renderReachRequest(answer));
|
|
7445
|
+
})
|
|
7446
|
+
);
|
|
7447
|
+
mac.command("take-back <mount>").description("end a folder, browser window, screen or apps and files lent from a Mac; every later call to it fails").action(
|
|
7448
|
+
run(async (mount) => {
|
|
7449
|
+
await takeBack(mount);
|
|
7450
|
+
console.log(`took back ${mount}`);
|
|
7451
|
+
})
|
|
7452
|
+
);
|
|
6872
7453
|
var browserProfile = program.command("browser-profile").description("saved-login profiles: the sites your agents stay signed in to");
|
|
6873
7454
|
browserProfile.command("list").description("your workspace's browser profiles: the sites your agents stay signed in to").option("--json", "print as JSON").action(
|
|
6874
7455
|
run(async (opts) => {
|
|
@@ -6932,10 +7513,16 @@ notifications.command("get").description("show the current notification preferen
|
|
|
6932
7513
|
console.log(JSON.stringify(prefs, null, 2));
|
|
6933
7514
|
return;
|
|
6934
7515
|
}
|
|
6935
|
-
console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done}`);
|
|
7516
|
+
console.log(`email: needs_you=${prefs.email.needs_you} done=${prefs.email.done} marketing=${prefs.email.marketing}`);
|
|
6936
7517
|
console.log(`push: needs_you=${prefs.push.needs_you} done=${prefs.push.done}`);
|
|
6937
7518
|
})
|
|
6938
7519
|
);
|
|
7520
|
+
notifications.command("unsubscribe-marketing").description("stop AgentSky marketing email to your address; login and account email continue").action(
|
|
7521
|
+
run(async () => {
|
|
7522
|
+
const { marketing } = await unsubscribeMarketing();
|
|
7523
|
+
console.log(marketing ? "Marketing email is still on." : "Unsubscribed from AgentSky marketing email.");
|
|
7524
|
+
})
|
|
7525
|
+
);
|
|
6939
7526
|
notifications.command("set").description("update notification preferences (pass at least one flag)").option("--email-needs-you <bool>", "email when the task needs your input: true or false").option("--email-done <bool>", "email when the task finishes: true or false").option("--push-needs-you <bool>", "push notification when the task needs your input: true or false").option("--push-done <bool>", "push notification when the task finishes: true or false").action(
|
|
6940
7527
|
run(async (opts) => {
|
|
6941
7528
|
const parseBool = (v) => v === void 0 ? void 0 : v === "true" ? true : v === "false" ? false : (() => {
|
|
@@ -7093,7 +7680,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
|
|
|
7093
7680
|
}));
|
|
7094
7681
|
var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
|
|
7095
7682
|
traceCmd.command("start").description("start a trace: a grant for this machine, a root span, and the spool the hooks write to").option("--name <name>", "a name for the task").option("--source <source>", "how this machine reports itself", "claude-code").option("--hooks [settings]", "also install the Claude Code hooks (default: ./.claude/settings.local.json)").option("--capture <scopes>", "also send content, redacted on this machine first: any of prompts,tool_output,files").option("--file <path>", "a file the files scope may send (repeatable)", collect).option("--dir <path>", "a folder whose files the files scope may send when a file tool touches them (repeatable)", collect).action(run(async (opts) => {
|
|
7096
|
-
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-
|
|
7683
|
+
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-ZRU624EY.js");
|
|
7097
7684
|
if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
|
|
7098
7685
|
const scopes = parseContentScopes(opts.capture);
|
|
7099
7686
|
const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
|
|
@@ -7128,7 +7715,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
|
|
|
7128
7715
|
}));
|
|
7129
7716
|
traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
|
|
7130
7717
|
try {
|
|
7131
|
-
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-
|
|
7718
|
+
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-ZRU624EY.js");
|
|
7132
7719
|
const chunks = [];
|
|
7133
7720
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7134
7721
|
const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
@@ -7141,14 +7728,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
|
|
|
7141
7728
|
}
|
|
7142
7729
|
});
|
|
7143
7730
|
traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
|
|
7144
|
-
const { readState, flush, grantSender } = await import("./trace-
|
|
7731
|
+
const { readState, flush, grantSender } = await import("./trace-ZRU624EY.js");
|
|
7145
7732
|
const state = readState();
|
|
7146
7733
|
if (!state) throw new Error("no trace is running here");
|
|
7147
7734
|
const outcome = await flush(grantSender(state), { attempts: 4 });
|
|
7148
7735
|
console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
|
|
7149
7736
|
}));
|
|
7150
7737
|
traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
|
|
7151
|
-
const { readState, spoolStatus } = await import("./trace-
|
|
7738
|
+
const { readState, spoolStatus } = await import("./trace-ZRU624EY.js");
|
|
7152
7739
|
const state = readState();
|
|
7153
7740
|
if (!state) {
|
|
7154
7741
|
console.log("No trace is running here.");
|
|
@@ -7158,7 +7745,7 @@ traceCmd.command("status").description("the running trace and its spool").action
|
|
|
7158
7745
|
console.log(`${state.trace_id} as ${state.source}: ${spool.pending} pending, ${spool.dropped_full} dropped (spool full), ${spool.dropped_expired} given up, ${spool.refused} refused`);
|
|
7159
7746
|
}));
|
|
7160
7747
|
traceCmd.command("show [id]").description("the trace bundle: spans, the sessions they reach, cost counted once per session").option("--json", "print the bundle as JSON").action(run(async (id, opts) => {
|
|
7161
|
-
const { readState } = await import("./trace-
|
|
7748
|
+
const { readState } = await import("./trace-ZRU624EY.js");
|
|
7162
7749
|
const ref = id ?? readState()?.trace_id;
|
|
7163
7750
|
if (!ref) throw new Error("name a trace, or start one here");
|
|
7164
7751
|
const bundle = await api("GET", `/traces/${ref}`);
|
|
@@ -7185,7 +7772,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
|
|
|
7185
7772
|
for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
|
|
7186
7773
|
}));
|
|
7187
7774
|
traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
|
|
7188
|
-
const { readState, record, flush, grantSender, clearState } = await import("./trace-
|
|
7775
|
+
const { readState, record, flush, grantSender, clearState } = await import("./trace-ZRU624EY.js");
|
|
7189
7776
|
const state = readState();
|
|
7190
7777
|
if (!state) throw new Error("no trace is running here");
|
|
7191
7778
|
if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
|
|
@@ -7195,7 +7782,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
|
|
|
7195
7782
|
console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
|
|
7196
7783
|
}));
|
|
7197
7784
|
traceCmd.command("sync [payload]").description("upload this machine's agent sessions (Claude Code, Codex, Cursor, Gemini CLI and every other agent `--agents` lists) to the matching workspace's trace library, redacted here first").option("--enable", "turn sync on for this machine").option("--disable", "turn sync off; nothing more is uploaded").option("--route <match=workspace>", "send sessions whose folder path contains <match> to <workspace> (repeatable; replaces the saved rules)", collect).option("--default <workspace>", "where a session no rule matches goes").option("--keep-local", "keep sessions no rule matches on this machine (the default)").option("--session <ref>", "only this session: its id or transcript path (repeatable)", collect).option("--import <export>", "add a downloaded cloud export (claude.ai, ChatGPT, Gemini, Grok, DeepSeek \u2026: its .zip, folder or file) so sync uploads it (repeatable)", collect).option("--agents", "list the agents sync reads and which are on this machine").option("--full", "re-read every session, not only files that changed").option("--limit <n>", "at most this many changed sessions this run", (v) => Number.parseInt(v, 10)).option("--dry-run", "show what would be uploaded without uploading").option("--hook", "run from a Claude Code SessionEnd hook or Codex notify: upload that session in the background and return at once").option("--install-schedule", "also sweep every 15 minutes (macOS launchd)").option("--status", "what sync has done on this machine").option("--quiet", "print only failures").action(async (payload, opts) => {
|
|
7198
|
-
const lib = await import("./trace-library-
|
|
7785
|
+
const lib = await import("./trace-library-QOQUAL5H.js");
|
|
7199
7786
|
if (opts.hook) {
|
|
7200
7787
|
try {
|
|
7201
7788
|
let raw = payload ?? "";
|
|
@@ -7254,10 +7841,10 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
|
|
|
7254
7841
|
console.log(`Added ${imported.sessions} ${imported.agent} session${imported.sessions === 1 ? "" : "s"} from ${input}.`);
|
|
7255
7842
|
}
|
|
7256
7843
|
if (opts.installSchedule) {
|
|
7257
|
-
const { join:
|
|
7258
|
-
const { homedir:
|
|
7844
|
+
const { join: join19 } = await import("path");
|
|
7845
|
+
const { homedir: homedir10 } = await import("os");
|
|
7259
7846
|
const { execFileSync } = await import("child_process");
|
|
7260
|
-
const log =
|
|
7847
|
+
const log = join19(homedir10(), ".config", "sky", "trace-sync.log");
|
|
7261
7848
|
const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
|
|
7262
7849
|
try {
|
|
7263
7850
|
execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
|
|
@@ -7267,11 +7854,11 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
|
|
|
7267
7854
|
console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
|
|
7268
7855
|
}
|
|
7269
7856
|
if (opts.status) {
|
|
7270
|
-
const { readFileSync:
|
|
7271
|
-
const { join:
|
|
7857
|
+
const { readFileSync: readFileSync18 } = await import("fs");
|
|
7858
|
+
const { join: join19 } = await import("path");
|
|
7272
7859
|
let state = { sessions: {} };
|
|
7273
7860
|
try {
|
|
7274
|
-
state = JSON.parse(
|
|
7861
|
+
state = JSON.parse(readFileSync18(join19(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
|
|
7275
7862
|
} catch {
|
|
7276
7863
|
}
|
|
7277
7864
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -7304,7 +7891,7 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
|
|
|
7304
7891
|
})();
|
|
7305
7892
|
});
|
|
7306
7893
|
traceCmd.command("search <query...>").description("search every session in a workspace's trace library (literal substring)").option("--workspace <slug>", "the workspace to search (default: the current one)").option("--limit <n>", "hits per page", (v) => Number.parseInt(v, 10), 20).option("--cursor <cursor>", "the next page of an earlier search").option("--json", "print the page as JSON").action(run(async (words, opts) => {
|
|
7307
|
-
const { libraryRequest } = await import("./trace-library-
|
|
7894
|
+
const { libraryRequest } = await import("./trace-library-QOQUAL5H.js");
|
|
7308
7895
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
7309
7896
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
7310
7897
|
const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
|
|
@@ -7321,7 +7908,7 @@ traceCmd.command("search <query...>").description("search every session in a wor
|
|
|
7321
7908
|
if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
|
|
7322
7909
|
}));
|
|
7323
7910
|
traceCmd.command("library").description("list the sessions in a workspace's trace library, most recently updated first").option("--workspace <slug>", "the workspace (default: the current one)").option("--limit <n>", "how many", (v) => Number.parseInt(v, 10), 20).option("--json", "print the page as JSON").action(run(async (opts) => {
|
|
7324
|
-
const { libraryRequest } = await import("./trace-library-
|
|
7911
|
+
const { libraryRequest } = await import("./trace-library-QOQUAL5H.js");
|
|
7325
7912
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
7326
7913
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
7327
7914
|
const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
|
|
@@ -7437,6 +8024,140 @@ thread.command("list").description("your workspace threads, most recently update
|
|
|
7437
8024
|
for (const t of threads) console.log(renderThreadSummary(t));
|
|
7438
8025
|
})
|
|
7439
8026
|
);
|
|
8027
|
+
thread.command("start <text>").description("start a thread and send it its first message, as the web composer does").requiredOption("--agent <type>", "the agent's harness type (e.g. claude_code, codex)").option("--llm <model>", "the model (the harness default when omitted)").option("--effort <level>", "thinking level").option("--vs <type:model...>", "more agents that get the same message, side by side", (val, acc) => [...acc, val], []).option("--saved-agent <id>", "run on one of your saved agents").option("--workspace <slug>", "start it in this team workspace").option("--plan", "send the first message in Plan mode").option("--json", "print the new thread as JSON").action(
|
|
8028
|
+
run(async (text, opts) => {
|
|
8029
|
+
const compared = (opts.vs ?? []).map((v) => {
|
|
8030
|
+
const [agentType, llm] = v.split(":");
|
|
8031
|
+
if (!agentType) throw new ApiError(400, "invalid_request", `--vs must be type[:model], got "${v}"`);
|
|
8032
|
+
return { agentType, ...llm ? { llm } : {} };
|
|
8033
|
+
});
|
|
8034
|
+
const result = await startThread({
|
|
8035
|
+
requestId: `sky-thread-${crypto.randomUUID()}`,
|
|
8036
|
+
agentType: opts.agent,
|
|
8037
|
+
text,
|
|
8038
|
+
...opts.llm ? { llm: opts.llm } : {},
|
|
8039
|
+
...opts.effort ? { reasoningEffort: opts.effort } : {},
|
|
8040
|
+
...compared.length ? { compare: compared } : {},
|
|
8041
|
+
...opts.savedAgent ? { agentId: opts.savedAgent } : {},
|
|
8042
|
+
...opts.workspace ? { workspace: opts.workspace } : {},
|
|
8043
|
+
...opts.plan ? { interactionMode: "plan" } : {}
|
|
8044
|
+
});
|
|
8045
|
+
if (opts.json) {
|
|
8046
|
+
console.log(JSON.stringify(result, null, 2));
|
|
8047
|
+
return;
|
|
8048
|
+
}
|
|
8049
|
+
console.log(`started ${result.playgroundId}`);
|
|
8050
|
+
for (const col of result.columns) console.log(` agent ${col.columnId} session ${col.sessionId}`);
|
|
8051
|
+
})
|
|
8052
|
+
);
|
|
8053
|
+
thread.command("show <id>").description("one thread with every agent and the session each one runs in").option("--json", "print as JSON").action(
|
|
8054
|
+
run(async (id, opts) => {
|
|
8055
|
+
const detail = await getThread(id);
|
|
8056
|
+
if (opts.json) {
|
|
8057
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
8058
|
+
return;
|
|
8059
|
+
}
|
|
8060
|
+
console.log(`thread ${detail.id} ${detail.title}`);
|
|
8061
|
+
for (const col of detail.columns) console.log(` agent ${col.id} session ${col.sessionId} ${col.blind ? "(hidden until the vote)" : `${col.agentType}/${col.llm}`} ${col.status ?? col.podStatus ?? ""}`);
|
|
8062
|
+
})
|
|
8063
|
+
);
|
|
8064
|
+
thread.command("rename <id> <title>").description("rename a thread").action(run(async (id, title) => {
|
|
8065
|
+
await organizeThread(id, { title });
|
|
8066
|
+
console.log(`renamed ${id}`);
|
|
8067
|
+
}));
|
|
8068
|
+
for (const [verb, change, description] of [
|
|
8069
|
+
["pin", { pinned: true }, "pin a thread to the top of the rail"],
|
|
8070
|
+
["unpin", { pinned: false }, "unpin a thread"],
|
|
8071
|
+
["archive", { archived: true }, "put a thread away"],
|
|
8072
|
+
["unarchive", { archived: false }, "bring an archived thread back"],
|
|
8073
|
+
["mark-read", { seen: true }, "mark a thread read"],
|
|
8074
|
+
["mark-unread", { seen: false }, "mark a thread unread"]
|
|
8075
|
+
]) {
|
|
8076
|
+
thread.command(`${verb} <id>`).description(description).action(run(async (id) => {
|
|
8077
|
+
await organizeThread(id, change);
|
|
8078
|
+
console.log(`${verb}: ${id}`);
|
|
8079
|
+
}));
|
|
8080
|
+
}
|
|
8081
|
+
thread.command("delete <id>").description("delete a thread with its sessions").action(run(async (id) => {
|
|
8082
|
+
await deleteThread(id);
|
|
8083
|
+
console.log(`deleted ${id}`);
|
|
8084
|
+
}));
|
|
8085
|
+
thread.command("send <id> <text>").description("send one message to every agent of the thread, or to the ones --agent names").option("--agent <columnId...>", "only these agents (column ids from `sky thread show`)").option("--json", "print each agent's outcome as JSON").action(
|
|
8086
|
+
run(async (id, text, opts) => {
|
|
8087
|
+
const { results } = await sendThreadMessage(id, { text, deliveryId: crypto.randomUUID(), ...opts.agent?.length ? { columnIds: opts.agent } : {} });
|
|
8088
|
+
if (opts.json) {
|
|
8089
|
+
console.log(JSON.stringify(results, null, 2));
|
|
8090
|
+
return;
|
|
8091
|
+
}
|
|
8092
|
+
for (const [column, outcome] of Object.entries(results)) console.log(` ${column} ${outcome.ok ? "sent" : `not sent: ${outcome.error ?? "refused"}`}`);
|
|
8093
|
+
})
|
|
8094
|
+
);
|
|
8095
|
+
thread.command("add-agent <id>").description("add an agent to the thread, in a session of its own").requiredOption("--agent <type>", "the agent's harness type").option("--llm <model>", "the model").option("--effort <level>", "thinking level").action(
|
|
8096
|
+
run(async (id, opts) => {
|
|
8097
|
+
const { columns } = await addThreadAgent(id, { requestId: `sky-agent-${crypto.randomUUID()}`, agentType: opts.agent, ...opts.llm ? { llm: opts.llm } : {}, ...opts.effort ? { reasoningEffort: opts.effort } : {} });
|
|
8098
|
+
for (const col of columns) console.log(`added agent ${col.columnId} session ${col.sessionId}`);
|
|
8099
|
+
})
|
|
8100
|
+
);
|
|
8101
|
+
thread.command("remove-agent <id> <columnId>").description("take one agent out of the thread; its session is left as it is").action(run(async (id, columnId) => {
|
|
8102
|
+
await removeThreadAgent(id, columnId);
|
|
8103
|
+
console.log(`removed ${columnId}`);
|
|
8104
|
+
}));
|
|
8105
|
+
thread.command("stop <id>").description("stop what the thread's agents are doing").option("--agent <columnId>", "stop only this agent").action(run(async (id, opts) => {
|
|
8106
|
+
await interruptThread(id, opts.agent);
|
|
8107
|
+
console.log(`stopped ${id}`);
|
|
8108
|
+
}));
|
|
8109
|
+
thread.command("fork <id>").description("continue one agent's conversation in a thread of its own").option("--agent <columnId>", "the agent to fork (the first when omitted)").option("--at <messageId>", "carry the conversation only up to this message").option("--to <type:model>", "continue on another agent or model").action(
|
|
8110
|
+
run(async (id, opts) => {
|
|
8111
|
+
const [agentType, llm] = (opts.to ?? "").split(":");
|
|
8112
|
+
const { playgroundId } = await forkThread(id, {
|
|
8113
|
+
requestId: `sky-fork-${crypto.randomUUID()}`,
|
|
8114
|
+
...opts.agent ? { columnId: opts.agent } : {},
|
|
8115
|
+
...opts.at ? { messageId: opts.at } : {},
|
|
8116
|
+
...agentType ? { target: { agentType, ...llm ? { llm } : {} } } : {}
|
|
8117
|
+
});
|
|
8118
|
+
console.log(`forked into ${playgroundId}`);
|
|
8119
|
+
})
|
|
8120
|
+
);
|
|
8121
|
+
thread.command("model <id>").description("change the model or thinking level one agent of the thread runs on, from its next turn").option("--agent <columnId>", "the agent (the first when omitted)").option("--llm <model>", "the new model").option("--effort <level>", 'the new thinking level, or "default"').action(
|
|
8122
|
+
run(async (id, opts) => {
|
|
8123
|
+
if (!opts.llm && !opts.effort) throw new ApiError(400, "invalid_request", "Provide --llm or --effort.");
|
|
8124
|
+
const result = await changeThreadModel(id, { ...opts.llm ? { llm: opts.llm } : {}, ...opts.effort ? { reasoningEffort: opts.effort === "default" ? null : opts.effort } : {} }, opts.agent);
|
|
8125
|
+
console.log(`now ${result.llm}${result.reasoningEffort ? ` (${result.reasoningEffort})` : ""}`);
|
|
8126
|
+
})
|
|
8127
|
+
);
|
|
8128
|
+
thread.command("search <query>").description("search the words of your conversations").option("--archived", "search archived threads only").option("--all", "search active and archived threads").option("--cursor <cursor>", "the next page").action(
|
|
8129
|
+
run(async (query, opts) => {
|
|
8130
|
+
const threads = opts.all ? "all" : opts.archived ? "archived" : void 0;
|
|
8131
|
+
console.log(JSON.stringify(await searchThreads(query, { ...threads ? { threads } : {}, ...opts.cursor ? { cursor: opts.cursor } : {} }), null, 2));
|
|
8132
|
+
})
|
|
8133
|
+
);
|
|
8134
|
+
thread.command("options").description("what a new thread can start on: agents, models, thinking levels and grantable tools").action(run(async () => {
|
|
8135
|
+
console.log(JSON.stringify(await threadOptions(), null, 2));
|
|
8136
|
+
}));
|
|
8137
|
+
thread.command("join <sessionId>").description("open a teammate's thread, or one shared with you, as the session link does").action(
|
|
8138
|
+
run(async (sessionId) => {
|
|
8139
|
+
const opened = await joinThread(sessionId);
|
|
8140
|
+
console.log(opened.readOnly ? `read-only: session ${opened.sessionId}` : `opened ${opened.playgroundId}`);
|
|
8141
|
+
})
|
|
8142
|
+
);
|
|
8143
|
+
var project = program.command("project").description("the GitHub repositories a new thread can start on");
|
|
8144
|
+
project.command("list").description("repositories a new thread can start on, and the one you chose last").option("--json", "print as JSON").action(
|
|
8145
|
+
run(async (opts) => {
|
|
8146
|
+
const listed = await listProjects();
|
|
8147
|
+
if (opts.json) {
|
|
8148
|
+
console.log(JSON.stringify(listed, null, 2));
|
|
8149
|
+
return;
|
|
8150
|
+
}
|
|
8151
|
+
if (listed.unavailable) console.log(listed.unavailable.message);
|
|
8152
|
+
for (const repo of listed.repos) console.log(`${repo.fullName === listed.last?.repoFullName ? "* " : " "}${repo.fullName}`);
|
|
8153
|
+
})
|
|
8154
|
+
);
|
|
8155
|
+
project.command("use [repo]").description("remember a repository (owner/name) for your next new thread, or none when omitted").option("--branch <branch>", "the branch", "main").action(
|
|
8156
|
+
run(async (repo, opts) => {
|
|
8157
|
+
const { project: remembered } = await rememberProject(repo ? { repoFullName: repo, branch: opts.branch } : null);
|
|
8158
|
+
console.log(remembered ? `next thread starts on ${remembered.repoFullName}@${remembered.branch}` : "next thread starts on no repository");
|
|
8159
|
+
})
|
|
8160
|
+
);
|
|
7440
8161
|
session.command("feedback <id>").description("read the thumbs-up/down feedback on this session's messages").option("--json", "print as JSON").action(
|
|
7441
8162
|
run(async (id, opts) => {
|
|
7442
8163
|
const result = await api(
|