@agentsky/sky 2.15.0 → 2.17.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-Y3NTKJLA.js → chunk-FU43NICZ.js} +17 -5
- package/dist/{chunk-R5FKQQMG.js → chunk-H7SWXCH7.js} +1 -1
- package/dist/{chunk-RHRREQMF.js → chunk-ZJVFWFOY.js} +13 -0
- package/dist/index.js +1525 -248
- package/dist/products.js +1 -1
- package/dist/{src-KLP774OA.js → src-TJJKOFXZ.js} +1 -1
- package/dist/{trace-PMUETYJS.js → trace-H3AP64UZ.js} +2 -2
- package/dist/{trace-library-FIMBRED6.js → trace-library-LGPKAQTY.js} +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
api,
|
|
5
5
|
apiBytes,
|
|
6
6
|
apiForm,
|
|
7
|
-
|
|
7
|
+
downloadFileTo,
|
|
8
8
|
postEvents,
|
|
9
9
|
resolveSessionRef,
|
|
10
10
|
streamEvents,
|
|
11
11
|
uploadFile
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-FU43NICZ.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-H7SWXCH7.js";
|
|
24
24
|
import {
|
|
25
25
|
AGENT_TYPES,
|
|
26
26
|
MODEL_SUBSCRIPTION_PLAN_PHRASE,
|
|
@@ -37,21 +37,126 @@ import {
|
|
|
37
37
|
modelSubscriptionProviderFor,
|
|
38
38
|
reasoningEffortDocLine,
|
|
39
39
|
redactLine,
|
|
40
|
+
redactSecretValues,
|
|
40
41
|
skillRefSchema,
|
|
41
42
|
splitMcpServers,
|
|
42
43
|
supersededBy
|
|
43
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-ZJVFWFOY.js";
|
|
44
45
|
import "./chunk-MLKGABMK.js";
|
|
45
46
|
|
|
46
47
|
// src/index.ts
|
|
47
|
-
import { existsSync as existsSync10, readFileSync as
|
|
48
|
-
import { basename as
|
|
49
|
-
import { createInterface as
|
|
48
|
+
import { existsSync as existsSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
|
|
49
|
+
import { basename as basename8, resolve as resolve9 } from "path";
|
|
50
|
+
import { createInterface as createInterface6 } from "readline";
|
|
50
51
|
import { Command } from "commander";
|
|
51
52
|
|
|
52
53
|
// src/files.ts
|
|
53
|
-
import { readFileSync } from "fs";
|
|
54
|
-
import { basename, extname } from "path";
|
|
54
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
55
|
+
import { basename as basename2, extname } from "path";
|
|
56
|
+
|
|
57
|
+
// src/resumable-upload.ts
|
|
58
|
+
import { createHash } from "crypto";
|
|
59
|
+
import { mkdirSync, openSync, readFileSync, readSync, closeSync, rmSync, statSync, writeFileSync } from "fs";
|
|
60
|
+
import { basename, dirname, join, resolve } from "path";
|
|
61
|
+
var SINGLE_REQUEST_MAX_BYTES = 32 * 1024 * 1024;
|
|
62
|
+
var MAX_ATTEMPTS = 8;
|
|
63
|
+
function statePath(path, size, mtimeMs) {
|
|
64
|
+
const key = createHash("sha256").update(`${resolve(path)}\0${size}\0${mtimeMs}`).digest("hex").slice(0, 32);
|
|
65
|
+
return join(dirname(configPath()), "uploads", `${key}.json`);
|
|
66
|
+
}
|
|
67
|
+
function loadSession(file, expiresInSeconds) {
|
|
68
|
+
try {
|
|
69
|
+
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
70
|
+
if (saved.expiresInSeconds !== expiresInSeconds) return null;
|
|
71
|
+
if (Date.parse(saved.expires_at) - Date.now() < 3600 * 1e3) return null;
|
|
72
|
+
return saved;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function acknowledged(res) {
|
|
78
|
+
const range = res.headers.get("range")?.match(/^bytes=0-(\d+)$/);
|
|
79
|
+
return range ? Number(range[1]) + 1 : 0;
|
|
80
|
+
}
|
|
81
|
+
async function probe(session2) {
|
|
82
|
+
const res = await fetch(session2.upload_url, {
|
|
83
|
+
method: "PUT",
|
|
84
|
+
headers: { "user-agent": USER_AGENT, "content-range": `bytes */${session2.size_bytes}` }
|
|
85
|
+
});
|
|
86
|
+
if (res.status === 200 || res.status === 201) return "done";
|
|
87
|
+
if (res.status === 308) return acknowledged(res);
|
|
88
|
+
if (res.status === 404 || res.status === 410) throw new ApiError(res.status, "upload_expired", "the upload session is gone");
|
|
89
|
+
throw new ApiError(res.status, "upload_failed", `upload status check failed: ${res.status} ${res.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
async function sendChunk(session2, fd, offset) {
|
|
92
|
+
const length = Math.min(session2.chunk_size, session2.size_bytes - offset);
|
|
93
|
+
const chunk = Buffer.alloc(length);
|
|
94
|
+
let read = 0;
|
|
95
|
+
while (read < length) read += readSync(fd, chunk, read, length - read, offset + read);
|
|
96
|
+
const res = await fetch(session2.upload_url, {
|
|
97
|
+
method: "PUT",
|
|
98
|
+
headers: {
|
|
99
|
+
"user-agent": USER_AGENT,
|
|
100
|
+
"content-range": `bytes ${offset}-${offset + length - 1}/${session2.size_bytes}`
|
|
101
|
+
},
|
|
102
|
+
body: chunk
|
|
103
|
+
});
|
|
104
|
+
if (res.status === 200 || res.status === 201) return "done";
|
|
105
|
+
if (res.status === 308) return acknowledged(res);
|
|
106
|
+
if (res.status >= 500 || res.status === 429) throw new Error(`transient ${res.status}`);
|
|
107
|
+
throw new ApiError(res.status, "upload_failed", `chunk upload failed: ${res.status} ${(await res.text()).slice(0, 200)}`);
|
|
108
|
+
}
|
|
109
|
+
async function transfer(session2, path, onProgress) {
|
|
110
|
+
const fd = openSync(path, "r");
|
|
111
|
+
try {
|
|
112
|
+
let failures = 0;
|
|
113
|
+
let state = await probe(session2);
|
|
114
|
+
while (state !== "done") {
|
|
115
|
+
onProgress(state, session2.size_bytes);
|
|
116
|
+
try {
|
|
117
|
+
state = await sendChunk(session2, fd, state);
|
|
118
|
+
failures = 0;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof ApiError || ++failures >= MAX_ATTEMPTS) throw error;
|
|
121
|
+
await new Promise((done) => setTimeout(done, Math.min(3e4, 500 * 2 ** failures)));
|
|
122
|
+
state = await probe(session2);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
onProgress(session2.size_bytes, session2.size_bytes);
|
|
126
|
+
} finally {
|
|
127
|
+
closeSync(fd);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function uploadLargeFile(path, mediaType, expiresInSeconds, onProgress) {
|
|
131
|
+
const { size, mtimeMs } = statSync(path);
|
|
132
|
+
const stateFile = statePath(path, size, mtimeMs);
|
|
133
|
+
let session2 = loadSession(stateFile, expiresInSeconds);
|
|
134
|
+
if (session2) {
|
|
135
|
+
try {
|
|
136
|
+
await probe(session2);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (!(error instanceof ApiError) || error.status !== 404 && error.status !== 410) throw error;
|
|
139
|
+
session2 = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!session2) {
|
|
143
|
+
session2 = await api("POST", "/files/uploads", {
|
|
144
|
+
filename: basename(path),
|
|
145
|
+
size_bytes: size,
|
|
146
|
+
mime_type: mediaType,
|
|
147
|
+
...expiresInSeconds ? { expires_in_seconds: expiresInSeconds } : {}
|
|
148
|
+
});
|
|
149
|
+
mkdirSync(dirname(stateFile), { recursive: true });
|
|
150
|
+
const saved = { ...session2, path: resolve(path), mtimeMs, expiresInSeconds };
|
|
151
|
+
writeFileSync(stateFile, JSON.stringify(saved), { mode: 384 });
|
|
152
|
+
}
|
|
153
|
+
await transfer(session2, path, onProgress);
|
|
154
|
+
const file = await api("POST", `/files/uploads/${encodeURIComponent(session2.id)}/complete`);
|
|
155
|
+
rmSync(stateFile, { force: true });
|
|
156
|
+
return file;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/files.ts
|
|
55
160
|
var MEDIA_TYPES = {
|
|
56
161
|
".png": "image/png",
|
|
57
162
|
".jpg": "image/jpeg",
|
|
@@ -71,20 +176,24 @@ var MEDIA_TYPES = {
|
|
|
71
176
|
function mediaTypeFor(path) {
|
|
72
177
|
return MEDIA_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
73
178
|
}
|
|
74
|
-
async function uploadLocalFile(path, expiresInSeconds) {
|
|
75
|
-
|
|
76
|
-
|
|
179
|
+
async function uploadLocalFile(path, expiresInSeconds, onProgress = () => {
|
|
180
|
+
}) {
|
|
181
|
+
if (statSync2(path).size > SINGLE_REQUEST_MAX_BYTES) {
|
|
182
|
+
return uploadLargeFile(path, mediaTypeFor(path), expiresInSeconds, onProgress);
|
|
183
|
+
}
|
|
184
|
+
const bytes = readFileSync2(path);
|
|
185
|
+
return uploadFile(new Uint8Array(bytes), basename2(path), mediaTypeFor(path), expiresInSeconds);
|
|
77
186
|
}
|
|
78
187
|
function fileBlock(file) {
|
|
79
188
|
const source = { type: "file", file_id: file.id };
|
|
80
189
|
return file.mime_type.startsWith("image/") ? { type: "image", source } : { type: "document", source, title: file.filename };
|
|
81
190
|
}
|
|
82
|
-
function eventsFor(text, files2, system) {
|
|
191
|
+
function eventsFor(text, files2, system, interactionMode2) {
|
|
83
192
|
const content = [];
|
|
84
193
|
if (text.trim()) content.push({ type: "text", text });
|
|
85
194
|
for (const file of files2) content.push(fileBlock(file));
|
|
86
195
|
if (content.length === 0) throw new Error("nothing to send: pass text, --file or --image");
|
|
87
|
-
const events = [{ type: "user.message", content }];
|
|
196
|
+
const events = [{ type: "user.message", content, ...interactionMode2 ? { interaction_mode: interactionMode2 } : {} }];
|
|
88
197
|
if (system?.trim()) events.push({ type: "system.message", content: [{ type: "text", text: system }] });
|
|
89
198
|
return events;
|
|
90
199
|
}
|
|
@@ -286,8 +395,8 @@ function renderContextPage(page) {
|
|
|
286
395
|
}
|
|
287
396
|
|
|
288
397
|
// src/agent-file.ts
|
|
289
|
-
import { existsSync, readFileSync as
|
|
290
|
-
import { dirname, resolve } from "path";
|
|
398
|
+
import { existsSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
399
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
291
400
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
292
401
|
var fileInstructionSchema = external_exports.object({
|
|
293
402
|
name: external_exports.string(),
|
|
@@ -319,7 +428,7 @@ var agentFileSchema = external_exports.object({
|
|
|
319
428
|
memory_mb: external_exports.number().int().optional()
|
|
320
429
|
});
|
|
321
430
|
function readAgentFile(path) {
|
|
322
|
-
const raw =
|
|
431
|
+
const raw = readFileSync3(path, "utf8");
|
|
323
432
|
const parsed = agentFileSchema.safeParse(parseToml(raw));
|
|
324
433
|
if (!parsed.success) {
|
|
325
434
|
const issue = parsed.error.issues[0];
|
|
@@ -331,11 +440,11 @@ function readAgentFile(path) {
|
|
|
331
440
|
`${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
|
|
332
441
|
);
|
|
333
442
|
}
|
|
334
|
-
const base =
|
|
443
|
+
const base = dirname2(resolve2(path));
|
|
335
444
|
const inline = (ref, inlineContent) => {
|
|
336
445
|
if (inlineContent !== void 0) return inlineContent;
|
|
337
446
|
if (!ref) return void 0;
|
|
338
|
-
return
|
|
447
|
+
return readFileSync3(resolve2(base, ref), "utf8");
|
|
339
448
|
};
|
|
340
449
|
const spec = {
|
|
341
450
|
name: file.name,
|
|
@@ -361,14 +470,14 @@ function readAgentFile(path) {
|
|
|
361
470
|
return { file, spec };
|
|
362
471
|
}
|
|
363
472
|
function stampId(path, id) {
|
|
364
|
-
const doc = parseToml(
|
|
473
|
+
const doc = parseToml(readFileSync3(path, "utf8"));
|
|
365
474
|
doc.id = id;
|
|
366
475
|
delete doc.slug;
|
|
367
|
-
|
|
476
|
+
writeFileSync2(path, stringifyToml(doc));
|
|
368
477
|
}
|
|
369
478
|
function scaffoldAgentFile(path, name) {
|
|
370
479
|
if (existsSync(path)) throw new Error(`${path} already exists`);
|
|
371
|
-
|
|
480
|
+
writeFileSync2(
|
|
372
481
|
path,
|
|
373
482
|
`# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
|
|
374
483
|
name = "${name}"
|
|
@@ -454,18 +563,18 @@ function renderCatalogAgent(agent2) {
|
|
|
454
563
|
|
|
455
564
|
// src/clone.ts
|
|
456
565
|
import { spawn } from "child_process";
|
|
457
|
-
import { existsSync as existsSync2, readFileSync as
|
|
566
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
458
567
|
import { homedir } from "os";
|
|
459
|
-
import { basename as
|
|
568
|
+
import { basename as basename3, join as join2 } from "path";
|
|
460
569
|
import { createInterface } from "readline";
|
|
461
570
|
var PROMPT_CAP = 1e5;
|
|
462
571
|
var HARNESSES = ["claude_code", "codex", "openclaw"];
|
|
463
572
|
function claudeConfigRoot() {
|
|
464
|
-
return process.env.CLAUDE_CONFIG_DIR ??
|
|
573
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir(), ".claude");
|
|
465
574
|
}
|
|
466
575
|
function readIfExists(path) {
|
|
467
576
|
try {
|
|
468
|
-
return
|
|
577
|
+
return readFileSync4(path, "utf8");
|
|
469
578
|
} catch {
|
|
470
579
|
return void 0;
|
|
471
580
|
}
|
|
@@ -481,9 +590,9 @@ function readJson(path) {
|
|
|
481
590
|
}
|
|
482
591
|
function claudeModel(configRoot) {
|
|
483
592
|
const candidates = [
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
593
|
+
join2(process.cwd(), ".claude", "settings.local.json"),
|
|
594
|
+
join2(process.cwd(), ".claude", "settings.json"),
|
|
595
|
+
join2(configRoot, "settings.json")
|
|
487
596
|
];
|
|
488
597
|
for (const path of candidates) {
|
|
489
598
|
const model = readJson(path)?.model;
|
|
@@ -493,11 +602,11 @@ function claudeModel(configRoot) {
|
|
|
493
602
|
}
|
|
494
603
|
function claudeMcpServers() {
|
|
495
604
|
const merged = {};
|
|
496
|
-
const userConfig = readJson(
|
|
605
|
+
const userConfig = readJson(join2(homedir(), ".claude.json"));
|
|
497
606
|
Object.assign(merged, userConfig?.mcpServers ?? {});
|
|
498
607
|
Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
|
|
499
608
|
const projectConfig = readJson(
|
|
500
|
-
|
|
609
|
+
join2(process.cwd(), ".mcp.json")
|
|
501
610
|
);
|
|
502
611
|
Object.assign(merged, projectConfig?.mcpServers ?? {});
|
|
503
612
|
return merged;
|
|
@@ -514,13 +623,13 @@ function readClaudeCode() {
|
|
|
514
623
|
const configRoot = claudeConfigRoot();
|
|
515
624
|
const parts = [];
|
|
516
625
|
const sources = [];
|
|
517
|
-
const global = readIfExists(
|
|
626
|
+
const global = readIfExists(join2(configRoot, "CLAUDE.md"));
|
|
518
627
|
if (global?.trim()) {
|
|
519
628
|
parts.push({ label: "Global CLAUDE.md", content: global });
|
|
520
629
|
sources.push("global CLAUDE.md");
|
|
521
630
|
}
|
|
522
631
|
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
523
|
-
const project = readIfExists(
|
|
632
|
+
const project = readIfExists(join2(process.cwd(), name));
|
|
524
633
|
if (project?.trim()) {
|
|
525
634
|
parts.push({ label: `Project ${name}`, content: project });
|
|
526
635
|
sources.push(`project ${name}`);
|
|
@@ -561,8 +670,8 @@ function detectHarness(explicit) {
|
|
|
561
670
|
return explicit;
|
|
562
671
|
}
|
|
563
672
|
if (existsSync2(claudeConfigRoot())) return "claude_code";
|
|
564
|
-
if (existsSync2(
|
|
565
|
-
if (existsSync2(
|
|
673
|
+
if (existsSync2(join2(homedir(), ".codex"))) return "codex";
|
|
674
|
+
if (existsSync2(join2(homedir(), ".openclaw"))) return "openclaw";
|
|
566
675
|
throw new Error(
|
|
567
676
|
"no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
|
|
568
677
|
);
|
|
@@ -574,14 +683,14 @@ function readLocal(harness2) {
|
|
|
574
683
|
);
|
|
575
684
|
}
|
|
576
685
|
function linkPath(configRoot) {
|
|
577
|
-
return
|
|
686
|
+
return join2(configRoot, "agentsky-link.json");
|
|
578
687
|
}
|
|
579
688
|
function readLink(configRoot) {
|
|
580
689
|
const link = readJson(linkPath(configRoot));
|
|
581
690
|
return link?.agentId ? link : void 0;
|
|
582
691
|
}
|
|
583
692
|
function writeLink(configRoot, link) {
|
|
584
|
-
|
|
693
|
+
writeFileSync3(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
|
|
585
694
|
`);
|
|
586
695
|
}
|
|
587
696
|
var DEFAULT_VAULT_NAME = "default";
|
|
@@ -596,10 +705,10 @@ async function ensureDefaultVaultId() {
|
|
|
596
705
|
const existing = await fetchDefaultVaultId();
|
|
597
706
|
if (existing) return existing;
|
|
598
707
|
try {
|
|
599
|
-
const { vault } = await api("POST", "/vaults", {
|
|
708
|
+
const { vault: vault2 } = await api("POST", "/vaults", {
|
|
600
709
|
display_name: DEFAULT_VAULT_NAME
|
|
601
710
|
});
|
|
602
|
-
return
|
|
711
|
+
return vault2.id;
|
|
603
712
|
} catch (error) {
|
|
604
713
|
if (error instanceof ApiError && error.status === 409) {
|
|
605
714
|
const raced = await fetchDefaultVaultId();
|
|
@@ -683,7 +792,7 @@ async function createClone(local, opts) {
|
|
|
683
792
|
`model ${local.model ?? "(harness default)"}`,
|
|
684
793
|
`${keptNames.length} MCP server${keptNames.length === 1 ? "" : "s"}${keptNames.length ? ` (${keptNames.join(", ")})` : ""}`
|
|
685
794
|
].join(" \xB7 ");
|
|
686
|
-
console.log(`Detected ${local.harness} at ${local.configRoot} (project ${
|
|
795
|
+
console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename3(process.cwd())})`);
|
|
687
796
|
console.log(`Will clone: ${summary}`);
|
|
688
797
|
if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
|
|
689
798
|
if (local.droppedServers.length) {
|
|
@@ -820,22 +929,22 @@ async function clone(opts) {
|
|
|
820
929
|
}
|
|
821
930
|
|
|
822
931
|
// src/import-local.ts
|
|
823
|
-
import { closeSync, existsSync as existsSync5, openSync, readdirSync as
|
|
932
|
+
import { closeSync as closeSync3, existsSync as existsSync5, openSync as openSync3, readdirSync as readdirSync3, readFileSync as readFileSync8, readSync as readSync3, statSync as statSync5 } from "fs";
|
|
824
933
|
import { homedir as homedir4 } from "os";
|
|
825
|
-
import { join as
|
|
826
|
-
import { createHash } from "crypto";
|
|
934
|
+
import { join as join6 } from "path";
|
|
935
|
+
import { createHash as createHash3 } from "crypto";
|
|
827
936
|
|
|
828
937
|
// src/import-files.ts
|
|
829
938
|
import { isUtf8 } from "buffer";
|
|
830
|
-
import { existsSync as existsSync4, readFileSync as
|
|
939
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
831
940
|
import { homedir as homedir3 } from "os";
|
|
832
|
-
import { dirname as
|
|
941
|
+
import { dirname as dirname4, extname as extname2, isAbsolute, join as join4, relative as relative2, resolve as resolve4, sep as sep2, basename as basename5 } from "path";
|
|
833
942
|
|
|
834
943
|
// src/import-workspace.ts
|
|
835
944
|
import { spawnSync } from "child_process";
|
|
836
|
-
import { existsSync as existsSync3, readdirSync, readFileSync as
|
|
945
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
|
|
837
946
|
import { homedir as homedir2 } from "os";
|
|
838
|
-
import { basename as
|
|
947
|
+
import { basename as basename4, dirname as dirname3, join as join3, relative, sep } from "path";
|
|
839
948
|
import { parse as parseToml2 } from "smol-toml";
|
|
840
949
|
var MAX_TREE = 5e3;
|
|
841
950
|
var SKILL_LINE = /Base directory for this skill: (\/[^\s"'\\<>]+)/g;
|
|
@@ -845,26 +954,26 @@ function git(cwd, ...args) {
|
|
|
845
954
|
}
|
|
846
955
|
var isFile = (path) => {
|
|
847
956
|
try {
|
|
848
|
-
return
|
|
957
|
+
return statSync3(path).isFile();
|
|
849
958
|
} catch {
|
|
850
959
|
return false;
|
|
851
960
|
}
|
|
852
961
|
};
|
|
853
962
|
var isDir = (path) => {
|
|
854
963
|
try {
|
|
855
|
-
return
|
|
964
|
+
return statSync3(path).isDirectory();
|
|
856
965
|
} catch {
|
|
857
966
|
return false;
|
|
858
967
|
}
|
|
859
968
|
};
|
|
860
969
|
function sharedInstructionPaths(home = homedir2()) {
|
|
861
|
-
const rules =
|
|
970
|
+
const rules = join3(home, ".agents", "rules");
|
|
862
971
|
let ruleFiles = [];
|
|
863
972
|
try {
|
|
864
|
-
ruleFiles = readdirSync(rules).filter((name) => name.endsWith(".md")).sort().map((name) =>
|
|
973
|
+
ruleFiles = readdirSync(rules).filter((name) => name.endsWith(".md")).sort().map((name) => join3(rules, name));
|
|
865
974
|
} catch {
|
|
866
975
|
}
|
|
867
|
-
return [
|
|
976
|
+
return [join3(home, "AGENTS.md"), join3(home, "MEMORY.md"), ...ruleFiles].filter(isFile);
|
|
868
977
|
}
|
|
869
978
|
function strings(value, out, depth = 0) {
|
|
870
979
|
if (depth > 12) return;
|
|
@@ -873,18 +982,18 @@ function strings(value, out, depth = 0) {
|
|
|
873
982
|
else if (value && typeof value === "object") for (const item of Object.values(value)) strings(item, out, depth + 1);
|
|
874
983
|
}
|
|
875
984
|
function skillRoots(cwd, home) {
|
|
876
|
-
const claude = process.env.CLAUDE_CONFIG_DIR ??
|
|
877
|
-
const codex = process.env.CODEX_HOME ??
|
|
985
|
+
const claude = process.env.CLAUDE_CONFIG_DIR ?? join3(home, ".claude");
|
|
986
|
+
const codex = process.env.CODEX_HOME ?? join3(home, ".codex");
|
|
878
987
|
return [
|
|
879
|
-
...cwd ? [
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
988
|
+
...cwd ? [join3(cwd, ".claude", "skills"), join3(cwd, ".agents", "skills")] : [],
|
|
989
|
+
join3(home, ".agents", "skills"),
|
|
990
|
+
join3(claude, "skills"),
|
|
991
|
+
join3(codex, "skills"),
|
|
992
|
+
join3(home, ".hermes", "skills")
|
|
884
993
|
];
|
|
885
994
|
}
|
|
886
995
|
function pluginSkill(plugin, skill, home) {
|
|
887
|
-
const base =
|
|
996
|
+
const base = join3(process.env.CLAUDE_CONFIG_DIR ?? join3(home, ".claude"), "plugins");
|
|
888
997
|
const walk = (dir, depth) => {
|
|
889
998
|
if (depth > 7) return null;
|
|
890
999
|
let entries;
|
|
@@ -895,8 +1004,8 @@ function pluginSkill(plugin, skill, home) {
|
|
|
895
1004
|
}
|
|
896
1005
|
for (const entry2 of entries) {
|
|
897
1006
|
if (!entry2.isDirectory() || entry2.name === "node_modules" || entry2.name === ".git") continue;
|
|
898
|
-
const path =
|
|
899
|
-
if (entry2.name === skill &&
|
|
1007
|
+
const path = join3(dir, entry2.name);
|
|
1008
|
+
if (entry2.name === skill && basename4(dir) === "skills" && path.includes(`${sep}${plugin}${sep}`) && isFile(join3(path, "SKILL.md"))) return path;
|
|
900
1009
|
const found = walk(path, depth + 1);
|
|
901
1010
|
if (found) return found;
|
|
902
1011
|
}
|
|
@@ -908,16 +1017,16 @@ function findSkill(name, cwd, home) {
|
|
|
908
1017
|
const [scope, skill] = name.includes(":") ? name.split(":", 2) : [null, name];
|
|
909
1018
|
if (scope && skill) return pluginSkill(scope, skill, home);
|
|
910
1019
|
for (const root of skillRoots(cwd, home)) {
|
|
911
|
-
const direct =
|
|
912
|
-
if (isFile(
|
|
1020
|
+
const direct = join3(root, name);
|
|
1021
|
+
if (isFile(join3(direct, "SKILL.md"))) return realpathSync(direct);
|
|
913
1022
|
}
|
|
914
1023
|
return null;
|
|
915
1024
|
}
|
|
916
1025
|
function loadedSkills(records, touched, cwd, home = homedir2()) {
|
|
917
1026
|
const dirs = /* @__PURE__ */ new Map();
|
|
918
1027
|
const missing = /* @__PURE__ */ new Set();
|
|
919
|
-
const add = (dir, name =
|
|
920
|
-
if (!isFile(
|
|
1028
|
+
const add = (dir, name = basename4(dir)) => {
|
|
1029
|
+
if (!isFile(join3(dir, "SKILL.md"))) return false;
|
|
921
1030
|
const real = realpathSync(dir);
|
|
922
1031
|
if (![...dirs.values()].includes(real)) dirs.set(name, real);
|
|
923
1032
|
return true;
|
|
@@ -937,13 +1046,13 @@ function loadedSkills(records, touched, cwd, home = homedir2()) {
|
|
|
937
1046
|
if (!dir || !add(dir, name)) missing.add(name);
|
|
938
1047
|
}
|
|
939
1048
|
}
|
|
940
|
-
for (const path of touched) if (
|
|
1049
|
+
for (const path of touched) if (basename4(path) === "SKILL.md") add(dirname3(path));
|
|
941
1050
|
for (const name of dirs.keys()) missing.delete(name);
|
|
942
1051
|
return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
|
|
943
1052
|
}
|
|
944
1053
|
function readJson2(path) {
|
|
945
1054
|
try {
|
|
946
|
-
const value = JSON.parse(
|
|
1055
|
+
const value = JSON.parse(readFileSync5(path, "utf8"));
|
|
947
1056
|
return value && typeof value === "object" ? value : null;
|
|
948
1057
|
} catch {
|
|
949
1058
|
return null;
|
|
@@ -988,16 +1097,16 @@ function mcpServers(harness2, cwd, home = homedir2()) {
|
|
|
988
1097
|
}
|
|
989
1098
|
};
|
|
990
1099
|
if (harness2 === "claude_code") {
|
|
991
|
-
const config = readJson2(
|
|
1100
|
+
const config = readJson2(join3(process.env.CLAUDE_CONFIG_DIR ? dirname3(process.env.CLAUDE_CONFIG_DIR) : home, ".claude.json"));
|
|
992
1101
|
const projects = config?.projects && typeof config.projects === "object" ? config.projects : {};
|
|
993
1102
|
const root = cwd ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || cwd : "";
|
|
994
1103
|
if (cwd) push(projects[cwd]?.mcpServers ?? projects[root]?.mcpServers, "local");
|
|
995
|
-
if (root) push(readJson2(
|
|
1104
|
+
if (root) push(readJson2(join3(root, ".mcp.json"))?.mcpServers, "project");
|
|
996
1105
|
push(config?.mcpServers, "user");
|
|
997
1106
|
}
|
|
998
1107
|
if (harness2 === "codex") {
|
|
999
1108
|
try {
|
|
1000
|
-
const config = parseToml2(
|
|
1109
|
+
const config = parseToml2(readFileSync5(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "config.toml"), "utf8"));
|
|
1001
1110
|
push(config.mcp_servers, "user");
|
|
1002
1111
|
} catch {
|
|
1003
1112
|
}
|
|
@@ -1035,7 +1144,7 @@ function workspaceState(cwd, harness2, skills2, home = homedir2(), title = null)
|
|
|
1035
1144
|
harness: harness2,
|
|
1036
1145
|
title: title?.trim().slice(0, 120) || null,
|
|
1037
1146
|
cwd: cwd ? cwd.replace(home, "~") : null,
|
|
1038
|
-
project: cwd ?
|
|
1147
|
+
project: cwd ? basename4(root ?? cwd) : null,
|
|
1039
1148
|
git: gitState,
|
|
1040
1149
|
tree: tree.slice(0, MAX_TREE),
|
|
1041
1150
|
tree_truncated: tree.length > MAX_TREE,
|
|
@@ -1057,7 +1166,7 @@ function skillFiles(skill) {
|
|
|
1057
1166
|
}
|
|
1058
1167
|
for (const entry2 of entries) {
|
|
1059
1168
|
if (entry2.name === "node_modules" || entry2.name === ".git" || entry2.name === "__pycache__" || entry2.name === ".venv") continue;
|
|
1060
|
-
const path =
|
|
1169
|
+
const path = join3(dir, entry2.name);
|
|
1061
1170
|
if (entry2.isDirectory()) walk(path, depth + 1);
|
|
1062
1171
|
else if (entry2.isFile()) out.push({ path, name: `skills/${skill.name}/${relative(skill.dir, path).split(sep).join("/")}` });
|
|
1063
1172
|
}
|
|
@@ -1068,7 +1177,7 @@ function skillFiles(skill) {
|
|
|
1068
1177
|
function codexThreadName(sessionId, home = homedir2()) {
|
|
1069
1178
|
let name = null;
|
|
1070
1179
|
try {
|
|
1071
|
-
for (const line of
|
|
1180
|
+
for (const line of readFileSync5(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "session_index.jsonl"), "utf8").split("\n")) {
|
|
1072
1181
|
if (!line.includes(sessionId)) continue;
|
|
1073
1182
|
try {
|
|
1074
1183
|
const entry2 = JSON.parse(line);
|
|
@@ -1130,7 +1239,7 @@ function classify(path) {
|
|
|
1130
1239
|
if (DOCUMENTS[ext]) return { kind: "document", mediaType: DOCUMENTS[ext] };
|
|
1131
1240
|
if (IMAGES[ext]) return { kind: "image", mediaType: IMAGES[ext] };
|
|
1132
1241
|
if (TEXT[ext]) return { kind: "text", mediaType: TEXT[ext] };
|
|
1133
|
-
if (PLAIN_TEXT.has(ext) || PLAIN_NAMES.has(
|
|
1242
|
+
if (PLAIN_TEXT.has(ext) || PLAIN_NAMES.has(basename5(path))) return { kind: "text", mediaType: "text/plain" };
|
|
1134
1243
|
return null;
|
|
1135
1244
|
}
|
|
1136
1245
|
var SHELL_READ = /(?:^|[\s;&|(`"'])(?:cat|head|tail|nl|less|more|wc|bat|sed\s+-n\s+(?:'[^']*'|"[^"]*"|\S+))(?:\s+-[\w-]+(?:\s+\d+)?)*\s+((?:~|\.{0,2}\/)?[\w@%+./~-]*[\w-]\.[\w]{1,10}|\/[\w@%+./-]+)/g;
|
|
@@ -1149,8 +1258,8 @@ function touchedPaths(records, cwd, home = homedir3()) {
|
|
|
1149
1258
|
const add = (raw, base) => {
|
|
1150
1259
|
if (!raw || raw.length > 1e3) return;
|
|
1151
1260
|
const path = raw.replace(/^~(?=\/)/, home);
|
|
1152
|
-
if (isAbsolute(path)) out.add(
|
|
1153
|
-
else if (base) out.add(
|
|
1261
|
+
if (isAbsolute(path)) out.add(resolve4(path));
|
|
1262
|
+
else if (base) out.add(resolve4(base, path));
|
|
1154
1263
|
};
|
|
1155
1264
|
const call = (name, input) => {
|
|
1156
1265
|
const i = obj(input);
|
|
@@ -1187,33 +1296,33 @@ function touchedPaths(records, cwd, home = homedir3()) {
|
|
|
1187
1296
|
return [...out];
|
|
1188
1297
|
}
|
|
1189
1298
|
function gitRoot(dir) {
|
|
1190
|
-
for (let d = dir; ; d =
|
|
1191
|
-
if (existsSync4(
|
|
1192
|
-
if (d ===
|
|
1299
|
+
for (let d = dir; ; d = dirname4(d)) {
|
|
1300
|
+
if (existsSync4(join4(d, ".git"))) return d;
|
|
1301
|
+
if (d === dirname4(d)) return null;
|
|
1193
1302
|
}
|
|
1194
1303
|
}
|
|
1195
1304
|
function instructionPaths(cwd, harness2, home = homedir3()) {
|
|
1196
1305
|
const out = [];
|
|
1197
1306
|
if (cwd && existsSync4(cwd)) {
|
|
1198
1307
|
const root = gitRoot(cwd) ?? cwd;
|
|
1199
|
-
for (let d =
|
|
1200
|
-
for (const name of INSTRUCTION_FILES) out.push(
|
|
1201
|
-
if (d === root || d ===
|
|
1308
|
+
for (let d = resolve4(cwd); ; d = dirname4(d)) {
|
|
1309
|
+
for (const name of INSTRUCTION_FILES) out.push(join4(d, name));
|
|
1310
|
+
if (d === root || d === dirname4(d)) break;
|
|
1202
1311
|
}
|
|
1203
1312
|
}
|
|
1204
|
-
if (harness2 === "claude_code") out.push(
|
|
1205
|
-
if (harness2 === "codex") out.push(
|
|
1313
|
+
if (harness2 === "claude_code") out.push(join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "CLAUDE.md"));
|
|
1314
|
+
if (harness2 === "codex") out.push(join4(process.env.CODEX_HOME ?? join4(home, ".codex"), "AGENTS.md"));
|
|
1206
1315
|
out.push(...sharedInstructionPaths(home));
|
|
1207
1316
|
return out.filter((path) => {
|
|
1208
1317
|
try {
|
|
1209
|
-
return
|
|
1318
|
+
return statSync4(path).isFile();
|
|
1210
1319
|
} catch {
|
|
1211
1320
|
return false;
|
|
1212
1321
|
}
|
|
1213
1322
|
});
|
|
1214
1323
|
}
|
|
1215
1324
|
function resourceName(path, cwd, home = homedir3()) {
|
|
1216
|
-
const inside = cwd ? relative2(
|
|
1325
|
+
const inside = cwd ? relative2(resolve4(cwd), path) : "";
|
|
1217
1326
|
if (cwd && inside && !inside.startsWith("..") && !isAbsolute(inside)) return `workspace/${inside.split(sep2).join("/")}`;
|
|
1218
1327
|
const underHome = relative2(home, path);
|
|
1219
1328
|
const shown = !underHome.startsWith("..") && !isAbsolute(underHome) ? `~/${underHome}` : path.replace(/^\/+/, "");
|
|
@@ -1225,7 +1334,7 @@ function resourceLine(name, mediaType, data) {
|
|
|
1225
1334
|
}
|
|
1226
1335
|
function collectSessionFiles(records, opts) {
|
|
1227
1336
|
const home = opts.home ?? homedir3();
|
|
1228
|
-
const exclude = new Set((opts.exclude ?? []).map((path) =>
|
|
1337
|
+
const exclude = new Set((opts.exclude ?? []).map((path) => resolve4(path)));
|
|
1229
1338
|
const leftOut = [];
|
|
1230
1339
|
const candidates = [];
|
|
1231
1340
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1240,7 +1349,7 @@ function collectSessionFiles(records, opts) {
|
|
|
1240
1349
|
}
|
|
1241
1350
|
let size;
|
|
1242
1351
|
try {
|
|
1243
|
-
const stat =
|
|
1352
|
+
const stat = statSync4(path);
|
|
1244
1353
|
if (!stat.isFile()) return;
|
|
1245
1354
|
size = stat.size;
|
|
1246
1355
|
} catch {
|
|
@@ -1290,7 +1399,7 @@ function collectSessionFiles(records, opts) {
|
|
|
1290
1399
|
for (const file of candidates) {
|
|
1291
1400
|
let data;
|
|
1292
1401
|
try {
|
|
1293
|
-
data =
|
|
1402
|
+
data = readFileSync6(file.path);
|
|
1294
1403
|
} catch {
|
|
1295
1404
|
leftOut.push({ path: file.path, reason: "missing" });
|
|
1296
1405
|
continue;
|
|
@@ -1324,6 +1433,325 @@ function collectSessionFiles(records, opts) {
|
|
|
1324
1433
|
return { lines, bytes, attached, leftOut, setup };
|
|
1325
1434
|
}
|
|
1326
1435
|
|
|
1436
|
+
// src/import-environment.ts
|
|
1437
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1438
|
+
import { createHash as createHash2 } from "crypto";
|
|
1439
|
+
import { closeSync as closeSync2, createWriteStream, lstatSync, mkdtempSync, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, readSync as readSync2, rmSync as rmSync2 } from "fs";
|
|
1440
|
+
import { tmpdir } from "os";
|
|
1441
|
+
import { join as join5 } from "path";
|
|
1442
|
+
import { isUtf8 as isUtf82 } from "buffer";
|
|
1443
|
+
import { createGzip } from "zlib";
|
|
1444
|
+
|
|
1445
|
+
// ../../packages/spec/src/import-layout.ts
|
|
1446
|
+
import { gzipSync } from "zlib";
|
|
1447
|
+
function folderName(workspace2, sourceCwd) {
|
|
1448
|
+
const raw = workspace2?.project || (sourceCwd ? sourceCwd.split("/").filter(Boolean).pop() : "") || "";
|
|
1449
|
+
const cleaned = raw.replace(/[^A-Za-z0-9._ -]/g, "-").replace(/^[^A-Za-z0-9_]+/, "").replace(/\.\.+/g, ".").slice(0, 80).trim();
|
|
1450
|
+
return cleaned || "imported-session";
|
|
1451
|
+
}
|
|
1452
|
+
function envName(server, field) {
|
|
1453
|
+
return `${server}_${field}`.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1454
|
+
}
|
|
1455
|
+
function ownServer(server, platform) {
|
|
1456
|
+
try {
|
|
1457
|
+
return server.transport !== "stdio" && platform.ownHost.test(new URL(server.url ?? "").hostname);
|
|
1458
|
+
} catch {
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function mcpJson(servers, platform) {
|
|
1463
|
+
const entries = servers.filter((server) => !ownServer(server, platform)).map((server) => {
|
|
1464
|
+
if (server.transport === "stdio") {
|
|
1465
|
+
return [server.name, {
|
|
1466
|
+
command: server.command,
|
|
1467
|
+
...server.args?.length ? { args: server.args } : {},
|
|
1468
|
+
...server.env?.length ? { env: Object.fromEntries(server.env.map((name) => [name, `\${${name}:-}`])) } : {}
|
|
1469
|
+
}];
|
|
1470
|
+
}
|
|
1471
|
+
const query = server.query?.length ? `?${server.query.map((name) => `${encodeURIComponent(name)}=\${${envName(server.name, name)}:-}`).join("&")}` : "";
|
|
1472
|
+
return [server.name, {
|
|
1473
|
+
type: server.transport,
|
|
1474
|
+
url: `${server.url}${query}`,
|
|
1475
|
+
...server.headers?.length ? { headers: Object.fromEntries(server.headers.map((name) => [name, `\${${envName(server.name, name)}:-}`])) } : {}
|
|
1476
|
+
}];
|
|
1477
|
+
});
|
|
1478
|
+
return JSON.stringify({ mcpServers: Object.fromEntries(entries) }, null, 2) + "\n";
|
|
1479
|
+
}
|
|
1480
|
+
function bullet(items, none) {
|
|
1481
|
+
return items.length ? items.map((item) => `- ${item}`).join("\n") : `- ${none}`;
|
|
1482
|
+
}
|
|
1483
|
+
function readme(folder, workspace2, counts, skills2, mcpWritten, platform) {
|
|
1484
|
+
const git2 = workspace2?.git;
|
|
1485
|
+
const servers = workspace2?.mcp_servers ?? [];
|
|
1486
|
+
const missingSkills = (workspace2?.skills ?? []).filter((skill) => !skill.found).map((skill) => skill.name);
|
|
1487
|
+
const secrets2 = servers.filter((server) => !ownServer(server, platform)).flatMap((server) => [
|
|
1488
|
+
...server.env ?? [],
|
|
1489
|
+
...(server.headers ?? []).map((name) => envName(server.name, name)),
|
|
1490
|
+
...(server.query ?? []).map((name) => envName(server.name, name))
|
|
1491
|
+
]);
|
|
1492
|
+
const where = workspace2?.cwd ? ` (${workspace2.cwd})` : "";
|
|
1493
|
+
return [
|
|
1494
|
+
"# Imported working copy",
|
|
1495
|
+
"",
|
|
1496
|
+
`This folder (/workspace/${folder}) holds what the imported conversation worked with on the person's computer.`,
|
|
1497
|
+
"It is historical context: verify a path before acting on it, and follow the person's current message.",
|
|
1498
|
+
"",
|
|
1499
|
+
"## Files",
|
|
1500
|
+
counts.wholeFolder ? `- The whole working directory${where}, ${counts.project} files at the same relative paths: its tracked files and new files git does not ignore (every file when it is not a repository), without version-control data, dependency and build output, or credential files.` : `- ${counts.project} files from the original working directory${where}, at the same relative paths: the files the conversation read, wrote or edited and its instruction files. The rest of the working tree did not travel${workspace2?.tree?.length ? `; its full file list is in ${platform.notesDir}/workspace.json (\`tree\`)` : ""}.`,
|
|
1501
|
+
...counts.outside ? [`- ${counts.outside} files from elsewhere on that computer (shared instructions such as ~/AGENTS.md, and files outside the working directory) under ${platform.notesDir}/files/, with ~ as home/.`] : [],
|
|
1502
|
+
...counts.attachments ? [`- ${counts.attachments} attachments of the conversation under ${platform.notesDir}/attachments/.`] : [],
|
|
1503
|
+
"- Text was redacted on that computer before upload: a masked value (\u25CF\u25CF\u25CF\u25CF\u25CF) is a secret that was removed, not the real content.",
|
|
1504
|
+
"",
|
|
1505
|
+
"## Repository",
|
|
1506
|
+
...git2 ? [
|
|
1507
|
+
`- Remote: ${git2.remote ?? "none"}`,
|
|
1508
|
+
`- Branch: ${git2.branch ?? "detached"} at commit ${git2.commit ?? "unknown"}${git2.ahead ? ` (${git2.ahead} commits not pushed)` : ""}`,
|
|
1509
|
+
...git2.dirty?.length ? [`- Uncommitted changes in ${git2.dirty.length} paths${counts.wholeFolder ? " are already in these files" : ""}; the change against that commit is ${platform.notesDir}/uncommitted.patch (apply with \`git apply\` on a checkout of that commit).`] : ["- No uncommitted changes."],
|
|
1510
|
+
counts.wholeFolder ? "- This folder's own git history starts with this copy; the project's history is on the remote." : `- To get the whole repository, clone the remote at that commit (connect GitHub in ${platform.name} if it is private), apply the patch, then use the files here.`
|
|
1511
|
+
] : ["- The working directory was not a git repository."],
|
|
1512
|
+
"",
|
|
1513
|
+
"## Skills",
|
|
1514
|
+
bullet(skills2.map((name) => `${name}: .claude/skills/${name}/SKILL.md (also .agents/skills/${name}/)`), "No skills were loaded."),
|
|
1515
|
+
...missingSkills.length ? ["", `Loaded but no longer on that computer, so not carried: ${missingSkills.join(", ")}.`] : [],
|
|
1516
|
+
"",
|
|
1517
|
+
"## Tool servers (MCP)",
|
|
1518
|
+
bullet(servers.map((server) => `${server.name} (${server.transport}${server.scope ? `, ${server.scope}` : ""}): ${server.transport === "stdio" ? [server.command, ...server.args ?? []].join(" ") : server.url}`), "None were configured."),
|
|
1519
|
+
...mcpWritten ? ["", `They are written to .mcp.json in this folder with every key as \${NAME:-}, except ${platform.name}'s own server, whose tools this session already has. Their values never leave the person's computer${secrets2.length ? `; these must be supplied to use them: ${[...new Set(secrets2)].join(", ")}` : ""}. A stdio server runs only if its command is installed here, and Claude Code connects a project server only once it is approved.`] : [],
|
|
1520
|
+
""
|
|
1521
|
+
].join("\n");
|
|
1522
|
+
}
|
|
1523
|
+
function parseWorkspace(file) {
|
|
1524
|
+
if (!file) return null;
|
|
1525
|
+
try {
|
|
1526
|
+
const value = JSON.parse(file.data.toString("utf8"));
|
|
1527
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1528
|
+
} catch {
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function importLayout(files2, sourceCwd, platform, wholeFolder) {
|
|
1533
|
+
if (!files2.length && !wholeFolder) return null;
|
|
1534
|
+
const workspace2 = parseWorkspace(files2.find((file) => file.name === "agentsky/workspace.json"));
|
|
1535
|
+
const folder = folderName(workspace2, sourceCwd);
|
|
1536
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1537
|
+
const skills2 = /* @__PURE__ */ new Set();
|
|
1538
|
+
const counts = { project: wholeFolder?.files ?? 0, outside: 0, attachments: 0, wholeFolder: Boolean(wholeFolder) };
|
|
1539
|
+
for (const file of files2) {
|
|
1540
|
+
const [kind, ...rest] = file.name.split("/");
|
|
1541
|
+
const path = rest.join("/");
|
|
1542
|
+
if (kind === "workspace") {
|
|
1543
|
+
if (wholeFolder) continue;
|
|
1544
|
+
entries.set(path, file.data);
|
|
1545
|
+
counts.project += 1;
|
|
1546
|
+
} else if (kind === "files") {
|
|
1547
|
+
entries.set(`${platform.notesDir}/files/${path.replace(/^~\//, "home/")}`, file.data);
|
|
1548
|
+
counts.outside += 1;
|
|
1549
|
+
} else if (kind === "imported-attachments") {
|
|
1550
|
+
entries.set(`${platform.notesDir}/attachments/${path}`, file.data);
|
|
1551
|
+
counts.attachments += 1;
|
|
1552
|
+
} else if (kind === "agentsky") entries.set(`${platform.notesDir}/${path}`, file.data);
|
|
1553
|
+
else if (kind === "skills" && rest.length > 1) {
|
|
1554
|
+
const skill = rest[0].split(":").pop();
|
|
1555
|
+
const inside = rest.slice(1).join("/");
|
|
1556
|
+
skills2.add(skill);
|
|
1557
|
+
entries.set(`.claude/skills/${skill}/${inside}`, file.data);
|
|
1558
|
+
entries.set(`.agents/skills/${skill}/${inside}`, file.data);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
const servers = (workspace2?.mcp_servers ?? []).filter((server) => server && typeof server.name === "string" && (server.transport === "stdio" ? typeof server.command === "string" : typeof server.url === "string"));
|
|
1562
|
+
const mcpWritten = servers.length > 0 && !entries.has(".mcp.json") && !wholeFolder?.paths.has(".mcp.json");
|
|
1563
|
+
if (mcpWritten) entries.set(".mcp.json", Buffer.from(mcpJson(servers, platform)));
|
|
1564
|
+
entries.set(`${platform.notesDir}/README.md`, Buffer.from(readme(folder, workspace2 ? { ...workspace2, mcp_servers: servers } : null, counts, [...skills2], mcpWritten, platform)));
|
|
1565
|
+
if (wholeFolder) for (const path of wholeFolder.paths) entries.delete(path);
|
|
1566
|
+
return { folder, entries, skills: [...skills2], mcpServers: servers.map((server) => server.name) };
|
|
1567
|
+
}
|
|
1568
|
+
var TAR_BLOCK = 512;
|
|
1569
|
+
var TAR_END = Buffer.alloc(TAR_BLOCK * 2);
|
|
1570
|
+
function header(name, size, type) {
|
|
1571
|
+
const block = Buffer.alloc(TAR_BLOCK);
|
|
1572
|
+
block.write(name, 0, 100, "utf8");
|
|
1573
|
+
block.write("0000644\0", 100);
|
|
1574
|
+
block.write("0000000\0", 108);
|
|
1575
|
+
block.write("0000000\0", 116);
|
|
1576
|
+
block.write(size.toString(8).padStart(11, "0") + "\0", 124);
|
|
1577
|
+
block.write("00000000000\0", 136);
|
|
1578
|
+
block.write(" ", 148);
|
|
1579
|
+
block.write(type, 156);
|
|
1580
|
+
block.write("ustar\x0000", 257);
|
|
1581
|
+
let sum = 0;
|
|
1582
|
+
for (const byte of block) sum += byte;
|
|
1583
|
+
block.write(sum.toString(8).padStart(6, "0") + "\0 ", 148);
|
|
1584
|
+
return block;
|
|
1585
|
+
}
|
|
1586
|
+
function paxRecord(key, value) {
|
|
1587
|
+
const body = ` ${key}=${value}
|
|
1588
|
+
`;
|
|
1589
|
+
let length = Buffer.byteLength(body);
|
|
1590
|
+
for (; ; ) {
|
|
1591
|
+
const total = String(length).length + length;
|
|
1592
|
+
if (String(total).length + length === total) return Buffer.from(`${total}${body}`);
|
|
1593
|
+
length += 1;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function tarPadding(size) {
|
|
1597
|
+
const rest = size % TAR_BLOCK;
|
|
1598
|
+
return Buffer.alloc(rest ? TAR_BLOCK - rest : 0);
|
|
1599
|
+
}
|
|
1600
|
+
function tarHeader(path, size) {
|
|
1601
|
+
if (Buffer.byteLength(path) <= 100) return header(path, size, "0");
|
|
1602
|
+
const pax = paxRecord("path", path);
|
|
1603
|
+
return Buffer.concat([header("PaxHeader", pax.length, "x"), pax, tarPadding(pax.length), header(path, size, "0")]);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// src/import-environment.ts
|
|
1607
|
+
var IMPORT_PLATFORM = { name: "AgentSky", ownHost: /(^|\.)(agentsky\.dev|asteroidbelt\.ai)$/i, notesDir: ".agentsky-import" };
|
|
1608
|
+
var MAX_ENVIRONMENT_FILES = 2e5;
|
|
1609
|
+
var MAX_ENVIRONMENT_BYTES = 2 * 1024 ** 3;
|
|
1610
|
+
var MAX_REDACTED_TEXT_BYTES = 10 * 1024 * 1024;
|
|
1611
|
+
var SKIPPED = /* @__PURE__ */ new Set([".git", "node_modules", ".venv", "venv", "__pycache__", ".next", "dist", "build", "target", ".DS_Store", ".cache", ".turbo"]);
|
|
1612
|
+
var SECRET_FILE2 = /(^|\/)(\.env(\..*)?|\.netrc|\.npmrc|\.pypirc|\.git-credentials|\.?credentials(\.json)?|auth\.json|\.claude\.json|id_(rsa|ed25519|ecdsa)[^/]*|.*\.(pem|key|p12|pfx|keystore|jks|kdbx))$|(^|\/)\.ssh\/|(^|\/)\.aws\/|(^|\/)\.gnupg\/|(^|\/)\.docker\/config\.json$/i;
|
|
1613
|
+
function listFiles(cwd) {
|
|
1614
|
+
const git2 = spawnSync2("git", ["-C", cwd, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 512 * 1024 * 1024 });
|
|
1615
|
+
if (git2.status === 0) return git2.stdout.split("\0").filter(Boolean).filter((path) => !path.split("/").some((part) => part === ".git"));
|
|
1616
|
+
const out = [];
|
|
1617
|
+
const walk = (dir, prefix) => {
|
|
1618
|
+
let entries;
|
|
1619
|
+
try {
|
|
1620
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
1621
|
+
} catch {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
for (const entry2 of entries) {
|
|
1625
|
+
if (SKIPPED.has(entry2.name)) continue;
|
|
1626
|
+
const rel = prefix ? `${prefix}/${entry2.name}` : entry2.name;
|
|
1627
|
+
if (entry2.isDirectory()) walk(join5(dir, entry2.name), rel);
|
|
1628
|
+
else out.push(rel);
|
|
1629
|
+
if (out.length > MAX_ENVIRONMENT_FILES) return;
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
walk(cwd, "");
|
|
1633
|
+
return out;
|
|
1634
|
+
}
|
|
1635
|
+
function textOf(path, size) {
|
|
1636
|
+
if (size > MAX_REDACTED_TEXT_BYTES) return null;
|
|
1637
|
+
const data = readFileSync7(path);
|
|
1638
|
+
if (data.subarray(0, 8e3).includes(0) || !isUtf82(data)) return null;
|
|
1639
|
+
return data;
|
|
1640
|
+
}
|
|
1641
|
+
async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
|
|
1642
|
+
const listed = listFiles(cwd);
|
|
1643
|
+
if (listed.length > MAX_ENVIRONMENT_FILES) throw new Error(`The working folder has more than ${MAX_ENVIRONMENT_FILES} files; import it with --no-environment.`);
|
|
1644
|
+
const scratch = mkdtempSync(join5(tmpdir(), "sky-environment-"));
|
|
1645
|
+
const path = join5(scratch, "environment.tar.gz");
|
|
1646
|
+
const leftOut = [];
|
|
1647
|
+
const written = /* @__PURE__ */ new Set();
|
|
1648
|
+
const hash = createHash2("sha256");
|
|
1649
|
+
let bytes = 0;
|
|
1650
|
+
let redacted2 = 0;
|
|
1651
|
+
const gzip = createGzip({ level: 6 });
|
|
1652
|
+
const sink = createWriteStream(path);
|
|
1653
|
+
gzip.on("data", (chunk) => {
|
|
1654
|
+
bytes += chunk.length;
|
|
1655
|
+
hash.update(chunk);
|
|
1656
|
+
});
|
|
1657
|
+
gzip.pipe(sink);
|
|
1658
|
+
const done = new Promise((resolve10, reject) => {
|
|
1659
|
+
sink.on("finish", () => resolve10());
|
|
1660
|
+
sink.on("error", reject);
|
|
1661
|
+
gzip.on("error", reject);
|
|
1662
|
+
});
|
|
1663
|
+
const write = (chunk) => new Promise((resolve10, reject) => {
|
|
1664
|
+
if (bytes > MAX_ENVIRONMENT_BYTES) {
|
|
1665
|
+
reject(new Error(`The working folder packs to more than ${MAX_ENVIRONMENT_BYTES / 1024 ** 3} GiB; import it with --no-environment.`));
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
if (gzip.write(chunk)) resolve10();
|
|
1669
|
+
else gzip.once("drain", resolve10);
|
|
1670
|
+
});
|
|
1671
|
+
try {
|
|
1672
|
+
for (const rel of listed) {
|
|
1673
|
+
const full = join5(cwd, ...rel.split("/"));
|
|
1674
|
+
if (SECRET_FILE2.test(rel)) {
|
|
1675
|
+
leftOut.push({ path: rel, reason: "credential file" });
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
let stat;
|
|
1679
|
+
try {
|
|
1680
|
+
stat = lstatSync(full);
|
|
1681
|
+
} catch {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
if (!stat.isFile()) {
|
|
1685
|
+
leftOut.push({ path: rel, reason: "not a regular file" });
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
const text = textOf(full, stat.size);
|
|
1689
|
+
if (text) {
|
|
1690
|
+
const masked = redactSecretValues(text.toString("utf8"));
|
|
1691
|
+
if (Object.keys(masked.hits).length) redacted2 += 1;
|
|
1692
|
+
const data = Buffer.from(masked.text, "utf8");
|
|
1693
|
+
await write(tarHeader(rel, data.length));
|
|
1694
|
+
await write(data);
|
|
1695
|
+
await write(tarPadding(data.length));
|
|
1696
|
+
} else {
|
|
1697
|
+
await write(tarHeader(rel, stat.size));
|
|
1698
|
+
const fd = openSync2(full, "r");
|
|
1699
|
+
try {
|
|
1700
|
+
const chunk = Buffer.alloc(1024 * 1024);
|
|
1701
|
+
let left = stat.size;
|
|
1702
|
+
for (let offset = 0; left > 0; ) {
|
|
1703
|
+
const read = readSync2(fd, chunk, 0, Math.min(chunk.length, left), offset);
|
|
1704
|
+
if (read <= 0) throw new Error(`${rel} changed while it was packed; import again.`);
|
|
1705
|
+
await write(Buffer.from(chunk.subarray(0, read)));
|
|
1706
|
+
offset += read;
|
|
1707
|
+
left -= read;
|
|
1708
|
+
}
|
|
1709
|
+
} finally {
|
|
1710
|
+
closeSync2(fd);
|
|
1711
|
+
}
|
|
1712
|
+
await write(tarPadding(stat.size));
|
|
1713
|
+
}
|
|
1714
|
+
written.add(rel);
|
|
1715
|
+
}
|
|
1716
|
+
const layout = importLayout(resources, cwd, platform, { files: written.size, paths: written });
|
|
1717
|
+
for (const [entry2, data] of layout?.entries ?? []) {
|
|
1718
|
+
await write(tarHeader(entry2, data.length));
|
|
1719
|
+
await write(data);
|
|
1720
|
+
await write(tarPadding(data.length));
|
|
1721
|
+
}
|
|
1722
|
+
await write(TAR_END);
|
|
1723
|
+
gzip.end();
|
|
1724
|
+
await done;
|
|
1725
|
+
if (bytes > MAX_ENVIRONMENT_BYTES) throw new Error(`The working folder packs to more than ${MAX_ENVIRONMENT_BYTES / 1024 ** 3} GiB; import it with --no-environment.`);
|
|
1726
|
+
return {
|
|
1727
|
+
path,
|
|
1728
|
+
folder: layout?.folder ?? "imported-session",
|
|
1729
|
+
bytes,
|
|
1730
|
+
sha256: hash.digest("hex"),
|
|
1731
|
+
files: written.size + (layout?.entries.size ?? 0),
|
|
1732
|
+
redacted: redacted2,
|
|
1733
|
+
leftOut,
|
|
1734
|
+
dispose: () => rmSync2(scratch, { recursive: true, force: true })
|
|
1735
|
+
};
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
gzip.destroy();
|
|
1738
|
+
sink.destroy();
|
|
1739
|
+
rmSync2(scratch, { recursive: true, force: true });
|
|
1740
|
+
throw error;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
function resourcesOf(lines) {
|
|
1744
|
+
return lines.flatMap((line) => {
|
|
1745
|
+
try {
|
|
1746
|
+
const record = JSON.parse(line);
|
|
1747
|
+
if (record.type !== "import.resource" || typeof record.name !== "string" || typeof record.media_type !== "string" || typeof record.data !== "string") return [];
|
|
1748
|
+
return [{ name: record.name, mediaType: record.media_type, data: Buffer.from(record.data, "base64") }];
|
|
1749
|
+
} catch {
|
|
1750
|
+
return [];
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1327
1755
|
// src/import-local.ts
|
|
1328
1756
|
function redactLine2(raw) {
|
|
1329
1757
|
return redactLine(raw).line;
|
|
@@ -1336,12 +1764,12 @@ function decodeWorkspace(encoded) {
|
|
|
1336
1764
|
}
|
|
1337
1765
|
}
|
|
1338
1766
|
function readPrefix(filePath) {
|
|
1339
|
-
const fd =
|
|
1767
|
+
const fd = openSync3(filePath, "r");
|
|
1340
1768
|
try {
|
|
1341
1769
|
const bytes = Buffer.alloc(256 * 1024);
|
|
1342
|
-
return bytes.subarray(0,
|
|
1770
|
+
return bytes.subarray(0, readSync3(fd, bytes, 0, bytes.length, 0)).toString("utf8");
|
|
1343
1771
|
} finally {
|
|
1344
|
-
|
|
1772
|
+
closeSync3(fd);
|
|
1345
1773
|
}
|
|
1346
1774
|
}
|
|
1347
1775
|
function sniffTitle(filePath) {
|
|
@@ -1396,16 +1824,16 @@ function sniffTitle(filePath) {
|
|
|
1396
1824
|
}
|
|
1397
1825
|
}
|
|
1398
1826
|
function listClaudeCodeSessions(limit) {
|
|
1399
|
-
const root = process.env.CLAUDE_CONFIG_DIR ??
|
|
1400
|
-
const projectsDir =
|
|
1827
|
+
const root = process.env.CLAUDE_CONFIG_DIR ?? join6(homedir4(), ".claude");
|
|
1828
|
+
const projectsDir = join6(root, "projects");
|
|
1401
1829
|
if (!existsSync5(projectsDir)) return [];
|
|
1402
1830
|
const sessions = [];
|
|
1403
1831
|
try {
|
|
1404
|
-
for (const encoded of
|
|
1405
|
-
const projectDir =
|
|
1832
|
+
for (const encoded of readdirSync3(projectsDir)) {
|
|
1833
|
+
const projectDir = join6(projectsDir, encoded);
|
|
1406
1834
|
let entries;
|
|
1407
1835
|
try {
|
|
1408
|
-
entries =
|
|
1836
|
+
entries = readdirSync3(projectDir);
|
|
1409
1837
|
} catch {
|
|
1410
1838
|
continue;
|
|
1411
1839
|
}
|
|
@@ -1413,22 +1841,22 @@ function listClaudeCodeSessions(limit) {
|
|
|
1413
1841
|
for (const entry2 of entries) {
|
|
1414
1842
|
if (!entry2.endsWith(".jsonl")) continue;
|
|
1415
1843
|
const sessionId = entry2.slice(0, -6);
|
|
1416
|
-
const mainFile =
|
|
1844
|
+
const mainFile = join6(projectDir, entry2);
|
|
1417
1845
|
let stat;
|
|
1418
1846
|
try {
|
|
1419
|
-
stat =
|
|
1847
|
+
stat = statSync5(mainFile);
|
|
1420
1848
|
} catch {
|
|
1421
1849
|
continue;
|
|
1422
1850
|
}
|
|
1423
1851
|
if (!stat.isFile()) continue;
|
|
1424
1852
|
let discoveryError;
|
|
1425
1853
|
const subagentFiles = [];
|
|
1426
|
-
const subagentDir =
|
|
1854
|
+
const subagentDir = join6(projectDir, sessionId, "subagents");
|
|
1427
1855
|
if (existsSync5(subagentDir)) {
|
|
1428
1856
|
try {
|
|
1429
|
-
const subFiles =
|
|
1857
|
+
const subFiles = readdirSync3(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
1430
1858
|
for (const f of subFiles) {
|
|
1431
|
-
subagentFiles.push(
|
|
1859
|
+
subagentFiles.push(join6(subagentDir, f));
|
|
1432
1860
|
}
|
|
1433
1861
|
} catch {
|
|
1434
1862
|
discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
|
|
@@ -1450,17 +1878,17 @@ function listClaudeCodeSessions(limit) {
|
|
|
1450
1878
|
return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
1451
1879
|
}
|
|
1452
1880
|
function listCodexSessions(limit) {
|
|
1453
|
-
const sessionsDir =
|
|
1881
|
+
const sessionsDir = join6(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join6(homedir4(), ".codex"), "sessions");
|
|
1454
1882
|
const records = [];
|
|
1455
1883
|
function walk(dir) {
|
|
1456
1884
|
let entries;
|
|
1457
1885
|
try {
|
|
1458
|
-
entries =
|
|
1886
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
1459
1887
|
} catch {
|
|
1460
1888
|
return;
|
|
1461
1889
|
}
|
|
1462
1890
|
for (const entry2 of entries) {
|
|
1463
|
-
const full =
|
|
1891
|
+
const full = join6(dir, entry2.name);
|
|
1464
1892
|
if (entry2.isDirectory()) {
|
|
1465
1893
|
walk(full);
|
|
1466
1894
|
continue;
|
|
@@ -1478,7 +1906,7 @@ function listCodexSessions(limit) {
|
|
|
1478
1906
|
sessionId: data.id,
|
|
1479
1907
|
workspace: typeof data.cwd === "string" ? data.cwd : dir,
|
|
1480
1908
|
title: sniffTitle(full),
|
|
1481
|
-
mtimeMs:
|
|
1909
|
+
mtimeMs: statSync5(full).mtimeMs,
|
|
1482
1910
|
files: [full]
|
|
1483
1911
|
},
|
|
1484
1912
|
...typeof parent === "string" ? { parent } : {}
|
|
@@ -1547,7 +1975,7 @@ async function mintTrace(base, harness2) {
|
|
|
1547
1975
|
function loadSummaryFile(summaryPath) {
|
|
1548
1976
|
let raw;
|
|
1549
1977
|
try {
|
|
1550
|
-
raw =
|
|
1978
|
+
raw = readFileSync8(summaryPath, "utf8");
|
|
1551
1979
|
} catch {
|
|
1552
1980
|
throw new Error(`Cannot read summary file: ${summaryPath}`);
|
|
1553
1981
|
}
|
|
@@ -1579,7 +2007,7 @@ function prepareImport(session2, summaryPath, options = {}) {
|
|
|
1579
2007
|
let inputBytes = 0;
|
|
1580
2008
|
let outputBytes = 0;
|
|
1581
2009
|
const lines = [];
|
|
1582
|
-
const digest =
|
|
2010
|
+
const digest = createHash3("sha256");
|
|
1583
2011
|
let nativeId;
|
|
1584
2012
|
let redactionCount = 0;
|
|
1585
2013
|
let encryptedReasoningCount = 0;
|
|
@@ -1588,9 +2016,9 @@ function prepareImport(session2, summaryPath, options = {}) {
|
|
|
1588
2016
|
const source = new SessionSourceCollector();
|
|
1589
2017
|
const records = [];
|
|
1590
2018
|
for (const path of session2.files) {
|
|
1591
|
-
const stat =
|
|
2019
|
+
const stat = statSync5(path);
|
|
1592
2020
|
if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
|
|
1593
|
-
const text =
|
|
2021
|
+
const text = readFileSync8(path, "utf8");
|
|
1594
2022
|
for (const raw of text.split("\n")) {
|
|
1595
2023
|
if (!raw.trim()) continue;
|
|
1596
2024
|
let record;
|
|
@@ -1668,16 +2096,17 @@ function prepareImport(session2, summaryPath, options = {}) {
|
|
|
1668
2096
|
if (encryptedReasoningCount > 0) gaps.push({ kind: "encrypted_reasoning", count: encryptedReasoningCount });
|
|
1669
2097
|
if (compactionCount > 0) gaps.push({ kind: "compaction", count: compactionCount });
|
|
1670
2098
|
if (unreadableCount > 0) gaps.push({ kind: "unreadable", count: unreadableCount });
|
|
1671
|
-
|
|
2099
|
+
const cwd = meta.cwd ?? (session2.harness === "claude_code" ? session2.workspace : "");
|
|
2100
|
+
return { body, transcriptLines: lines.length, outputBytes, redactionCount, gaps, attachedFiles: files2.attached, leftOutFiles: files2.leftOut, setup: files2.setup, cwd, resourceLines: files2.lines };
|
|
1672
2101
|
}
|
|
1673
2102
|
async function importSession(opts) {
|
|
1674
2103
|
const base = (opts.agentUseUrl ?? agentUseBase()).replace(/\/$/, "");
|
|
1675
|
-
const
|
|
1676
|
-
if (
|
|
2104
|
+
const destination2 = new URL(base);
|
|
2105
|
+
if (destination2.username || destination2.password || destination2.protocol !== "https:" && !(destination2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(destination2.hostname))) {
|
|
1677
2106
|
throw new Error("Import requires HTTPS, or a loopback development server.");
|
|
1678
2107
|
}
|
|
1679
2108
|
opts.onProgress?.("Reading transcript and redacting secrets locally\u2026");
|
|
1680
|
-
const { body, transcriptLines, outputBytes, redactionCount, gaps, attachedFiles, leftOutFiles, setup } = prepareImport(opts.session, opts.summaryPath, { files: opts.files });
|
|
2109
|
+
const { body, transcriptLines, outputBytes, redactionCount, gaps, attachedFiles, leftOutFiles, setup, cwd, resourceLines } = prepareImport(opts.session, opts.summaryPath, { files: opts.files });
|
|
1681
2110
|
let traceId;
|
|
1682
2111
|
let key;
|
|
1683
2112
|
if (opts.importKey && opts.grantTraceId) {
|
|
@@ -1701,6 +2130,7 @@ async function importSession(opts) {
|
|
|
1701
2130
|
}
|
|
1702
2131
|
const result = await response.json();
|
|
1703
2132
|
const finalTraceId = result.traceId ?? traceId;
|
|
2133
|
+
const environment = opts.files === false || opts.environment === false || !cwd || !existsSync5(cwd) ? null : await uploadEnvironment(base, key, finalTraceId, cwd, resourceLines, opts.onProgress).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
|
|
1704
2134
|
return {
|
|
1705
2135
|
traceId: finalTraceId,
|
|
1706
2136
|
url: `${base}/playground/new?import=${encodeURIComponent(finalTraceId)}`,
|
|
@@ -1712,12 +2142,96 @@ async function importSession(opts) {
|
|
|
1712
2142
|
gaps,
|
|
1713
2143
|
attachedFiles,
|
|
1714
2144
|
leftOutFiles,
|
|
1715
|
-
setup
|
|
2145
|
+
setup,
|
|
2146
|
+
environment
|
|
1716
2147
|
};
|
|
1717
2148
|
}
|
|
2149
|
+
async function grantCall(base, key, path, body) {
|
|
2150
|
+
const response = await fetch(`${base}${path}`, {
|
|
2151
|
+
method: "POST",
|
|
2152
|
+
redirect: "error",
|
|
2153
|
+
signal: AbortSignal.timeout(3e5),
|
|
2154
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json", "user-agent": USER_AGENT },
|
|
2155
|
+
body: JSON.stringify(body)
|
|
2156
|
+
});
|
|
2157
|
+
const answer = await response.json().catch(() => null);
|
|
2158
|
+
if (!response.ok) throw new Error(typeof answer?.error === "string" ? answer.error : answer?.error?.message ?? `The environment upload failed (${response.status}).`);
|
|
2159
|
+
return answer;
|
|
2160
|
+
}
|
|
2161
|
+
async function uploadEnvironment(base, key, traceId, cwd, resourceLines, onProgress) {
|
|
2162
|
+
onProgress?.("Packing the working folder (secrets redacted locally)\u2026");
|
|
2163
|
+
const packed = await packEnvironment(cwd, resourcesOf(resourceLines));
|
|
2164
|
+
try {
|
|
2165
|
+
const path = `/api/traces/${encodeURIComponent(traceId)}/environment`;
|
|
2166
|
+
const { upload } = await grantCall(base, key, path, { name: packed.folder, size: packed.bytes, sha256: packed.sha256, files: packed.files });
|
|
2167
|
+
let shown = -1;
|
|
2168
|
+
await transfer(upload, packed.path, (sent, total) => {
|
|
2169
|
+
const percent = Math.floor(sent / total * 100);
|
|
2170
|
+
if (percent >= shown + 10 || sent === total) {
|
|
2171
|
+
shown = percent;
|
|
2172
|
+
onProgress?.(`Uploading the working folder\u2026 ${percent}% of ${(total / 1024 / 1024).toFixed(1)} MiB`);
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
await grantCall(base, key, `${path}/complete`, {});
|
|
2176
|
+
return { folder: packed.folder, files: packed.files, bytes: packed.bytes, redacted: packed.redacted, leftOut: packed.leftOut.length };
|
|
2177
|
+
} finally {
|
|
2178
|
+
packed.dispose();
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/channels.ts
|
|
2183
|
+
function connectionLine(c) {
|
|
2184
|
+
const route = c.bound_session ? `\u2192 ${c.bound_session.id}${c.bound_session.label ? ` (${c.bound_session.label})` : ""}` : "unrouted";
|
|
2185
|
+
return `${c.id} ${c.platform.padEnd(9)} ${c.status.padEnd(12)} ${c.label ?? ""} ${route}`;
|
|
2186
|
+
}
|
|
2187
|
+
function connectSteps(c) {
|
|
2188
|
+
const steps = [];
|
|
2189
|
+
if (c.connect?.url) steps.push(`Open: ${c.connect.url}`);
|
|
2190
|
+
if (c.connect?.phone && c.connect.code) steps.push(`Text ${c.connect.code} to ${c.connect.phone}`);
|
|
2191
|
+
else if (c.connect?.app_id && c.connect.code) steps.push(`Send ${c.connect.code} to the bot (app ${c.connect.app_id})`);
|
|
2192
|
+
else if (c.connect?.code) steps.push(`Send ${c.connect.code} to the bot`);
|
|
2193
|
+
if (c.connect?.expires_at) steps.push(`Expires ${c.connect.expires_at.replace("T", " ").slice(0, 16)}`);
|
|
2194
|
+
return steps;
|
|
2195
|
+
}
|
|
2196
|
+
function destination(opts) {
|
|
2197
|
+
if (Boolean(opts.session) === Boolean(opts.webhook)) {
|
|
2198
|
+
throw new ApiError(400, "invalid_destination", "pass exactly one of --session or --webhook");
|
|
2199
|
+
}
|
|
2200
|
+
return opts.session ? { session: opts.session } : { webhook: opts.webhook };
|
|
2201
|
+
}
|
|
2202
|
+
async function listConnections() {
|
|
2203
|
+
return (await api("GET", "/channels/connections")).connections;
|
|
2204
|
+
}
|
|
2205
|
+
async function connect(platform, opts) {
|
|
2206
|
+
return api("POST", "/channels/connections", {
|
|
2207
|
+
platform,
|
|
2208
|
+
...opts.label ? { label: opts.label } : {},
|
|
2209
|
+
...opts.app ? { app: opts.app } : {},
|
|
2210
|
+
...opts.session || opts.webhook ? { destination: destination(opts) } : {}
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
async function waitConnected(id, opts = {}) {
|
|
2214
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 6e4);
|
|
2215
|
+
for (; ; ) {
|
|
2216
|
+
const found = (await listConnections()).find((c) => c.id === id);
|
|
2217
|
+
if (!found) throw new ApiError(404, "not_found", `connection ${id} is gone`);
|
|
2218
|
+
if (found.status !== "PENDING") return found;
|
|
2219
|
+
if (Date.now() > deadline) throw new ApiError(408, "connect_timeout", "the connection was not claimed within 10 minutes");
|
|
2220
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 3e3));
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
async function bind(connectionId, opts) {
|
|
2224
|
+
await api("PUT", `/channels/connections/${encodeURIComponent(connectionId)}/binding`, { destination: destination(opts) });
|
|
2225
|
+
}
|
|
2226
|
+
async function send(threadId, text) {
|
|
2227
|
+
await api("POST", `/channels/threads/${encodeURIComponent(threadId)}/messages`, { parts: [{ type: "markdown", text }] });
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/connectors.ts
|
|
2231
|
+
import { createServer as createServer2 } from "http";
|
|
1718
2232
|
|
|
1719
2233
|
// src/login.ts
|
|
1720
|
-
import { createHash as
|
|
2234
|
+
import { createHash as createHash4, randomBytes } from "crypto";
|
|
1721
2235
|
import { createServer } from "http";
|
|
1722
2236
|
import { hostname } from "os";
|
|
1723
2237
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -1733,7 +2247,7 @@ function openBrowser2(url) {
|
|
|
1733
2247
|
}
|
|
1734
2248
|
async function login(opts) {
|
|
1735
2249
|
const verifier = b64url(randomBytes(32));
|
|
1736
|
-
const challenge = b64url(
|
|
2250
|
+
const challenge = b64url(createHash4("sha256").update(verifier).digest());
|
|
1737
2251
|
const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
|
|
1738
2252
|
saveConfig({
|
|
1739
2253
|
...loadConfig(),
|
|
@@ -1749,7 +2263,7 @@ async function login(opts) {
|
|
|
1749
2263
|
}
|
|
1750
2264
|
async function browserLogin(verifier, challenge) {
|
|
1751
2265
|
const state = b64url(randomBytes(16));
|
|
1752
|
-
const code = await new Promise((
|
|
2266
|
+
const code = await new Promise((resolve10, reject) => {
|
|
1753
2267
|
const server = createServer((req, res) => {
|
|
1754
2268
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1755
2269
|
if (url.pathname !== "/callback") {
|
|
@@ -1762,7 +2276,7 @@ async function browserLogin(verifier, challenge) {
|
|
|
1762
2276
|
if (url.searchParams.get("state") !== state || !got) {
|
|
1763
2277
|
reject(new Error("login callback state mismatch"));
|
|
1764
2278
|
} else {
|
|
1765
|
-
|
|
2279
|
+
resolve10(got);
|
|
1766
2280
|
}
|
|
1767
2281
|
server.close();
|
|
1768
2282
|
});
|
|
@@ -1815,18 +2329,251 @@ async function headlessLogin(verifier, challenge) {
|
|
|
1815
2329
|
throw new Error("login timed out after 10 minutes");
|
|
1816
2330
|
}
|
|
1817
2331
|
|
|
2332
|
+
// src/connectors.ts
|
|
2333
|
+
async function listApps(query, limit = 50) {
|
|
2334
|
+
const apps = [];
|
|
2335
|
+
let cursor = null;
|
|
2336
|
+
do {
|
|
2337
|
+
const params = new URLSearchParams();
|
|
2338
|
+
if (query) params.set("q", query);
|
|
2339
|
+
if (cursor) params.set("cursor", cursor);
|
|
2340
|
+
const qs = params.toString();
|
|
2341
|
+
const page = await api("GET", `/connectors/apps${qs ? `?${qs}` : ""}`);
|
|
2342
|
+
apps.push(...page.apps);
|
|
2343
|
+
cursor = page.next_cursor;
|
|
2344
|
+
} while (cursor && apps.length < limit);
|
|
2345
|
+
return apps.slice(0, limit);
|
|
2346
|
+
}
|
|
2347
|
+
function connectionLine2(c) {
|
|
2348
|
+
return `${c.id} ${c.app_name} (${c.app}) ${c.label} since ${c.created_at.slice(0, 10)}`;
|
|
2349
|
+
}
|
|
2350
|
+
function workspaceConnectionLine(c) {
|
|
2351
|
+
const owner = c.workspace_owned ? "the workspace" : c.mine ? "you" : c.shared_by?.name ?? c.shared_by?.user_id ?? "someone";
|
|
2352
|
+
return `${c.connection_id} ${c.app_name} (${c.app}) ${c.label} shared by ${owner}`;
|
|
2353
|
+
}
|
|
2354
|
+
function browserReturn(start, opts = {}) {
|
|
2355
|
+
return new Promise((resolve10, reject) => {
|
|
2356
|
+
const server = createServer2((req, res) => {
|
|
2357
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
2358
|
+
if (url.pathname !== "/done" && url.pathname !== "/failed") {
|
|
2359
|
+
res.writeHead(404).end();
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const ok = url.pathname === "/done";
|
|
2363
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
2364
|
+
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>");
|
|
2365
|
+
server.close();
|
|
2366
|
+
if (ok) resolve10(url.searchParams.get("account_id") ?? void 0);
|
|
2367
|
+
else reject(new ApiError(400, "connect_failed", "the app was not connected"));
|
|
2368
|
+
});
|
|
2369
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2370
|
+
const address = server.address();
|
|
2371
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
2372
|
+
const base = `http://127.0.0.1:${port}`;
|
|
2373
|
+
start(`${base}/done`, `${base}/failed`).then(
|
|
2374
|
+
(authorizeUrl) => {
|
|
2375
|
+
console.log(`Authorize access in your browser:
|
|
2376
|
+
${authorizeUrl}`);
|
|
2377
|
+
if (opts.open !== false) openBrowser2(authorizeUrl);
|
|
2378
|
+
},
|
|
2379
|
+
(err) => {
|
|
2380
|
+
server.close();
|
|
2381
|
+
reject(err);
|
|
2382
|
+
}
|
|
2383
|
+
);
|
|
2384
|
+
});
|
|
2385
|
+
setTimeout(() => {
|
|
2386
|
+
server.close();
|
|
2387
|
+
reject(new ApiError(408, "connect_timeout", "nobody finished the authorization within 10 minutes"));
|
|
2388
|
+
}, opts.timeoutMs ?? 10 * 6e4).unref();
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
async function connectApp(app, opts = {}) {
|
|
2392
|
+
const base = opts.workspace ? `/universes/${encodeURIComponent(opts.workspace)}/connections` : "/connectors/connections";
|
|
2393
|
+
const accountId = await browserReturn(async (success_url, error_url) => {
|
|
2394
|
+
const { url } = await api("POST", base, { app, success_url, error_url });
|
|
2395
|
+
return url;
|
|
2396
|
+
}, { open: opts.open });
|
|
2397
|
+
const done = await api("POST", `${base}/complete`, {
|
|
2398
|
+
app,
|
|
2399
|
+
...accountId ? { account_id: accountId } : {}
|
|
2400
|
+
});
|
|
2401
|
+
return done.connection;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// src/routine-triggers.ts
|
|
2405
|
+
var triggerPath = (id, resource) => `/routines/${encodeURIComponent(id)}/${resource}`;
|
|
2406
|
+
function inboundLine(u) {
|
|
2407
|
+
if (!u) return "no inbound URL \u2014 make one with --create";
|
|
2408
|
+
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"}`;
|
|
2409
|
+
}
|
|
2410
|
+
function precheckLine(p) {
|
|
2411
|
+
if (!p) return "no precheck: every scheduled fire runs";
|
|
2412
|
+
const last = p.last_result ? `last check ${p.last_result}${p.last_error ? ` (${p.last_error.type}: ${p.last_error.message})` : ""}` : "not checked yet";
|
|
2413
|
+
return `fires only when ${p.url} changes \xB7 ${last}`;
|
|
2414
|
+
}
|
|
2415
|
+
function triggerLine(t, available = true) {
|
|
2416
|
+
if (!t) return "no chat trigger";
|
|
2417
|
+
const where = t.thread_id ? `thread ${t.thread_id} on ${t.connection_id}` : `any conversation on ${t.connection_id}`;
|
|
2418
|
+
const what = t.text_prefix ? `messages starting "${t.text_prefix}"` : "every message";
|
|
2419
|
+
return `${what} in ${where} start a run${available ? "" : " (chat messages are not offered to routines in this deployment)"}`;
|
|
2420
|
+
}
|
|
2421
|
+
function deliverLine(d) {
|
|
2422
|
+
return d ? `answers post to thread ${d.thread_id} on ${d.connection_id}` : "answers are posted nowhere";
|
|
2423
|
+
}
|
|
2424
|
+
function triggerBody(opts) {
|
|
2425
|
+
if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
|
|
2426
|
+
return { connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {}, ...opts.prefix ? { text_prefix: opts.prefix } : {} };
|
|
2427
|
+
}
|
|
2428
|
+
function deliverBody(opts) {
|
|
2429
|
+
if (!opts.connection) throw new ApiError(400, "invalid_request", "--connection is required (see `sky channel list`)");
|
|
2430
|
+
return { type: "channel_thread", connection_id: opts.connection, ...opts.thread ? { thread_id: opts.thread } : {} };
|
|
2431
|
+
}
|
|
2432
|
+
async function triggerRequest(id, resource, change) {
|
|
2433
|
+
const path = triggerPath(id, resource);
|
|
2434
|
+
if (change.remove) return api("DELETE", path);
|
|
2435
|
+
if (change.create) return api("POST", path);
|
|
2436
|
+
if (change.body) return api("PUT", path, change.body);
|
|
2437
|
+
return api("GET", path);
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/session-workspace.ts
|
|
2441
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2442
|
+
import { createHash as createHash5, randomUUID } from "crypto";
|
|
2443
|
+
import { readdirSync as readdirSync4, statSync as statSync6 } from "fs";
|
|
2444
|
+
import { basename as basename6, join as join7, relative as relative3, resolve as resolve5 } from "path";
|
|
2445
|
+
var SINGLE_IMPORT_MAX = 25 * 1024 * 1024;
|
|
2446
|
+
var PART_SIZE = 8 * 1024 * 1024;
|
|
2447
|
+
var SKIPPED2 = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2448
|
+
var workspacePath = (id) => `/sessions/${encodeURIComponent(id)}/workspace`;
|
|
2449
|
+
async function listFiles2(id, path, hidden) {
|
|
2450
|
+
const query = new URLSearchParams();
|
|
2451
|
+
if (path) query.set("path", path);
|
|
2452
|
+
if (hidden) query.set("hidden", "true");
|
|
2453
|
+
const qs = query.toString();
|
|
2454
|
+
return api("GET", `${workspacePath(id)}/files${qs ? `?${qs}` : ""}`);
|
|
2455
|
+
}
|
|
2456
|
+
function entryLine(e) {
|
|
2457
|
+
const size = e.type === "directory" ? "-" : String(e.size ?? 0);
|
|
2458
|
+
return `${e.type === "directory" ? "d" : "-"} ${size.padStart(10)} ${(e.mtime ?? "").slice(0, 16).replace("T", " ")} ${e.name}${e.type === "directory" ? "/" : ""}`;
|
|
2459
|
+
}
|
|
2460
|
+
async function readFile(id, path) {
|
|
2461
|
+
return (await apiBytes("GET", `${workspacePath(id)}/files/content?path=${encodeURIComponent(path)}`)).bytes;
|
|
2462
|
+
}
|
|
2463
|
+
function renderChanges(changes) {
|
|
2464
|
+
if (!changes.repos.length) return [`no git repository under ${changes.root}`];
|
|
2465
|
+
return changes.repos.flatMap((r) => [
|
|
2466
|
+
`${r.path === "." ? changes.root : r.path}: ${r.status ? "changed" : "clean"}`,
|
|
2467
|
+
...(r.stat ?? r.status ?? "").split("\n").filter(Boolean).map((line) => ` ${line}`)
|
|
2468
|
+
]);
|
|
2469
|
+
}
|
|
2470
|
+
async function workspacePatch(id, repo) {
|
|
2471
|
+
const { patch, ...meta } = await api("POST", `${workspacePath(id)}/patch`, { repo });
|
|
2472
|
+
return { meta, patch: Buffer.from(patch, "base64") };
|
|
2473
|
+
}
|
|
2474
|
+
function folderFiles(dir) {
|
|
2475
|
+
const git2 = spawnSync3("git", ["-C", dir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8" });
|
|
2476
|
+
const top = spawnSync3("git", ["-C", dir, "rev-parse", "--show-prefix"], { encoding: "utf8" });
|
|
2477
|
+
if (git2.status === 0 && top.status === 0) {
|
|
2478
|
+
return git2.stdout.split("\0").filter((path) => path && statSync6(join7(dir, path), { throwIfNoEntry: false })?.isFile());
|
|
2479
|
+
}
|
|
2480
|
+
const files2 = [];
|
|
2481
|
+
const walk = (at) => {
|
|
2482
|
+
for (const entry2 of readdirSync4(at, { withFileTypes: true })) {
|
|
2483
|
+
if (SKIPPED2.has(entry2.name)) continue;
|
|
2484
|
+
const full = join7(at, entry2.name);
|
|
2485
|
+
if (entry2.isDirectory()) walk(full);
|
|
2486
|
+
else if (entry2.isFile()) files2.push(relative3(dir, full));
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
walk(dir);
|
|
2490
|
+
return files2;
|
|
2491
|
+
}
|
|
2492
|
+
function folderArchive(dir, files2) {
|
|
2493
|
+
const tar = spawnSync3("tar", ["--format=ustar", "-czf", "-", "-C", dir, "-T", "-"], {
|
|
2494
|
+
input: files2.join("\n"),
|
|
2495
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
2496
|
+
maxBuffer: 2 * 1024 * 1024 * 1024
|
|
2497
|
+
});
|
|
2498
|
+
if (tar.status !== 0) throw new ApiError(400, "archive_failed", `tar failed: ${tar.stderr.toString().trim()}`);
|
|
2499
|
+
return tar.stdout;
|
|
2500
|
+
}
|
|
2501
|
+
async function importFolder(id, dir, opts = {}) {
|
|
2502
|
+
const root = resolve5(dir);
|
|
2503
|
+
const name = opts.name ?? basename6(root);
|
|
2504
|
+
const archive = opts.archive ?? folderArchive(root, folderFiles(root));
|
|
2505
|
+
if (archive.length <= SINGLE_IMPORT_MAX) {
|
|
2506
|
+
const res = await apiBytes("POST", `${workspacePath(id)}/import?name=${encodeURIComponent(name)}`, { bytes: archive, mediaType: "application/gzip" });
|
|
2507
|
+
return res.json();
|
|
2508
|
+
}
|
|
2509
|
+
const sha256 = createHash5("sha256").update(archive).digest("hex");
|
|
2510
|
+
let upload = await api("POST", `${workspacePath(id)}/imports`, { name, size: archive.length, sha256, part_size: PART_SIZE });
|
|
2511
|
+
const uploadPath = `${workspacePath(id)}/imports/${encodeURIComponent(upload.id)}`;
|
|
2512
|
+
for (let part = 0; part < upload.parts; part += 1) {
|
|
2513
|
+
if (upload.received.includes(part)) continue;
|
|
2514
|
+
const bytes = archive.subarray(part * upload.part_size, (part + 1) * upload.part_size);
|
|
2515
|
+
await apiBytes("PUT", `${uploadPath}/parts/${part}`, { bytes, mediaType: "application/octet-stream" });
|
|
2516
|
+
}
|
|
2517
|
+
upload = await api("POST", `${uploadPath}/complete`);
|
|
2518
|
+
while (upload.state !== "imported" && upload.state !== "failed") {
|
|
2519
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
|
|
2520
|
+
upload = await api("GET", uploadPath);
|
|
2521
|
+
}
|
|
2522
|
+
if (upload.state === "failed") throw new ApiError(422, upload.error?.code ?? "import_failed", upload.error?.message ?? "the import failed");
|
|
2523
|
+
return { path: upload.path ?? name, files: upload.files ?? 0 };
|
|
2524
|
+
}
|
|
2525
|
+
function renderAccess(a) {
|
|
2526
|
+
const lines = [
|
|
2527
|
+
`${a.session_id}${a.title ? ` \u2014 ${a.title}` : ""}`,
|
|
2528
|
+
` ${a.visibility === "private" ? "private to its owner" : `visible to ${a.workspace.name} (${a.workspace.slug})`} \xB7 you are its ${a.access}`,
|
|
2529
|
+
` owner: ${a.owner.name ?? a.owner.email ?? "unknown"}`
|
|
2530
|
+
];
|
|
2531
|
+
if (a.viewers) lines.push(a.viewers.length ? ` viewers: ${a.viewers.map((v) => v.email).join(", ")}` : " viewers: none");
|
|
2532
|
+
return lines;
|
|
2533
|
+
}
|
|
2534
|
+
function accessPatch(opts) {
|
|
2535
|
+
if (opts.private && opts.members) throw new ApiError(400, "invalid_request", "pass --private or --members, not both");
|
|
2536
|
+
return {
|
|
2537
|
+
...opts.private ? { visibility: "private" } : opts.members ? { visibility: "workspace" } : {},
|
|
2538
|
+
...opts.addViewer?.length ? { add_viewers: opts.addViewer } : {},
|
|
2539
|
+
...opts.removeViewer?.length ? { remove_viewers: opts.removeViewer } : {}
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
function engineChange(model, thinking) {
|
|
2543
|
+
if (!model && !thinking) return null;
|
|
2544
|
+
return {
|
|
2545
|
+
...model ? { llm: model } : {},
|
|
2546
|
+
...thinking ? { reasoningEffort: thinking === "default" ? null : thinking } : {}
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
2550
|
+
async function handoff(id, targetAgent, opts = {}) {
|
|
2551
|
+
let { handoff: h } = await api("POST", `/sessions/${encodeURIComponent(id)}/handoff`, {
|
|
2552
|
+
target_agent: targetAgent,
|
|
2553
|
+
...opts.fromMessage ? { last_message_id: opts.fromMessage } : {}
|
|
2554
|
+
}, { idempotencyKey: opts.idempotencyKey ?? randomUUID() });
|
|
2555
|
+
opts.onStatus?.(h);
|
|
2556
|
+
while (opts.wait && !TERMINAL.has(h.status)) {
|
|
2557
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
|
|
2558
|
+
const next = (await api("GET", `/session-handoffs/${encodeURIComponent(h.id)}`)).handoff;
|
|
2559
|
+
if (next.status !== h.status) opts.onStatus?.(next);
|
|
2560
|
+
h = next;
|
|
2561
|
+
}
|
|
2562
|
+
return h;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
1818
2565
|
// src/local-agents.ts
|
|
1819
2566
|
import { existsSync as existsSync6 } from "fs";
|
|
1820
2567
|
import { homedir as homedir5 } from "os";
|
|
1821
|
-
import { join as
|
|
2568
|
+
import { join as join8 } from "path";
|
|
1822
2569
|
function localAgents(env = process.env, home = homedir5()) {
|
|
1823
|
-
const claude = env.CLAUDE_CONFIG_DIR ||
|
|
1824
|
-
const codex = env.CODEX_HOME ||
|
|
1825
|
-
const cursor =
|
|
2570
|
+
const claude = env.CLAUDE_CONFIG_DIR || join8(home, ".claude");
|
|
2571
|
+
const codex = env.CODEX_HOME || join8(home, ".codex");
|
|
2572
|
+
const cursor = join8(home, ".cursor");
|
|
1826
2573
|
return [
|
|
1827
|
-
{ id: "claude_code", name: "Claude Code", root: claude, skillsDir:
|
|
1828
|
-
{ id: "codex", name: "Codex", root: codex, skillsDir:
|
|
1829
|
-
{ id: "cursor", name: "Cursor", root: cursor, skillsDir:
|
|
2574
|
+
{ id: "claude_code", name: "Claude Code", root: claude, skillsDir: join8(claude, "skills") },
|
|
2575
|
+
{ id: "codex", name: "Codex", root: codex, skillsDir: join8(codex, "skills") },
|
|
2576
|
+
{ id: "cursor", name: "Cursor", root: cursor, skillsDir: join8(cursor, "skills") }
|
|
1830
2577
|
];
|
|
1831
2578
|
}
|
|
1832
2579
|
function detectLocalAgents(env = process.env, home = homedir5()) {
|
|
@@ -1926,21 +2673,21 @@ async function readTurn(sessionId, opts = {}) {
|
|
|
1926
2673
|
for (; ; ) {
|
|
1927
2674
|
const reading = latestTurn(await history(sessionId));
|
|
1928
2675
|
if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
|
|
1929
|
-
await new Promise((
|
|
2676
|
+
await new Promise((resolve10) => setTimeout(resolve10, opts.intervalMs ?? 3e3));
|
|
1930
2677
|
}
|
|
1931
2678
|
}
|
|
1932
2679
|
|
|
1933
2680
|
// src/setup.ts
|
|
1934
|
-
import { mkdirSync as
|
|
1935
|
-
import { dirname as
|
|
2681
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
|
|
2682
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
1936
2683
|
import { createInterface as createInterface3 } from "readline";
|
|
1937
2684
|
|
|
1938
2685
|
// src/mcp-register.ts
|
|
1939
|
-
import { spawnSync as
|
|
2686
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
1940
2687
|
|
|
1941
2688
|
// src/skill.ts
|
|
1942
|
-
import { mkdirSync, readFileSync as
|
|
1943
|
-
import { join as
|
|
2689
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
2690
|
+
import { join as join9 } from "path";
|
|
1944
2691
|
|
|
1945
2692
|
// src/catalog-file.ts
|
|
1946
2693
|
var CATALOG_FILE = "catalog.md";
|
|
@@ -1966,13 +2713,13 @@ function entry(agent2) {
|
|
|
1966
2713
|
return lines.join("\n");
|
|
1967
2714
|
}
|
|
1968
2715
|
function renderCatalog(agents, generatedAt) {
|
|
1969
|
-
const
|
|
2716
|
+
const header2 = [
|
|
1970
2717
|
"# AgentSky agent catalog",
|
|
1971
2718
|
"",
|
|
1972
2719
|
`Every agent you can hire, as of ${generatedAt.toISOString()}. Start one with \`start_session\` and its handle, written with the @.`,
|
|
1973
2720
|
"Run `npx @agentsky/sky setup` again to refresh this file; `search_catalog` always has the current list."
|
|
1974
2721
|
];
|
|
1975
|
-
if (!agents.length) return [...
|
|
2722
|
+
if (!agents.length) return [...header2, "", "The catalog was empty when this file was written. Use `search_catalog`.", ""].join("\n");
|
|
1976
2723
|
const own = agents.filter((agent2) => agent2.visibility === "workspace");
|
|
1977
2724
|
const shared = agents.filter((agent2) => agent2.visibility !== "workspace");
|
|
1978
2725
|
const sections = [];
|
|
@@ -1987,7 +2734,7 @@ function renderCatalog(agents, generatedAt) {
|
|
|
1987
2734
|
const title = category.charAt(0).toUpperCase() + category.slice(1);
|
|
1988
2735
|
sections.push([`## ${title}`, ...groups.get(category).map(entry)].join("\n\n"));
|
|
1989
2736
|
}
|
|
1990
|
-
return [...
|
|
2737
|
+
return [...header2, "", sections.join("\n\n"), ""].join("\n");
|
|
1991
2738
|
}
|
|
1992
2739
|
|
|
1993
2740
|
// src/skill.ts
|
|
@@ -2381,7 +3128,7 @@ To set a per-repo default, create \`.agentsky.json\` at the git root:
|
|
|
2381
3128
|
}
|
|
2382
3129
|
function installedVersion(dir) {
|
|
2383
3130
|
try {
|
|
2384
|
-
const marker = JSON.parse(
|
|
3131
|
+
const marker = JSON.parse(readFileSync9(join9(dir, SKILL_MARKER), "utf8"));
|
|
2385
3132
|
return typeof marker.version === "string" ? marker.version : null;
|
|
2386
3133
|
} catch {
|
|
2387
3134
|
return null;
|
|
@@ -2391,25 +3138,25 @@ function isInstalledSkill(dir) {
|
|
|
2391
3138
|
return installedVersion(dir) !== null;
|
|
2392
3139
|
}
|
|
2393
3140
|
function installSkill(agent2, opts = {}) {
|
|
2394
|
-
const dir =
|
|
3141
|
+
const dir = join9(agent2.skillsDir, SKILL_NAME);
|
|
2395
3142
|
const previous = installedVersion(dir);
|
|
2396
3143
|
const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
|
|
2397
3144
|
if (opts.dryRun) return { agent: agent2.id, path: dir, outcome };
|
|
2398
3145
|
if (outcome !== "current") {
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
3146
|
+
mkdirSync2(dir, { recursive: true });
|
|
3147
|
+
writeFileSync4(join9(dir, "SKILL.md"), skillDocument());
|
|
3148
|
+
writeFileSync4(join9(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
|
|
2402
3149
|
`);
|
|
2403
3150
|
}
|
|
2404
3151
|
if (opts.catalog) {
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
}
|
|
2408
|
-
const refsDir =
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
3152
|
+
mkdirSync2(dir, { recursive: true });
|
|
3153
|
+
writeFileSync4(join9(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
|
|
3154
|
+
}
|
|
3155
|
+
const refsDir = join9(dir, REFERENCES_DIR);
|
|
3156
|
+
mkdirSync2(refsDir, { recursive: true });
|
|
3157
|
+
writeFileSync4(join9(refsDir, "import.md"), importReferenceDoc());
|
|
3158
|
+
writeFileSync4(join9(refsDir, "workspaces.md"), workspacesReferenceDoc());
|
|
3159
|
+
writeFileSync4(join9(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
|
|
2413
3160
|
return { agent: agent2.id, path: dir, outcome };
|
|
2414
3161
|
}
|
|
2415
3162
|
|
|
@@ -2418,9 +3165,9 @@ var MCP_SERVER_NAME = SKILL_NAME;
|
|
|
2418
3165
|
var CODEX_TOKEN_ENV = "AGENTSKY_API_KEY";
|
|
2419
3166
|
function systemCommandRunner() {
|
|
2420
3167
|
return {
|
|
2421
|
-
has: (command) =>
|
|
3168
|
+
has: (command) => spawnSync4(command, ["--version"], { stdio: "ignore" }).status === 0,
|
|
2422
3169
|
run: (command, args) => {
|
|
2423
|
-
const result =
|
|
3170
|
+
const result = spawnSync4(command, args, { encoding: "utf8" });
|
|
2424
3171
|
return { status: result.status, stderr: result.stderr ?? "" };
|
|
2425
3172
|
}
|
|
2426
3173
|
};
|
|
@@ -2460,10 +3207,10 @@ function registerMcpServer(agent2, opts, runner = systemCommandRunner()) {
|
|
|
2460
3207
|
}
|
|
2461
3208
|
|
|
2462
3209
|
// src/setup-local.ts
|
|
2463
|
-
import { isUtf8 as
|
|
2464
|
-
import { existsSync as existsSync7, readdirSync as
|
|
3210
|
+
import { isUtf8 as isUtf83 } from "buffer";
|
|
3211
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
|
|
2465
3212
|
import { homedir as homedir6 } from "os";
|
|
2466
|
-
import { dirname as
|
|
3213
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join10, resolve as resolve6 } from "path";
|
|
2467
3214
|
import { parse as parseToml3 } from "smol-toml";
|
|
2468
3215
|
var MAX_SKILL_FILE_BYTES = 1024 * 1024;
|
|
2469
3216
|
var MAX_IMPORT_DEPTH = 5;
|
|
@@ -2477,7 +3224,7 @@ function sensitive(name) {
|
|
|
2477
3224
|
return name.startsWith(".env") || name.endsWith(".pem") || name.endsWith(".key");
|
|
2478
3225
|
}
|
|
2479
3226
|
function redacted(data) {
|
|
2480
|
-
if (data.includes(0) || !
|
|
3227
|
+
if (data.includes(0) || !isUtf83(data)) return new Uint8Array(data);
|
|
2481
3228
|
try {
|
|
2482
3229
|
const line = redactLine(JSON.stringify({ type: "import.resource", name: "skill", media_type: "text/plain", data: data.toString("base64") }), { requireStructured: true }).line;
|
|
2483
3230
|
return new Uint8Array(Buffer.from(JSON.parse(line).data, "base64"));
|
|
@@ -2487,12 +3234,12 @@ function redacted(data) {
|
|
|
2487
3234
|
}
|
|
2488
3235
|
function skillFiles2(root, prefix) {
|
|
2489
3236
|
const files2 = [];
|
|
2490
|
-
for (const entry2 of
|
|
2491
|
-
const path =
|
|
3237
|
+
for (const entry2 of readdirSync5(root, { withFileTypes: true })) {
|
|
3238
|
+
const path = join10(root, entry2.name);
|
|
2492
3239
|
if (entry2.isDirectory()) {
|
|
2493
3240
|
if (!SKIPPED_DIRS.has(entry2.name)) files2.push(...skillFiles2(path, `${prefix}/${entry2.name}`));
|
|
2494
|
-
} else if (entry2.isFile() && !sensitive(entry2.name) &&
|
|
2495
|
-
const bytes = redacted(
|
|
3241
|
+
} else if (entry2.isFile() && !sensitive(entry2.name) && statSync7(path).size <= MAX_SKILL_FILE_BYTES) {
|
|
3242
|
+
const bytes = redacted(readFileSync10(path));
|
|
2496
3243
|
if (bytes) files2.push({ path: `${prefix}/${entry2.name}`, bytes });
|
|
2497
3244
|
}
|
|
2498
3245
|
}
|
|
@@ -2503,18 +3250,18 @@ function readLocalSkills(agents) {
|
|
|
2503
3250
|
const ignored = [];
|
|
2504
3251
|
for (const agent2 of agents) {
|
|
2505
3252
|
if (!existsSync7(agent2.skillsDir)) continue;
|
|
2506
|
-
for (const entry2 of
|
|
2507
|
-
const dir =
|
|
3253
|
+
for (const entry2 of readdirSync5(agent2.skillsDir, { withFileTypes: true })) {
|
|
3254
|
+
const dir = join10(agent2.skillsDir, entry2.name);
|
|
2508
3255
|
let isDir2 = entry2.isDirectory();
|
|
2509
3256
|
if (entry2.isSymbolicLink()) {
|
|
2510
3257
|
try {
|
|
2511
|
-
isDir2 =
|
|
3258
|
+
isDir2 = statSync7(dir).isDirectory();
|
|
2512
3259
|
} catch {
|
|
2513
3260
|
isDir2 = false;
|
|
2514
3261
|
}
|
|
2515
3262
|
}
|
|
2516
|
-
if (!isDir2 || !existsSync7(
|
|
2517
|
-
const name = frontmatterName(
|
|
3263
|
+
if (!isDir2 || !existsSync7(join10(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
|
|
3264
|
+
const name = frontmatterName(readFileSync10(join10(dir, "SKILL.md"), "utf8"));
|
|
2518
3265
|
if (!name) {
|
|
2519
3266
|
ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
|
|
2520
3267
|
continue;
|
|
@@ -2542,11 +3289,11 @@ function inlineImports(content, from, home, depth, seen) {
|
|
|
2542
3289
|
}
|
|
2543
3290
|
const target = fence ? void 0 : line.match(/^@(\S+\.(?:md|markdown|txt))$/i)?.[1];
|
|
2544
3291
|
if (!target || depth >= MAX_IMPORT_DEPTH) return line;
|
|
2545
|
-
const path = target.startsWith("~/") ?
|
|
3292
|
+
const path = target.startsWith("~/") ? join10(home, target.slice(2)) : isAbsolute2(target) ? target : resolve6(dirname5(from), target);
|
|
2546
3293
|
if (seen.includes(path)) return "";
|
|
2547
3294
|
let imported;
|
|
2548
3295
|
try {
|
|
2549
|
-
imported =
|
|
3296
|
+
imported = readFileSync10(path, "utf8");
|
|
2550
3297
|
} catch {
|
|
2551
3298
|
return line;
|
|
2552
3299
|
}
|
|
@@ -2559,10 +3306,10 @@ function readGlobalInstructions(agents, home = homedir6()) {
|
|
|
2559
3306
|
for (const agent2 of agents) {
|
|
2560
3307
|
const file = INSTRUCTION_FILES2[agent2.id];
|
|
2561
3308
|
if (!file) continue;
|
|
2562
|
-
const path =
|
|
3309
|
+
const path = join10(agent2.root, file);
|
|
2563
3310
|
let raw;
|
|
2564
3311
|
try {
|
|
2565
|
-
raw =
|
|
3312
|
+
raw = readFileSync10(path, "utf8");
|
|
2566
3313
|
} catch {
|
|
2567
3314
|
continue;
|
|
2568
3315
|
}
|
|
@@ -2581,7 +3328,7 @@ ${item.content.trim()}`).join("\n\n");
|
|
|
2581
3328
|
}
|
|
2582
3329
|
function readJson3(path) {
|
|
2583
3330
|
try {
|
|
2584
|
-
return JSON.parse(
|
|
3331
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
2585
3332
|
} catch {
|
|
2586
3333
|
return void 0;
|
|
2587
3334
|
}
|
|
@@ -2595,10 +3342,10 @@ function bearerOf(def) {
|
|
|
2595
3342
|
return match?.[1] ?? null;
|
|
2596
3343
|
}
|
|
2597
3344
|
function serversOf(agent2, home) {
|
|
2598
|
-
if (agent2.id === "claude_code") return readJson3(
|
|
2599
|
-
if (agent2.id === "cursor") return readJson3(
|
|
3345
|
+
if (agent2.id === "claude_code") return readJson3(join10(home, ".claude.json"))?.mcpServers ?? {};
|
|
3346
|
+
if (agent2.id === "cursor") return readJson3(join10(agent2.root, "mcp.json"))?.mcpServers ?? {};
|
|
2600
3347
|
try {
|
|
2601
|
-
return parseToml3(
|
|
3348
|
+
return parseToml3(readFileSync10(join10(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
|
|
2602
3349
|
} catch {
|
|
2603
3350
|
return {};
|
|
2604
3351
|
}
|
|
@@ -2619,9 +3366,9 @@ function findLocalSecrets(agents, home = homedir6()) {
|
|
|
2619
3366
|
|
|
2620
3367
|
// src/subscriptions.ts
|
|
2621
3368
|
import { spawn as spawn3 } from "child_process";
|
|
2622
|
-
import { existsSync as existsSync8, readFileSync as
|
|
3369
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11 } from "fs";
|
|
2623
3370
|
import { homedir as homedir7 } from "os";
|
|
2624
|
-
import { join as
|
|
3371
|
+
import { join as join11 } from "path";
|
|
2625
3372
|
import { createInterface as createInterface2 } from "readline";
|
|
2626
3373
|
var ALIASES = {
|
|
2627
3374
|
claude: "anthropic",
|
|
@@ -2637,14 +3384,24 @@ function providerFor(name) {
|
|
|
2637
3384
|
}
|
|
2638
3385
|
return provider;
|
|
2639
3386
|
}
|
|
3387
|
+
var MANAGED_ALIASES = {
|
|
3388
|
+
...ALIASES,
|
|
3389
|
+
cursor: "cursor",
|
|
3390
|
+
grok: "xai",
|
|
3391
|
+
xai: "xai",
|
|
3392
|
+
muse: "meta",
|
|
3393
|
+
meta: "meta"
|
|
3394
|
+
};
|
|
2640
3395
|
function managedProviderFor(name) {
|
|
2641
|
-
|
|
2642
|
-
const provider = ALIASES[name.toLowerCase()];
|
|
3396
|
+
const provider = MANAGED_ALIASES[name.toLowerCase()];
|
|
2643
3397
|
if (!provider) {
|
|
2644
|
-
throw new ApiError(400, "unknown_provider", `unknown provider "${name}" \u2014 use claude, chatgpt or
|
|
3398
|
+
throw new ApiError(400, "unknown_provider", `unknown provider "${name}" \u2014 use claude, chatgpt, cursor, grok or muse`);
|
|
2645
3399
|
}
|
|
2646
3400
|
return provider;
|
|
2647
3401
|
}
|
|
3402
|
+
function accountQuery(account) {
|
|
3403
|
+
return account ? `?accountId=${encodeURIComponent(account)}` : "";
|
|
3404
|
+
}
|
|
2648
3405
|
function ask(question) {
|
|
2649
3406
|
return new Promise((done) => {
|
|
2650
3407
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
@@ -2707,11 +3464,11 @@ async function connectClaude() {
|
|
|
2707
3464
|
console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
|
|
2708
3465
|
}
|
|
2709
3466
|
function codexSignIn(env = process.env, home = homedir7()) {
|
|
2710
|
-
const path =
|
|
3467
|
+
const path = join11(env.CODEX_HOME || join11(home, ".codex"), "auth.json");
|
|
2711
3468
|
if (!existsSync8(path)) {
|
|
2712
3469
|
throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
|
|
2713
3470
|
}
|
|
2714
|
-
const raw =
|
|
3471
|
+
const raw = readFileSync11(path, "utf8");
|
|
2715
3472
|
try {
|
|
2716
3473
|
const parsed = JSON.parse(raw);
|
|
2717
3474
|
return { path, raw, account: parsed.tokens?.account_id };
|
|
@@ -2720,11 +3477,11 @@ function codexSignIn(env = process.env, home = homedir7()) {
|
|
|
2720
3477
|
}
|
|
2721
3478
|
}
|
|
2722
3479
|
function cursorSignIn(env = process.env, home = homedir7()) {
|
|
2723
|
-
const path =
|
|
3480
|
+
const path = join11(env.XDG_CONFIG_HOME || join11(home, ".config"), "cursor", "auth.json");
|
|
2724
3481
|
if (!existsSync8(path)) {
|
|
2725
3482
|
throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
|
|
2726
3483
|
}
|
|
2727
|
-
const raw =
|
|
3484
|
+
const raw = readFileSync11(path, "utf8");
|
|
2728
3485
|
try {
|
|
2729
3486
|
const parsed = JSON.parse(raw);
|
|
2730
3487
|
if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") throw new Error("shape");
|
|
@@ -2787,7 +3544,87 @@ async function disconnect(name, opts) {
|
|
|
2787
3544
|
console.log(
|
|
2788
3545
|
`${affectedAgents.length} agent${affectedAgents.length === 1 ? "" : "s"} still point at it and will fail: ` + affectedAgents.map((a) => a.agentId ?? a.id).join(", ")
|
|
2789
3546
|
);
|
|
2790
|
-
console.log(
|
|
3547
|
+
console.log(`Move them to platform credits with \`sky auth switch-to-credits ${name}\`, or reconnect.`);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
async function browserSignIn(name, opts) {
|
|
3551
|
+
const provider = managedProviderFor(name);
|
|
3552
|
+
const base = `/model-subscriptions/${provider}/logins`;
|
|
3553
|
+
const readCode = opts.readCode ?? ask;
|
|
3554
|
+
let { login: login2 } = await api("POST", base, {
|
|
3555
|
+
...opts.account ? { accountId: opts.account } : {},
|
|
3556
|
+
...opts.label ? { label: opts.label } : {}
|
|
3557
|
+
});
|
|
3558
|
+
let shown = false;
|
|
3559
|
+
let codeSent = false;
|
|
3560
|
+
const cancel = () => {
|
|
3561
|
+
if ("loginId" in login2 && login2.loginId) {
|
|
3562
|
+
void api("DELETE", `${base}/${encodeURIComponent(login2.loginId)}`).catch(() => void 0);
|
|
3563
|
+
}
|
|
3564
|
+
};
|
|
3565
|
+
process.once("SIGINT", cancel);
|
|
3566
|
+
try {
|
|
3567
|
+
for (; ; ) {
|
|
3568
|
+
if (login2.state === "connected" || login2.state === "cleanup_pending") {
|
|
3569
|
+
if (login2.state === "cleanup_pending") cancel();
|
|
3570
|
+
return login2.subscription;
|
|
3571
|
+
}
|
|
3572
|
+
if (login2.state === "failed") {
|
|
3573
|
+
if (login2.loginId) cancel();
|
|
3574
|
+
throw new ApiError(422, "sign_in_failed", login2.reason);
|
|
3575
|
+
}
|
|
3576
|
+
if (login2.state === "waiting" && !shown) {
|
|
3577
|
+
shown = true;
|
|
3578
|
+
console.log(`Sign in to ${MODEL_SUBSCRIPTION_PROVIDER_NAME[provider]} at:
|
|
3579
|
+
${login2.url}`);
|
|
3580
|
+
if (login2.userCode) console.log(`Confirm this code on that page: ${login2.userCode}`);
|
|
3581
|
+
if (opts.browser !== false) openBrowser2(login2.url);
|
|
3582
|
+
}
|
|
3583
|
+
if (login2.state === "waiting" && login2.acceptsCode && !codeSent) {
|
|
3584
|
+
const code = await readCode("Paste the code the sign-in page shows: ");
|
|
3585
|
+
if (!code) throw new ApiError(400, "no_code", "no code provided");
|
|
3586
|
+
await api("POST", `${base}/${encodeURIComponent(login2.loginId)}/code`, { code });
|
|
3587
|
+
codeSent = true;
|
|
3588
|
+
} else {
|
|
3589
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 2e3));
|
|
3590
|
+
}
|
|
3591
|
+
({ login: login2 } = await api("GET", `${base}/${encodeURIComponent(login2.loginId)}`));
|
|
3592
|
+
}
|
|
3593
|
+
} finally {
|
|
3594
|
+
process.removeListener("SIGINT", cancel);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
async function subscriptionUsage(name, account) {
|
|
3598
|
+
const provider = managedProviderFor(name);
|
|
3599
|
+
const { usage } = await api("GET", `/model-subscriptions/${provider}/usage${accountQuery(account)}`);
|
|
3600
|
+
return usage;
|
|
3601
|
+
}
|
|
3602
|
+
function renderSubscriptionUsage(name, usage) {
|
|
3603
|
+
const label = MODEL_SUBSCRIPTION_PROVIDER_NAME[managedProviderFor(name)];
|
|
3604
|
+
if (usage.status === "unavailable") return [`${label}: usage unavailable (${usage.reason ?? "unknown"})`];
|
|
3605
|
+
const head = [`${label}${usage.plan ? ` ${usage.plan}` : ""}`, usage.allowed === false ? "limit reached" : null].filter(Boolean).join(" \xB7 ");
|
|
3606
|
+
const rows = usage.windows.map(
|
|
3607
|
+
(w) => ` ${w.label}: ${Math.round(w.remainingPercent)}% left${w.resetsAt ? ` \xB7 resets ${w.resetsAt.slice(0, 16).replace("T", " ")}` : ""}`
|
|
3608
|
+
);
|
|
3609
|
+
return [head, ...rows.length ? rows : [" no usage windows reported"]];
|
|
3610
|
+
}
|
|
3611
|
+
async function switchToCredits(name, opts) {
|
|
3612
|
+
const provider = managedProviderFor(name);
|
|
3613
|
+
const label = MODEL_SUBSCRIPTION_PROVIDER_NAME[provider];
|
|
3614
|
+
if (!opts.yes) {
|
|
3615
|
+
const ok = await confirm2(`Move every agent on this ${label} account to platform credits? Their running sessions restart.`);
|
|
3616
|
+
if (!ok) {
|
|
3617
|
+
console.log("Aborted.");
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
const result = await api(
|
|
3622
|
+
"POST",
|
|
3623
|
+
`/model-subscriptions/${provider}/switch-to-credits${accountQuery(opts.account)}`
|
|
3624
|
+
);
|
|
3625
|
+
console.log(`Moved ${result.switched} agent${result.switched === 1 ? "" : "s"} to platform credits.`);
|
|
3626
|
+
if (result.restartFailures.length) {
|
|
3627
|
+
console.log(`Couldn't restart: ${result.restartFailures.join(", ")} \u2014 they pick up credits on their next start.`);
|
|
2791
3628
|
}
|
|
2792
3629
|
}
|
|
2793
3630
|
|
|
@@ -2803,11 +3640,11 @@ function planProviders(names2) {
|
|
|
2803
3640
|
}));
|
|
2804
3641
|
}
|
|
2805
3642
|
function reportPath() {
|
|
2806
|
-
return
|
|
3643
|
+
return join12(dirname6(configPath()), "setup.json");
|
|
2807
3644
|
}
|
|
2808
3645
|
function loadReport() {
|
|
2809
3646
|
try {
|
|
2810
|
-
const saved = JSON.parse(
|
|
3647
|
+
const saved = JSON.parse(readFileSync12(reportPath(), "utf8"));
|
|
2811
3648
|
return { agents: saved.agents ?? [], plans: saved.plans ?? [], ...saved.secrets ? { secrets: saved.secrets } : {} };
|
|
2812
3649
|
} catch {
|
|
2813
3650
|
return { agents: [], plans: [] };
|
|
@@ -2820,8 +3657,8 @@ function mergeReport(base, next) {
|
|
|
2820
3657
|
return { agents, plans, ...secrets2 ? { secrets: secrets2 } : {} };
|
|
2821
3658
|
}
|
|
2822
3659
|
function saveReport(report) {
|
|
2823
|
-
|
|
2824
|
-
|
|
3660
|
+
mkdirSync3(dirname6(reportPath()), { recursive: true });
|
|
3661
|
+
writeFileSync5(reportPath(), `${JSON.stringify(report, null, 2)}
|
|
2825
3662
|
`, { mode: 384 });
|
|
2826
3663
|
}
|
|
2827
3664
|
function confirm3(question) {
|
|
@@ -2908,7 +3745,7 @@ async function connectedProviders() {
|
|
|
2908
3745
|
var WORKSPACE_VAULT_NAME = "default";
|
|
2909
3746
|
async function findWorkspaceVault() {
|
|
2910
3747
|
const { vaults } = await api("GET", "/vaults");
|
|
2911
|
-
return vaults.find((
|
|
3748
|
+
return vaults.find((vault2) => vault2.display_name === WORKSPACE_VAULT_NAME)?.id ?? null;
|
|
2912
3749
|
}
|
|
2913
3750
|
async function workspaceVaultId() {
|
|
2914
3751
|
const existing = await findWorkspaceVault();
|
|
@@ -3170,17 +4007,17 @@ function renderArtifact(artifact2) {
|
|
|
3170
4007
|
}
|
|
3171
4008
|
|
|
3172
4009
|
// src/skills-store.ts
|
|
3173
|
-
import { readdirSync as
|
|
3174
|
-
import { basename as
|
|
4010
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
|
|
4011
|
+
import { basename as basename7, join as join13, relative as relative4, resolve as resolve7 } from "path";
|
|
3175
4012
|
function skillBundle(dir) {
|
|
3176
|
-
const root =
|
|
3177
|
-
const top =
|
|
4013
|
+
const root = resolve7(dir);
|
|
4014
|
+
const top = basename7(root);
|
|
3178
4015
|
const files2 = [];
|
|
3179
4016
|
const walk = (path) => {
|
|
3180
|
-
for (const name of
|
|
3181
|
-
const full =
|
|
3182
|
-
if (
|
|
3183
|
-
else files2.push({ path: `${top}/${
|
|
4017
|
+
for (const name of readdirSync6(path).sort()) {
|
|
4018
|
+
const full = join13(path, name);
|
|
4019
|
+
if (statSync8(full).isDirectory()) walk(full);
|
|
4020
|
+
else files2.push({ path: `${top}/${relative4(root, full)}`, bytes: readFileSync13(full) });
|
|
3184
4021
|
}
|
|
3185
4022
|
};
|
|
3186
4023
|
walk(root);
|
|
@@ -3190,7 +4027,7 @@ function skillBundle(dir) {
|
|
|
3190
4027
|
async function uploadSkill(dir, displayName) {
|
|
3191
4028
|
const form = new FormData();
|
|
3192
4029
|
for (const file of skillBundle(dir)) form.append("files", new Blob([new Uint8Array(file.bytes)]), file.path);
|
|
3193
|
-
form.append("displayName", displayName ??
|
|
4030
|
+
form.append("displayName", displayName ?? basename7(resolve7(dir)));
|
|
3194
4031
|
return apiForm("POST", "/skills", form);
|
|
3195
4032
|
}
|
|
3196
4033
|
async function listSkills() {
|
|
@@ -3222,9 +4059,9 @@ async function workspaceSlug(explicit) {
|
|
|
3222
4059
|
if (current) return current;
|
|
3223
4060
|
return (await api("GET", "/whoami")).universe.slug;
|
|
3224
4061
|
}
|
|
3225
|
-
var
|
|
4062
|
+
var workspacePath2 = (slug) => `/universes/${encodeURIComponent(slug)}`;
|
|
3226
4063
|
async function listMembers(slug) {
|
|
3227
|
-
return api("GET", `${
|
|
4064
|
+
return api("GET", `${workspacePath2(slug)}/members`);
|
|
3228
4065
|
}
|
|
3229
4066
|
function findMember(members, ref) {
|
|
3230
4067
|
const needle = ref.toLowerCase();
|
|
@@ -3243,20 +4080,20 @@ function inviteBody(emails, options) {
|
|
|
3243
4080
|
};
|
|
3244
4081
|
}
|
|
3245
4082
|
async function invite(slug, emails, options) {
|
|
3246
|
-
return api("POST", `${
|
|
4083
|
+
return api("POST", `${workspacePath2(slug)}/invites`, inviteBody(emails, options));
|
|
3247
4084
|
}
|
|
3248
4085
|
async function withdrawInvite(slug, inviteId) {
|
|
3249
|
-
await api("DELETE", `${
|
|
4086
|
+
await api("DELETE", `${workspacePath2(slug)}/invites/${encodeURIComponent(inviteId)}`);
|
|
3250
4087
|
}
|
|
3251
4088
|
async function setRole(slug, memberRef, role) {
|
|
3252
4089
|
if (role !== "admin" && role !== "member") throw new ApiError(400, "invalid_request", "a role is admin or member");
|
|
3253
4090
|
const member = findMember((await listMembers(slug)).members, memberRef);
|
|
3254
|
-
await api("PATCH", `${
|
|
4091
|
+
await api("PATCH", `${workspacePath2(slug)}/members/${encodeURIComponent(member.user_id)}`, { role });
|
|
3255
4092
|
return member;
|
|
3256
4093
|
}
|
|
3257
4094
|
async function removeMember(slug, memberRef) {
|
|
3258
4095
|
const member = findMember((await listMembers(slug)).members, memberRef);
|
|
3259
|
-
await api("DELETE", `${
|
|
4096
|
+
await api("DELETE", `${workspacePath2(slug)}/members/${encodeURIComponent(member.user_id)}`);
|
|
3260
4097
|
return member;
|
|
3261
4098
|
}
|
|
3262
4099
|
async function joinWorkspace(token) {
|
|
@@ -3289,7 +4126,7 @@ function policyRuleBody(options) {
|
|
|
3289
4126
|
...options.disabled ? { enabled: false } : {}
|
|
3290
4127
|
};
|
|
3291
4128
|
}
|
|
3292
|
-
var policiesPath = (scope) => "session" in scope ? `/sessions/${encodeURIComponent(scope.session)}/policies` : `${
|
|
4129
|
+
var policiesPath = (scope) => "session" in scope ? `/sessions/${encodeURIComponent(scope.session)}/policies` : `${workspacePath2(scope.slug)}/policies`;
|
|
3293
4130
|
async function listPolicies(scope, effective = false) {
|
|
3294
4131
|
if (effective && "session" in scope) return api("GET", `/sessions/${encodeURIComponent(scope.session)}/effective-policy`);
|
|
3295
4132
|
return api("GET", policiesPath(scope));
|
|
@@ -3353,20 +4190,99 @@ function typedConfirmation(slug) {
|
|
|
3353
4190
|
});
|
|
3354
4191
|
}
|
|
3355
4192
|
|
|
4193
|
+
// src/vaults.ts
|
|
4194
|
+
import { createInterface as createInterface5 } from "readline";
|
|
4195
|
+
var vaultPath = (id) => `/vaults/${encodeURIComponent(id)}`;
|
|
4196
|
+
function secretLine(s) {
|
|
4197
|
+
const target = s.secret_name ? `$${s.secret_name}` : s.mcp_server_url ?? "";
|
|
4198
|
+
return `${s.id} ${s.auth_type.padEnd(20)} ${s.display_name} ${target} ${s.status}${s.expires_at ? ` expires ${s.expires_at.slice(0, 10)}` : ""}`;
|
|
4199
|
+
}
|
|
4200
|
+
async function readSecretValue(prompt, input = process.stdin) {
|
|
4201
|
+
if (!input.isTTY) {
|
|
4202
|
+
const chunks = [];
|
|
4203
|
+
for await (const chunk of input) chunks.push(chunk);
|
|
4204
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
4205
|
+
}
|
|
4206
|
+
const rl = createInterface5({ input, output: process.stdout, terminal: true });
|
|
4207
|
+
const write = rl._writeToOutput.bind(rl);
|
|
4208
|
+
let prompted = false;
|
|
4209
|
+
rl._writeToOutput = (s) => {
|
|
4210
|
+
if (!prompted) {
|
|
4211
|
+
prompted = true;
|
|
4212
|
+
write(s);
|
|
4213
|
+
}
|
|
4214
|
+
};
|
|
4215
|
+
const value = await new Promise((done) => rl.question(prompt, done));
|
|
4216
|
+
rl.close();
|
|
4217
|
+
process.stdout.write("\n");
|
|
4218
|
+
return value;
|
|
4219
|
+
}
|
|
4220
|
+
function secretBody(input) {
|
|
4221
|
+
if (input.kind === "mcp") {
|
|
4222
|
+
return { display_name: input.displayName ?? new URL(input.url).host, auth: { type: "static_bearer", mcp_server_url: input.url, token: input.token } };
|
|
4223
|
+
}
|
|
4224
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(input.name)) {
|
|
4225
|
+
throw new ApiError(400, "invalid_name", `"${input.name}" is not an environment variable name`);
|
|
4226
|
+
}
|
|
4227
|
+
if (!input.hosts.length) throw new ApiError(400, "no_hosts", "name at least one --host the secret may be sent to");
|
|
4228
|
+
const injection = {
|
|
4229
|
+
header: true,
|
|
4230
|
+
body: false,
|
|
4231
|
+
...input.header ? { header_name: input.header } : {},
|
|
4232
|
+
...input.template ? { value_template: input.template } : {}
|
|
4233
|
+
};
|
|
4234
|
+
return {
|
|
4235
|
+
display_name: input.displayName ?? input.name,
|
|
4236
|
+
auth: { type: "environment_variable", secret_name: input.name, value: input.value, allowed_hosts: input.hosts, injection_location: injection }
|
|
4237
|
+
};
|
|
4238
|
+
}
|
|
4239
|
+
async function listVaults() {
|
|
4240
|
+
return (await api("GET", "/vaults")).vaults;
|
|
4241
|
+
}
|
|
4242
|
+
async function listSecrets(vaultId) {
|
|
4243
|
+
return (await api("GET", `${vaultPath(vaultId)}/secrets`)).secrets;
|
|
4244
|
+
}
|
|
4245
|
+
async function addSecret(vaultId, input) {
|
|
4246
|
+
return (await api("POST", `${vaultPath(vaultId)}/secrets`, secretBody(input))).secret;
|
|
4247
|
+
}
|
|
4248
|
+
async function updateSecret(vaultId, secretId, patch) {
|
|
4249
|
+
const body = {
|
|
4250
|
+
...patch.value !== void 0 ? { value: patch.value } : {},
|
|
4251
|
+
...patch.token !== void 0 ? { token: patch.token } : {},
|
|
4252
|
+
...patch.hosts?.length ? { allowed_hosts: patch.hosts } : {},
|
|
4253
|
+
...patch.displayName ? { display_name: patch.displayName } : {}
|
|
4254
|
+
};
|
|
4255
|
+
if (!Object.keys(body).length) throw new ApiError(400, "nothing_to_change", "pass a new value, --host or --name");
|
|
4256
|
+
return (await api("PATCH", `${vaultPath(vaultId)}/secrets/${encodeURIComponent(secretId)}`, body)).secret;
|
|
4257
|
+
}
|
|
4258
|
+
async function moveSecrets(intoVault, from, dryRun) {
|
|
4259
|
+
const result = await api("POST", `${vaultPath(intoVault)}/secrets/move`, { from, ...dryRun ? { dry_run: true } : {} });
|
|
4260
|
+
return result.secrets;
|
|
4261
|
+
}
|
|
4262
|
+
function confirmPrompt(question) {
|
|
4263
|
+
return new Promise((done) => {
|
|
4264
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
4265
|
+
rl.question(`${question} [y/N] `, (answer) => {
|
|
4266
|
+
rl.close();
|
|
4267
|
+
done(/^y(es)?$/i.test(answer.trim()));
|
|
4268
|
+
});
|
|
4269
|
+
});
|
|
4270
|
+
}
|
|
4271
|
+
|
|
3356
4272
|
// src/harness.ts
|
|
3357
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
3358
|
-
import { dirname as
|
|
4273
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
|
|
4274
|
+
import { dirname as dirname7, join as join14, resolve as resolve8 } from "path";
|
|
3359
4275
|
var HARNESS_FILE = "harness.json";
|
|
3360
4276
|
function harnessFilePath(dir) {
|
|
3361
|
-
return
|
|
4277
|
+
return join14(resolve8(dir), HARNESS_FILE);
|
|
3362
4278
|
}
|
|
3363
4279
|
function readHarnessFile(dir) {
|
|
3364
4280
|
const path = harnessFilePath(dir);
|
|
3365
4281
|
if (!existsSync9(path)) {
|
|
3366
|
-
throw new Error(`no ${HARNESS_FILE} found in ${
|
|
4282
|
+
throw new Error(`no ${HARNESS_FILE} found in ${resolve8(dir)} \u2014 run \`sky harness init\` first`);
|
|
3367
4283
|
}
|
|
3368
4284
|
try {
|
|
3369
|
-
return JSON.parse(
|
|
4285
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
3370
4286
|
} catch (err) {
|
|
3371
4287
|
throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3372
4288
|
}
|
|
@@ -3396,8 +4312,8 @@ function harnessInit(dir) {
|
|
|
3396
4312
|
if (existsSync9(path)) {
|
|
3397
4313
|
throw new Error(`${path} already exists \u2014 delete it to re-init`);
|
|
3398
4314
|
}
|
|
3399
|
-
|
|
3400
|
-
|
|
4315
|
+
mkdirSync4(dirname7(path), { recursive: true });
|
|
4316
|
+
writeFileSync6(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
|
|
3401
4317
|
`);
|
|
3402
4318
|
console.log(`Created ${path}`);
|
|
3403
4319
|
console.log(INIT_COMMENT);
|
|
@@ -3534,6 +4450,18 @@ program.hook("preAction", () => {
|
|
|
3534
4450
|
const workspace2 = program.opts().workspace;
|
|
3535
4451
|
if (workspace2) process.env.SKY_WORKSPACE = workspace2;
|
|
3536
4452
|
});
|
|
4453
|
+
var lastProgressDecile = -1;
|
|
4454
|
+
function uploadProgress(sent, total) {
|
|
4455
|
+
const percent = Math.floor(sent / total * 100);
|
|
4456
|
+
const line = `uploading ${(sent / 1024 / 1024).toFixed(0)} / ${(total / 1024 / 1024).toFixed(0)} MiB (${percent}%)`;
|
|
4457
|
+
if (process.stderr.isTTY) {
|
|
4458
|
+
process.stderr.write(`\r${line}${sent >= total ? "\n" : ""}`);
|
|
4459
|
+
} else if (Math.floor(percent / 10) !== lastProgressDecile) {
|
|
4460
|
+
lastProgressDecile = Math.floor(percent / 10);
|
|
4461
|
+
process.stderr.write(`${line}
|
|
4462
|
+
`);
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
3537
4465
|
function fail(error) {
|
|
3538
4466
|
if (error instanceof ApiError) console.error(`sky: ${error.message} (${error.code})`);
|
|
3539
4467
|
else console.error(`sky: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -3556,13 +4484,26 @@ auth.command("whoami").description("who the current token acts as").action(
|
|
|
3556
4484
|
);
|
|
3557
4485
|
})
|
|
3558
4486
|
);
|
|
3559
|
-
auth.command("connect <provider>").description("connect a model subscription: claude (setup-token) or chatgpt (Codex sign-in)").option("-y, --yes", "skip the consent prompt").action(
|
|
4487
|
+
auth.command("connect <provider>").description("connect a model subscription: claude (setup-token) or chatgpt (Codex sign-in); --browser signs any plan in on its own page").option("-y, --yes", "skip the consent prompt").option("--browser", "sign in on the provider's page instead of using this machine's sign-in (claude, chatgpt, cursor, grok, muse)").option("--account <id>", "with --browser: re-sign an additional account, or `new` to add one").option("--label <label>", "with --browser: name the account").option("--no-open", "with --browser: print the sign-in link without opening it").action(
|
|
3560
4488
|
run(async (name, opts) => {
|
|
4489
|
+
if (opts.browser) {
|
|
4490
|
+
const sub = await browserSignIn(name, { account: opts.account, label: opts.label, browser: opts.open });
|
|
4491
|
+
console.log(`Connected ${MODEL_SUBSCRIPTION_PROVIDER_NAME[sub.provider]} (${sub.label}). Powers new eligible agents.`);
|
|
4492
|
+
return;
|
|
4493
|
+
}
|
|
3561
4494
|
const provider = providerFor(name);
|
|
3562
4495
|
if (provider === "anthropic") await connectClaude();
|
|
3563
4496
|
else await connectChatgpt(opts);
|
|
3564
4497
|
})
|
|
3565
4498
|
);
|
|
4499
|
+
auth.command("usage <provider>").description("plan allowance left on a connected model subscription").option("--account <id>", "an additional account (default: the provider's first account)").option("--json", "print the raw usage").action(
|
|
4500
|
+
run(async (name, opts) => {
|
|
4501
|
+
const usage = await subscriptionUsage(name, opts.account);
|
|
4502
|
+
if (opts.json) console.log(JSON.stringify(usage, null, 2));
|
|
4503
|
+
else for (const line of renderSubscriptionUsage(name, usage)) console.log(line);
|
|
4504
|
+
})
|
|
4505
|
+
);
|
|
4506
|
+
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)));
|
|
3566
4507
|
auth.command("accounts").description("list connected model subscriptions and the agents they power").action(run(async () => listSubscriptions()));
|
|
3567
4508
|
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)));
|
|
3568
4509
|
var universe = program.command("universe").description("grouping boundary for agents");
|
|
@@ -3774,7 +4715,7 @@ agent.command("destroy <id>").description("delete the spec; requires all session
|
|
|
3774
4715
|
console.log(`Destroyed ${id}`);
|
|
3775
4716
|
})
|
|
3776
4717
|
);
|
|
3777
|
-
agent.command("message <id> [text]").description("send one message to the default session and stream the turn until idle").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").action(
|
|
4718
|
+
agent.command("message <id> [text]").description("send one message to the default session and stream the turn until idle").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
|
|
3778
4719
|
run(
|
|
3779
4720
|
async (id, text, opts) => sendAndFollow(await resolveSessionRef(id), text ?? "", opts)
|
|
3780
4721
|
)
|
|
@@ -3884,7 +4825,7 @@ session.command("delete <id>").description("permanently delete the session, its
|
|
|
3884
4825
|
console.log(`Deleted ${id}`);
|
|
3885
4826
|
})
|
|
3886
4827
|
);
|
|
3887
|
-
session.command("message <ref> [text]").description("send one message (session id, or agent id = its default session)").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").action(
|
|
4828
|
+
session.command("message <ref> [text]").description("send one message (session id, or agent id = its default session)").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
|
|
3888
4829
|
run(
|
|
3889
4830
|
async (ref, text, opts) => sendAndFollow(await resolveSessionRef(ref), text ?? "", opts)
|
|
3890
4831
|
)
|
|
@@ -4026,6 +4967,89 @@ sessionPorts.command("revoke <ref> <port>").description("stop exposing a port; i
|
|
|
4026
4967
|
console.log(`Revoked port ${port}`);
|
|
4027
4968
|
})
|
|
4028
4969
|
);
|
|
4970
|
+
session.command("access <ref>").description("who may use the session; --private/--members and --add-viewer/--remove-viewer change it (owner)").option("--private", "only you can use it").option("--members", "every member of its workspace can read and prompt it").option("--add-viewer <email...>", "people who may read its messages from any workspace").option("--remove-viewer <email...>", "take viewers off").action(
|
|
4971
|
+
run(async (ref, opts) => {
|
|
4972
|
+
const id = await resolveSessionRef(ref);
|
|
4973
|
+
const patch = accessPatch(opts);
|
|
4974
|
+
const path = `/sessions/${encodeURIComponent(id)}/access`;
|
|
4975
|
+
const access = await api(Object.keys(patch).length ? "PATCH" : "GET", path, Object.keys(patch).length ? patch : void 0);
|
|
4976
|
+
for (const line of renderAccess(access)) console.log(line);
|
|
4977
|
+
})
|
|
4978
|
+
);
|
|
4979
|
+
session.command("shared").description("sessions other people shared with you to read, newest first").option("--limit <n>", "at most this many (1\u2013100, default 50)").action(
|
|
4980
|
+
run(async (opts) => {
|
|
4981
|
+
const { sessions } = await api("GET", `/shared-sessions${opts.limit ? `?limit=${encodeURIComponent(opts.limit)}` : ""}`);
|
|
4982
|
+
if (!sessions.length) console.log("nobody has shared a session with you");
|
|
4983
|
+
for (const s of sessions) console.log(`${s.id} ${s.agent_type.padEnd(12)} ${s.title ?? "(untitled)"} from ${s.owner.name ?? s.owner.email ?? "unknown"} ${s.shared_at.slice(0, 10)}`);
|
|
4984
|
+
})
|
|
4985
|
+
);
|
|
4986
|
+
session.command("model <ref> [model]").description("show the session's model and thinking level, or switch them; the next turn uses the new ones").option("--thinking <level>", `thinking level for the next turns; "default" returns to the agent's own`).action(
|
|
4987
|
+
run(async (ref, model, opts) => {
|
|
4988
|
+
const id = await resolveSessionRef(ref);
|
|
4989
|
+
const change = engineChange(model, opts.thinking);
|
|
4990
|
+
const { session: s } = change ? await api("PATCH", `/sessions/${encodeURIComponent(id)}`, change) : await api("GET", `/sessions/${encodeURIComponent(id)}`);
|
|
4991
|
+
console.log(`${id}: ${s.llm}${s.reasoningEffort ? ` \xB7 thinking ${s.reasoningEffort}` : ""}${change ? " (from the next turn)" : ""}`);
|
|
4992
|
+
})
|
|
4993
|
+
);
|
|
4994
|
+
session.command("handoff <ref> <targetAgent>").description("hand the session's context and workspace to another agent (agent id or catalog handle); prints the new session").option("--from-message <id>", "carry the conversation up to this message only").option("--model <id>", "the model the new session runs").option("--thinking <level>", "the new session's thinking level").option("--no-wait", "return once the handoff is accepted").action(
|
|
4995
|
+
run(async (ref, targetAgent, opts) => {
|
|
4996
|
+
const id = await resolveSessionRef(ref);
|
|
4997
|
+
const change = engineChange(opts.model, opts.thinking);
|
|
4998
|
+
if (change && opts.wait === false) throw new ApiError(400, "invalid_request", "--model and --thinking need the handoff to finish; drop --no-wait");
|
|
4999
|
+
const done = await handoff(id, targetAgent, {
|
|
5000
|
+
fromMessage: opts.fromMessage,
|
|
5001
|
+
wait: opts.wait !== false,
|
|
5002
|
+
onStatus: (h) => console.error(`${h.id}: ${h.status}`)
|
|
5003
|
+
});
|
|
5004
|
+
if (done.status === "failed") throw new ApiError(422, done.error?.code ?? "handoff_failed", done.error?.message ?? "the handoff failed");
|
|
5005
|
+
if (done.target_session && change) await api("PATCH", `/sessions/${encodeURIComponent(done.target_session)}`, change);
|
|
5006
|
+
if (done.target_session) console.log(`${done.target_session} (${done.resolved_mode === "native_fork" ? "forked" : "from a written summary"})`);
|
|
5007
|
+
})
|
|
5008
|
+
);
|
|
5009
|
+
var sessionWorkspace = session.command("workspace").description("the session's workspace: list and read files, see changes, take a patch, copy a folder in");
|
|
5010
|
+
sessionWorkspace.command("ls <ref> [path]").description("list a directory (default: the workspace root)").option("--hidden", "include dotfiles").action(
|
|
5011
|
+
run(async (ref, path, opts) => {
|
|
5012
|
+
const listing = await listFiles2(await resolveSessionRef(ref), path, Boolean(opts.hidden));
|
|
5013
|
+
for (const e of listing.entries) console.log(entryLine(e));
|
|
5014
|
+
if (listing.truncated) console.error(`(only the first ${listing.entries.length} entries)`);
|
|
5015
|
+
})
|
|
5016
|
+
);
|
|
5017
|
+
sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1 MB), or save it with --out").option("--out <file>", "write it to this local file").action(
|
|
5018
|
+
run(async (ref, path, opts) => {
|
|
5019
|
+
const bytes = await readFile(await resolveSessionRef(ref), path);
|
|
5020
|
+
if (opts.out) {
|
|
5021
|
+
writeFileSync7(opts.out, bytes);
|
|
5022
|
+
console.error(`saved ${bytes.length} bytes to ${opts.out}`);
|
|
5023
|
+
} else process.stdout.write(bytes);
|
|
5024
|
+
})
|
|
5025
|
+
);
|
|
5026
|
+
sessionWorkspace.command("changes <ref>").description("git status and diff summary of each repository in the workspace").action(
|
|
5027
|
+
run(async (ref) => {
|
|
5028
|
+
const changes = await api("GET", `/sessions/${encodeURIComponent(await resolveSessionRef(ref))}/workspace/changes`);
|
|
5029
|
+
for (const line of renderChanges(changes)) console.log(line);
|
|
5030
|
+
})
|
|
5031
|
+
);
|
|
5032
|
+
sessionWorkspace.command("patch <ref> [repo]").description("the work in a repository since its base branch, as a git patch (default repo: the root)").option("--out <file>", "write the patch to this file instead of stdout").action(
|
|
5033
|
+
run(async (ref, repo, opts) => {
|
|
5034
|
+
const { meta, patch } = await workspacePatch(await resolveSessionRef(ref), repo ?? ".");
|
|
5035
|
+
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)})`);
|
|
5036
|
+
if (!patch.length) console.error("nothing changed");
|
|
5037
|
+
else if (opts.out) {
|
|
5038
|
+
writeFileSync7(opts.out, patch);
|
|
5039
|
+
console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
|
|
5040
|
+
} else process.stdout.write(patch);
|
|
5041
|
+
})
|
|
5042
|
+
);
|
|
5043
|
+
sessionWorkspace.command("import <ref> <dir>").description("copy a local folder into the workspace as a git repository (files git ignores are left out)").option("--name <name>", "its folder name in the workspace (default: the local folder's name)").action(
|
|
5044
|
+
run(async (ref, dir, opts) => {
|
|
5045
|
+
const imported = await importFolder(await resolveSessionRef(ref), dir, { name: opts.name });
|
|
5046
|
+
console.log(`${imported.path} (${imported.files} files)`);
|
|
5047
|
+
})
|
|
5048
|
+
);
|
|
5049
|
+
function interactionMode(opts) {
|
|
5050
|
+
if (opts.plan && opts.build) throw new ApiError(400, "invalid_request", "pass --plan or --build, not both");
|
|
5051
|
+
return opts.plan ? "plan" : opts.build ? "build" : void 0;
|
|
5052
|
+
}
|
|
4029
5053
|
async function uploadAttachments(opts) {
|
|
4030
5054
|
const files2 = [];
|
|
4031
5055
|
for (const path of [...opts.image ?? [], ...opts.file ?? []]) {
|
|
@@ -4042,7 +5066,8 @@ function turnOver(frame) {
|
|
|
4042
5066
|
return stopReason?.type !== "requires_action";
|
|
4043
5067
|
}
|
|
4044
5068
|
async function sendAndFollow(sessionId, text, opts) {
|
|
4045
|
-
const
|
|
5069
|
+
const mode = interactionMode(opts);
|
|
5070
|
+
const events = eventsFor(text, await uploadAttachments(opts), opts.system, mode);
|
|
4046
5071
|
const controller = new AbortController();
|
|
4047
5072
|
let opened = null;
|
|
4048
5073
|
if (!opts.detach) {
|
|
@@ -4067,7 +5092,7 @@ async function sendAndFollow(sessionId, text, opts) {
|
|
|
4067
5092
|
async function chatLoop(sessionId, opts = {}) {
|
|
4068
5093
|
console.log(`Chatting with ${sessionId} \u2014 Ctrl-C to leave.`);
|
|
4069
5094
|
const controller = new AbortController();
|
|
4070
|
-
const rl =
|
|
5095
|
+
const rl = createInterface6({ input: process.stdin, output: process.stdout, prompt: "> " });
|
|
4071
5096
|
let system = opts.system;
|
|
4072
5097
|
void (async () => {
|
|
4073
5098
|
try {
|
|
@@ -4181,9 +5206,9 @@ secrets.command("unset <id> <key>").action(
|
|
|
4181
5206
|
);
|
|
4182
5207
|
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(
|
|
4183
5208
|
run(async (opts) => {
|
|
4184
|
-
const path =
|
|
5209
|
+
const path = resolve9(opts.file);
|
|
4185
5210
|
if (!existsSync10(path)) {
|
|
4186
|
-
const name =
|
|
5211
|
+
const name = basename8(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
|
|
4187
5212
|
scaffoldAgentFile(path, name);
|
|
4188
5213
|
console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
|
|
4189
5214
|
}
|
|
@@ -4214,7 +5239,7 @@ program.command("launch").description("scaffold ./agent.toml if absent, create t
|
|
|
4214
5239
|
);
|
|
4215
5240
|
program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
|
|
4216
5241
|
run(async (opts) => {
|
|
4217
|
-
const path =
|
|
5242
|
+
const path = resolve9(opts.file);
|
|
4218
5243
|
const { file, spec } = readAgentFile(path);
|
|
4219
5244
|
if (!file.id) {
|
|
4220
5245
|
console.log("No agent id in the file yet \u2014 running launch instead.");
|
|
@@ -4257,7 +5282,7 @@ function formatRelative(msAgo) {
|
|
|
4257
5282
|
if (hours < 48) return `${hours}h ago`;
|
|
4258
5283
|
return `${Math.round(hours / 24)}d ago`;
|
|
4259
5284
|
}
|
|
4260
|
-
program.command("import").description("import a local Claude Code or Codex session to AgentSky and continue it in the cloud").option("--current", "import the session this agent is running in (reads CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID, then falls back to most recent session in the cwd)").option("--all", "import all found sessions up to --limit").option("-y, --yes", "confirm transfer without prompting (default when --current or --session is given in non-TTY)").option("--json", "print one JSON result object per import and nothing else to stdout").option("--limit <n>", "maximum sessions to list or import", "10").option("--session <id>", "import one specific session by its ID").option("--harness <type>", "claude_code | codex | all (default: all)").option("--summary <path>", "attach a pre-written summary JSON (asteroids.session-summary v1) instead of the auto-generated one").option("--key <importKey>", "import grant key minted by the web page (skips sky auth login)").option("--trace <traceId>", "trace id paired with --key").option("--url <base>", "agent-use app URL (default: derived from SKY_API_URL or AGENT_USE_URL)").option("--no-files", "send the conversation only, without its local files, skills, repository state or tool-server setup").action(
|
|
5285
|
+
program.command("import").description("import a local Claude Code or Codex session to AgentSky and continue it in the cloud").option("--current", "import the session this agent is running in (reads CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID, then falls back to most recent session in the cwd)").option("--all", "import all found sessions up to --limit").option("-y, --yes", "confirm transfer without prompting (default when --current or --session is given in non-TTY)").option("--json", "print one JSON result object per import and nothing else to stdout").option("--limit <n>", "maximum sessions to list or import", "10").option("--session <id>", "import one specific session by its ID").option("--harness <type>", "claude_code | codex | all (default: all)").option("--summary <path>", "attach a pre-written summary JSON (asteroids.session-summary v1) instead of the auto-generated one").option("--key <importKey>", "import grant key minted by the web page (skips sky auth login)").option("--trace <traceId>", "trace id paired with --key").option("--url <base>", "agent-use app URL (default: derived from SKY_API_URL or AGENT_USE_URL)").option("--no-files", "send the conversation only, without its local files, skills, repository state or tool-server setup").option("--no-environment", "keep the working folder on this computer: send only the files the conversation touched").action(
|
|
4261
5286
|
run(
|
|
4262
5287
|
async (opts) => {
|
|
4263
5288
|
if (opts.key && !opts.trace) throw new Error("--key requires --trace <traceId>");
|
|
@@ -4315,13 +5340,13 @@ program.command("import").description("import a local Claude Code or Codex sessi
|
|
|
4315
5340
|
console.log(` ${ws}`);
|
|
4316
5341
|
});
|
|
4317
5342
|
console.log();
|
|
4318
|
-
const rl =
|
|
5343
|
+
const rl = createInterface6({ input: process.stdin, output: process.stdout });
|
|
4319
5344
|
const answer = await new Promise(
|
|
4320
|
-
(
|
|
5345
|
+
(resolve10) => rl.question(
|
|
4321
5346
|
`Import which session? [1-${candidates.length}, or comma-separated, or "all"]: `,
|
|
4322
5347
|
(a) => {
|
|
4323
5348
|
rl.close();
|
|
4324
|
-
|
|
5349
|
+
resolve10(a.trim());
|
|
4325
5350
|
}
|
|
4326
5351
|
)
|
|
4327
5352
|
);
|
|
@@ -4343,10 +5368,10 @@ program.command("import").description("import a local Claude Code or Codex sessi
|
|
|
4343
5368
|
}
|
|
4344
5369
|
if (!autoConfirm) {
|
|
4345
5370
|
if (nonInteractive) throw new Error("Use --yes to confirm this transfer in a non-interactive terminal.");
|
|
4346
|
-
const prompt =
|
|
4347
|
-
const answer = await new Promise((
|
|
5371
|
+
const prompt = createInterface6({ input: process.stdin, output: process.stdout });
|
|
5372
|
+
const answer = await new Promise((resolve10) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
|
|
4348
5373
|
prompt.close();
|
|
4349
|
-
|
|
5374
|
+
resolve10(value);
|
|
4350
5375
|
}));
|
|
4351
5376
|
if (!/^y(?:es)?$/i.test(answer.trim())) {
|
|
4352
5377
|
console.log("Cancelled. Nothing uploaded.");
|
|
@@ -4365,6 +5390,7 @@ Importing ${label} \u2026
|
|
|
4365
5390
|
importKey: opts.key,
|
|
4366
5391
|
grantTraceId: opts.trace,
|
|
4367
5392
|
files: opts.files,
|
|
5393
|
+
environment: opts.environment,
|
|
4368
5394
|
onProgress: opts.json ? void 0 : (msg) => process.stdout.write(` ${msg}
|
|
4369
5395
|
`)
|
|
4370
5396
|
});
|
|
@@ -4384,11 +5410,14 @@ Importing ${label} \u2026
|
|
|
4384
5410
|
skills: result.setup.skills,
|
|
4385
5411
|
missingSkills: result.setup.missingSkills,
|
|
4386
5412
|
mcpServers: result.setup.mcpServers,
|
|
5413
|
+
environment: result.environment,
|
|
4387
5414
|
continueUrl: result.url
|
|
4388
5415
|
}));
|
|
4389
5416
|
} else {
|
|
4390
5417
|
if (result.setup.skills.length || result.setup.mcpServers.length) console.log(` Carried ${result.setup.skills.length} ${result.setup.skills.length === 1 ? "skill" : "skills"} and the setup of ${result.setup.mcpServers.length} tool ${result.setup.mcpServers.length === 1 ? "server" : "servers"} (their keys stay on this computer).`);
|
|
4391
5418
|
if (result.attachedFiles.length || result.leftOutFiles.length) console.log(` Attached ${result.attachedFiles.length} local ${result.attachedFiles.length === 1 ? "file" : "files"}${result.leftOutFiles.length ? `; left out ${result.leftOutFiles.length} (credential, missing, unsupported or over the 30 MiB upload)` : ""}.`);
|
|
5419
|
+
if (result.environment && "folder" in result.environment) console.log(` Uploaded the working folder ${result.environment.folder}: ${result.environment.files} files, ${(result.environment.bytes / 1024 / 1024).toFixed(1)} MiB packed${result.environment.leftOut ? `, ${result.environment.leftOut} credential or special files left on this computer` : ""}.`);
|
|
5420
|
+
else if (result.environment) console.log(` The working folder did not upload (${result.environment.error}); the conversation and the files it touched did.`);
|
|
4392
5421
|
console.log(` \u2713 Imported. Continue here:
|
|
4393
5422
|
${result.url}
|
|
4394
5423
|
`);
|
|
@@ -4400,7 +5429,7 @@ Importing ${label} \u2026
|
|
|
4400
5429
|
var files = program.command("files").description("files: upload once, reference by id from any message");
|
|
4401
5430
|
files.command("upload <path>").description("upload a local file; prints the file id to reference from a message").option("--expires <seconds>", "expire the upload after this many seconds (3600\u20137776000)").action(
|
|
4402
5431
|
run(async (path, opts) => {
|
|
4403
|
-
const file = await uploadLocalFile(path, opts.expires ? Number(opts.expires) : void 0);
|
|
5432
|
+
const file = await uploadLocalFile(path, opts.expires ? Number(opts.expires) : void 0, uploadProgress);
|
|
4404
5433
|
console.log(`${file.id} ${file.filename} ${file.mime_type} ${file.size_bytes} bytes`);
|
|
4405
5434
|
})
|
|
4406
5435
|
);
|
|
@@ -4431,9 +5460,8 @@ files.command("rm <id>").description("delete a file (a session that already rece
|
|
|
4431
5460
|
);
|
|
4432
5461
|
files.command("download <id>").description("download a session file's bytes").requiredOption("-o, --out <path>", "where to write the bytes").action(
|
|
4433
5462
|
run(async (id, opts) => {
|
|
4434
|
-
const
|
|
4435
|
-
|
|
4436
|
-
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
5463
|
+
const written = await downloadFileTo(id, opts.out);
|
|
5464
|
+
console.log(`Wrote ${written} bytes to ${opts.out}`);
|
|
4437
5465
|
})
|
|
4438
5466
|
);
|
|
4439
5467
|
var task = program.command("task").description("a task's shared files and open requests (the task is its first session's id)");
|
|
@@ -4448,14 +5476,14 @@ task.command("get <task> <path>").description("read one task file; prints it, or
|
|
|
4448
5476
|
run(async (taskId, path, opts) => {
|
|
4449
5477
|
const bytes = await readTaskFile(taskId, path);
|
|
4450
5478
|
if (opts.out) {
|
|
4451
|
-
|
|
5479
|
+
writeFileSync7(opts.out, bytes);
|
|
4452
5480
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
4453
5481
|
} else process.stdout.write(bytes);
|
|
4454
5482
|
})
|
|
4455
5483
|
);
|
|
4456
5484
|
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(
|
|
4457
5485
|
run(async (taskId, path, local) => {
|
|
4458
|
-
const { file, conflict } = await writeTaskFile(taskId, path,
|
|
5486
|
+
const { file, conflict } = await writeTaskFile(taskId, path, readFileSync15(local), mediaTypeFor(local));
|
|
4459
5487
|
console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
|
|
4460
5488
|
})
|
|
4461
5489
|
);
|
|
@@ -4504,6 +5532,28 @@ skills.command("list").description("the workspace's stored skills").action(
|
|
|
4504
5532
|
for (const skill of list) console.log(`${skill.id} ${skill.displayName ?? ""}`);
|
|
4505
5533
|
})
|
|
4506
5534
|
);
|
|
5535
|
+
async function decideReflection(id, verb) {
|
|
5536
|
+
const { reflection_proposal: p } = await api("POST", `/reflection_proposals/${encodeURIComponent(id)}/${verb}`);
|
|
5537
|
+
console.log(`${p.id} ${p.status}`);
|
|
5538
|
+
}
|
|
5539
|
+
var reflections = program.command("reflections").description("skill and lesson changes proposed after finished sessions; an owner or admin approves each one");
|
|
5540
|
+
reflections.command("list").description("the workspace's proposals, newest first (default: pending)").option("--status <status>", "pending, approved, rejected or superseded", "pending").option("--json", "print JSON").action(
|
|
5541
|
+
run(async (opts) => {
|
|
5542
|
+
const { reflection_proposals: list } = await api(
|
|
5543
|
+
"GET",
|
|
5544
|
+
`/reflection_proposals?status=${encodeURIComponent(opts.status)}`
|
|
5545
|
+
);
|
|
5546
|
+
if (opts.json) return void console.log(JSON.stringify(list, null, 2));
|
|
5547
|
+
if (!list.length) return void console.log(`no ${opts.status} proposals`);
|
|
5548
|
+
for (const p of list) {
|
|
5549
|
+
const target = p.kind === "save_lesson" ? p.path : p.kind === "create_skill" ? `new skill ${p.payload.name}` : `skill ${p.target_skill_id}`;
|
|
5550
|
+
console.log(`${p.id} ${p.kind} ${target} from ${p.session_id}
|
|
5551
|
+
${p.payload.why ?? ""}`);
|
|
5552
|
+
}
|
|
5553
|
+
})
|
|
5554
|
+
);
|
|
5555
|
+
reflections.command("approve <id>").description("write the proposal through the skill or memory store").action(run(async (id) => decideReflection(id, "approve")));
|
|
5556
|
+
reflections.command("reject <id>").description("decline the proposal; nothing is written").action(run(async (id) => decideReflection(id, "reject")));
|
|
4507
5557
|
var artifact = program.command("artifact").description("artifacts: publish a task's files as a named, shareable result");
|
|
4508
5558
|
artifact.command("list").description("the workspace's artifacts, or one task's with --task").option("--task <task>", "only this task's artifacts").action(
|
|
4509
5559
|
run(async (opts) => {
|
|
@@ -4521,7 +5571,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
|
|
|
4521
5571
|
run(async (id, path, opts) => {
|
|
4522
5572
|
const bytes = await readArtifactFile(id, path);
|
|
4523
5573
|
if (opts.out) {
|
|
4524
|
-
|
|
5574
|
+
writeFileSync7(opts.out, bytes);
|
|
4525
5575
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
4526
5576
|
} else process.stdout.write(bytes);
|
|
4527
5577
|
})
|
|
@@ -4633,6 +5683,50 @@ routine.command("runs [id]").description("every fire attempt, newest first \u201
|
|
|
4633
5683
|
}
|
|
4634
5684
|
})
|
|
4635
5685
|
);
|
|
5686
|
+
routine.command("inbound-url <id>").description("a URL that starts a run when posted to, for services that can't hold an API token").option("--create", "make one (replaces any earlier URL); the URL is printed once").option("--rm", "remove it; posting to it stops working").action(
|
|
5687
|
+
run(async (id, opts) => {
|
|
5688
|
+
const res = await triggerRequest(id, "inbound-url", { create: opts.create, remove: opts.rm });
|
|
5689
|
+
console.log(opts.rm ? res.removed ? "Removed the inbound URL" : "There was no inbound URL" : inboundLine(res.inbound_url ?? null));
|
|
5690
|
+
})
|
|
5691
|
+
);
|
|
5692
|
+
routine.command("precheck <id> [url]").description("run scheduled fires only when the content at an https URL changed; no url shows the precheck").option("--rm", "remove it: every scheduled fire runs again").action(
|
|
5693
|
+
run(async (id, url, opts) => {
|
|
5694
|
+
const res = await triggerRequest(id, "precheck", { body: url ? { url } : void 0, remove: opts.rm });
|
|
5695
|
+
console.log(opts.rm ? res.removed ? "Removed the precheck" : "There was no precheck" : precheckLine(res.precheck ?? null));
|
|
5696
|
+
})
|
|
5697
|
+
);
|
|
5698
|
+
routine.command("result-email <id> [state]").description("email you each run's result: on or off; no state shows the setting (new-session routines only)").action(
|
|
5699
|
+
run(async (id, state) => {
|
|
5700
|
+
if (state && state !== "on" && state !== "off") throw new ApiError(400, "invalid_request", "state is on or off");
|
|
5701
|
+
const res = await api(
|
|
5702
|
+
state === "on" ? "PUT" : state === "off" ? "DELETE" : "GET",
|
|
5703
|
+
triggerPath(id, "result-email")
|
|
5704
|
+
);
|
|
5705
|
+
console.log(`result email ${res.result_email.enabled ? "on" : "off"}`);
|
|
5706
|
+
})
|
|
5707
|
+
);
|
|
5708
|
+
routine.command("chat-trigger <id>").description("start a run from your own message in a connected chat; no flags shows the trigger").option("--connection <id>", "the chat connection (see `sky channel list`)").option("--thread <id>", "only this conversation (default: every conversation on the connection)").option("--prefix <text>", "only messages starting with this; the rest of the message is the run's input").option("--rm", "remove the trigger").action(
|
|
5709
|
+
run(async (id, opts) => {
|
|
5710
|
+
const setting = Boolean(opts.connection || opts.thread || opts.prefix);
|
|
5711
|
+
const res = await triggerRequest(
|
|
5712
|
+
id,
|
|
5713
|
+
"channel-trigger",
|
|
5714
|
+
{ body: setting ? triggerBody(opts) : void 0, remove: opts.rm }
|
|
5715
|
+
);
|
|
5716
|
+
console.log(opts.rm ? res.removed ? "Removed the chat trigger" : "There was no chat trigger" : triggerLine(res.channel_trigger ?? null, res.available ?? true));
|
|
5717
|
+
})
|
|
5718
|
+
);
|
|
5719
|
+
routine.command("deliver-to <id>").description("post each run's answer to a conversation in a connected chat; no flags shows where").option("--connection <id>", "the chat connection (see `sky channel list`)").option("--thread <id>", "the conversation (default: the chat itself)").option("--rm", "stop posting answers").action(
|
|
5720
|
+
run(async (id, opts) => {
|
|
5721
|
+
const setting = Boolean(opts.connection || opts.thread);
|
|
5722
|
+
const res = await triggerRequest(
|
|
5723
|
+
id,
|
|
5724
|
+
"deliver-to",
|
|
5725
|
+
{ body: setting ? deliverBody(opts) : void 0, remove: opts.rm }
|
|
5726
|
+
);
|
|
5727
|
+
console.log(opts.rm ? res.removed ? "Stopped posting answers" : "Answers were posted nowhere" : deliverLine(res.deliver_to ?? null));
|
|
5728
|
+
})
|
|
5729
|
+
);
|
|
4636
5730
|
var collect = (value, previous = []) => [...previous, value];
|
|
4637
5731
|
function goalPath(sessionId, suffix = "") {
|
|
4638
5732
|
return `/sessions/${encodeURIComponent(sessionId)}/goal${suffix}`;
|
|
@@ -4775,7 +5869,7 @@ usageOptions(
|
|
|
4775
5869
|
run(async (opts) => {
|
|
4776
5870
|
const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
|
|
4777
5871
|
if (opts.out) {
|
|
4778
|
-
|
|
5872
|
+
writeFileSync7(opts.out, bytes);
|
|
4779
5873
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
4780
5874
|
} else {
|
|
4781
5875
|
process.stdout.write(bytes);
|
|
@@ -4868,6 +5962,189 @@ webhook.command("rm <id>").description("delete an endpoint").action(
|
|
|
4868
5962
|
console.log(`Deleted ${id}`);
|
|
4869
5963
|
})
|
|
4870
5964
|
);
|
|
5965
|
+
var channel = program.command("channel").description("chat surfaces (Slack, Telegram, Discord, WhatsApp, iMessage, \u2026) routed to a session");
|
|
5966
|
+
channel.command("list").description("your connections and the session each routes to").action(
|
|
5967
|
+
run(async () => {
|
|
5968
|
+
const connections = await listConnections();
|
|
5969
|
+
if (!connections.length) console.log("no channels \u2014 connect one with `sky channel connect <platform> --session sess_\u2026`");
|
|
5970
|
+
for (const c of connections) console.log(connectionLine(c));
|
|
5971
|
+
})
|
|
5972
|
+
);
|
|
5973
|
+
channel.command("connect <platform>").description("connect a surface; prints the link or code that finishes it").option("--session <id>", "route it to this session (sess_\u2026) once claimed").option("--webhook <id>", "route it to this channel webhook endpoint instead").option("--app <id>", "run the connection on your own registered bot or number (see `sky channel apps`)").option("--label <label>", "a name for the connection").option("--wait", "wait until the connection is claimed").action(
|
|
5974
|
+
run(async (platform, opts) => {
|
|
5975
|
+
const created = await connect(platform, opts);
|
|
5976
|
+
console.log(connectionLine(created));
|
|
5977
|
+
for (const step of connectSteps(created)) console.log(` ${step}`);
|
|
5978
|
+
if (opts.wait && created.status === "PENDING") {
|
|
5979
|
+
const done = await waitConnected(created.id);
|
|
5980
|
+
console.log(connectionLine(done));
|
|
5981
|
+
}
|
|
5982
|
+
})
|
|
5983
|
+
);
|
|
5984
|
+
channel.command("bind <connectionId>").description("route a connection to a session or a channel webhook endpoint").option("--session <id>", "the session (sess_\u2026)").option("--webhook <id>", "the channel webhook endpoint").action(
|
|
5985
|
+
run(async (connectionId, opts) => {
|
|
5986
|
+
await bind(connectionId, opts);
|
|
5987
|
+
console.log(`${connectionId} \u2192 ${opts.session ?? opts.webhook}`);
|
|
5988
|
+
})
|
|
5989
|
+
);
|
|
5990
|
+
channel.command("send <threadId> <text...>").description("post a markdown message into a connected thread").action(
|
|
5991
|
+
run(async (threadId, text) => {
|
|
5992
|
+
await send(threadId, text.join(" "));
|
|
5993
|
+
console.log(`Sent to ${threadId}`);
|
|
5994
|
+
})
|
|
5995
|
+
);
|
|
5996
|
+
channel.command("disconnect <connectionId>").description("disconnect a surface").action(
|
|
5997
|
+
run(async (connectionId) => {
|
|
5998
|
+
await api("DELETE", `/channels/connections/${encodeURIComponent(connectionId)}`);
|
|
5999
|
+
console.log(`Disconnected ${connectionId}`);
|
|
6000
|
+
})
|
|
6001
|
+
);
|
|
6002
|
+
channel.command("apps").description("your own bots and numbers registered as channel apps").action(
|
|
6003
|
+
run(async () => {
|
|
6004
|
+
const { apps } = await api("GET", "/channels/apps");
|
|
6005
|
+
if (!apps.length) console.log("no channel apps \u2014 connections use the shared AgentSky bots");
|
|
6006
|
+
for (const a of apps) console.log(`${a.id} ${a.platform.padEnd(9)} ${a.label}${a.status ? ` ${a.status}` : ""}`);
|
|
6007
|
+
})
|
|
6008
|
+
);
|
|
6009
|
+
channel.command("identities").description("chat identities whose messages run as you").action(
|
|
6010
|
+
run(async () => {
|
|
6011
|
+
const { identities } = await api("GET", "/channels/identities");
|
|
6012
|
+
if (!identities.length) console.log("no linked chat identities");
|
|
6013
|
+
for (const i of identities) console.log(`${i.id} ${i.platform.padEnd(9)} ${i.display_name ?? i.external_user_id}${i.linked_at ? ` since ${i.linked_at.slice(0, 10)}` : ""}`);
|
|
6014
|
+
})
|
|
6015
|
+
);
|
|
6016
|
+
channel.command("unlink <identityId>").description("unlink a chat identity; that person's messages stop running as you").action(
|
|
6017
|
+
run(async (identityId) => {
|
|
6018
|
+
await api("DELETE", `/channels/identities/${encodeURIComponent(identityId)}`);
|
|
6019
|
+
console.log(`Unlinked ${identityId}`);
|
|
6020
|
+
})
|
|
6021
|
+
);
|
|
6022
|
+
var connector = program.command("connector").description("apps your agents can use: connect, list, share with a workspace");
|
|
6023
|
+
connector.command("apps [query]").description("apps you can connect, searched by name").option("--limit <n>", "at most this many", (v) => Number.parseInt(v, 10), 50).action(
|
|
6024
|
+
run(async (query, opts) => {
|
|
6025
|
+
for (const a of await listApps(query, opts.limit)) console.log(`${a.slug.padEnd(24)} ${a.name} \u2014 ${a.description}`);
|
|
6026
|
+
})
|
|
6027
|
+
);
|
|
6028
|
+
connector.command("list").description("the accounts you connected").action(
|
|
6029
|
+
run(async () => {
|
|
6030
|
+
const { connections, credit } = await api("GET", "/connectors/connections");
|
|
6031
|
+
if (!connections.length) console.log("no connections \u2014 connect one with `sky connector connect <app>`");
|
|
6032
|
+
for (const c of connections) console.log(connectionLine2(c));
|
|
6033
|
+
if (credit.cap_usd > 0) console.log(`credit earned by connecting: $${credit.earned_usd} of $${credit.cap_usd}`);
|
|
6034
|
+
})
|
|
6035
|
+
);
|
|
6036
|
+
connector.command("connect <app>").description("connect an app in the browser (the slug from `sky connector apps`)").option("--universe <slug>", "connect it for this workspace itself (owner or admin)").option("--no-open", "print the authorization link without opening it").action(
|
|
6037
|
+
run(async (app, opts) => {
|
|
6038
|
+
const connection = await connectApp(app, { workspace: opts.universe, open: opts.open });
|
|
6039
|
+
console.log(`Connected ${connection.app_name} (${connection.label})${opts.universe ? ` for ${opts.universe}` : ""}`);
|
|
6040
|
+
})
|
|
6041
|
+
);
|
|
6042
|
+
connector.command("disconnect <connectionId>").description("disconnect one of your connections; agents lose it at once").action(
|
|
6043
|
+
run(async (connectionId) => {
|
|
6044
|
+
await api("DELETE", `/connectors/connections/${encodeURIComponent(connectionId)}`);
|
|
6045
|
+
console.log(`Disconnected ${connectionId}`);
|
|
6046
|
+
})
|
|
6047
|
+
);
|
|
6048
|
+
connector.command("repos").description("repositories your GitHub connection can reach").action(
|
|
6049
|
+
run(async () => {
|
|
6050
|
+
const { repos } = await api("GET", "/connectors/github/repos");
|
|
6051
|
+
for (const r of repos) console.log(`${r.full_name} ${r.default_branch}${r.private ? " private" : ""}`);
|
|
6052
|
+
})
|
|
6053
|
+
);
|
|
6054
|
+
connector.command("workspace [slug]").description("connections shared with a workspace (default: the current one)").action(
|
|
6055
|
+
run(async (slug) => {
|
|
6056
|
+
const ws = await api("GET", `/universes/${encodeURIComponent(await workspaceSlug(slug))}/connections`);
|
|
6057
|
+
if (!ws.sharing) console.log("this workspace does not take shared connections");
|
|
6058
|
+
for (const c of ws.connections) console.log(workspaceConnectionLine(c));
|
|
6059
|
+
})
|
|
6060
|
+
);
|
|
6061
|
+
connector.command("share <connectionId>").description("share one of your connections with a workspace, so its sessions can call through it").option("--universe <slug>", "the workspace (default: the current one)").action(
|
|
6062
|
+
run(async (connectionId, opts) => {
|
|
6063
|
+
const slug = await workspaceSlug(opts.universe);
|
|
6064
|
+
await api("POST", `/universes/${encodeURIComponent(slug)}/connections`, { connection_id: connectionId });
|
|
6065
|
+
console.log(`Shared ${connectionId} with ${slug}`);
|
|
6066
|
+
})
|
|
6067
|
+
);
|
|
6068
|
+
connector.command("unshare <connectionId>").description("stop sharing a connection with a workspace").option("--universe <slug>", "the workspace (default: the current one)").action(
|
|
6069
|
+
run(async (connectionId, opts) => {
|
|
6070
|
+
const slug = await workspaceSlug(opts.universe);
|
|
6071
|
+
await api("DELETE", `/universes/${encodeURIComponent(slug)}/connections/${encodeURIComponent(connectionId)}`);
|
|
6072
|
+
console.log(`Stopped sharing ${connectionId} with ${slug}`);
|
|
6073
|
+
})
|
|
6074
|
+
);
|
|
6075
|
+
connector.command("shared").description("everything you share with workspaces, and the calls made through each").action(
|
|
6076
|
+
run(async () => {
|
|
6077
|
+
const { shared } = await api("GET", "/shared-connections");
|
|
6078
|
+
if (!shared.length) console.log("you share no connections");
|
|
6079
|
+
for (const s of shared) console.log(`${s.connection_id} ${s.app_name} (${s.label}) \u2192 ${s.workspace.slug} ${s.use_count} call${s.use_count === 1 ? "" : "s"}`);
|
|
6080
|
+
})
|
|
6081
|
+
);
|
|
6082
|
+
var vault = program.command("vault").description("vaults: credentials a session is granted when it starts (values write-only)");
|
|
6083
|
+
vault.command("list").description("the workspace's vaults").action(
|
|
6084
|
+
run(async () => {
|
|
6085
|
+
const vaults = await listVaults();
|
|
6086
|
+
if (!vaults.length) console.log("no vaults");
|
|
6087
|
+
for (const v of vaults) console.log(`${v.id} ${v.display_name}`);
|
|
6088
|
+
})
|
|
6089
|
+
);
|
|
6090
|
+
vault.command("create <name>").description("create a vault; pass its id as vault_ids when starting a session").action(
|
|
6091
|
+
run(async (name) => {
|
|
6092
|
+
const { vault: created } = await api("POST", "/vaults", { display_name: name });
|
|
6093
|
+
console.log(`${created.id} ${created.display_name}`);
|
|
6094
|
+
})
|
|
6095
|
+
);
|
|
6096
|
+
vault.command("show <vaultId>").description("a vault's secrets (metadata only)").action(
|
|
6097
|
+
run(async (vaultId) => {
|
|
6098
|
+
const secrets2 = await listSecrets(vaultId);
|
|
6099
|
+
if (!secrets2.length) console.log("no secrets");
|
|
6100
|
+
for (const secret of secrets2) console.log(secretLine(secret));
|
|
6101
|
+
})
|
|
6102
|
+
);
|
|
6103
|
+
vault.command("rm <vaultId>").description("delete a vault and every secret in it").option("-y, --yes", "skip the confirmation prompt").action(
|
|
6104
|
+
run(async (vaultId, opts) => {
|
|
6105
|
+
if (!opts.yes && !await confirmPrompt(`Delete ${vaultId} and every secret in it?`)) return void console.log("Aborted.");
|
|
6106
|
+
await api("DELETE", `/vaults/${encodeURIComponent(vaultId)}`);
|
|
6107
|
+
console.log(`Deleted ${vaultId}`);
|
|
6108
|
+
})
|
|
6109
|
+
);
|
|
6110
|
+
vault.command("move <intoVaultId>").description("move the secrets of other vaults into this one").requiredOption("--from <vaultId...>", "the vaults to move out of").option("--dry-run", "only report what would move").action(
|
|
6111
|
+
run(async (intoVaultId, opts) => {
|
|
6112
|
+
for (const m of await moveSecrets(intoVaultId, opts.from, Boolean(opts.dryRun))) {
|
|
6113
|
+
console.log(`${m.outcome.padEnd(8)} ${m.name} from ${m.from}${m.reason ? ` (${m.reason})` : ""}`);
|
|
6114
|
+
}
|
|
6115
|
+
})
|
|
6116
|
+
);
|
|
6117
|
+
var vaultSecret = vault.command("secret").description("the secrets in a vault");
|
|
6118
|
+
vaultSecret.command("add <vaultId> <name>").description("add an environment variable (value from stdin or a hidden prompt), or an MCP server's bearer token with --mcp").option("--host <host...>", "hosts the environment variable may be sent to").option("--header <name>", "the header it is sent in (default: Authorization)").option("--template <template>", "how the header value is built, e.g. 'Bearer {secret}'").option("--mcp <url>", "store a bearer token for this MCP server instead; <name> is its display name").action(
|
|
6119
|
+
run(async (vaultId, name, opts) => {
|
|
6120
|
+
const value = await readSecretValue(opts.mcp ? "Token: " : `Value for ${name}: `);
|
|
6121
|
+
if (!value) throw new ApiError(400, "no_value", "no value provided");
|
|
6122
|
+
const input = opts.mcp ? { kind: "mcp", url: opts.mcp, token: value, displayName: name } : { kind: "env", name, value, hosts: opts.host ?? [], header: opts.header, template: opts.template };
|
|
6123
|
+
console.log(secretLine(await addSecret(vaultId, input)));
|
|
6124
|
+
})
|
|
6125
|
+
);
|
|
6126
|
+
vaultSecret.command("rotate <vaultId> <secretId>").description("replace a secret's value in place (stdin or a hidden prompt); running sessions keep working").option("--host <host...>", "also replace the hosts it may be sent to").option("--keep-value", "only change --host, keep the value").action(
|
|
6127
|
+
run(async (vaultId, secretId, opts) => {
|
|
6128
|
+
const current = (await listSecrets(vaultId)).find((s) => s.id === secretId);
|
|
6129
|
+
if (!current) throw new ApiError(404, "not_found", `no secret ${secretId} in ${vaultId}`);
|
|
6130
|
+
const value = opts.keepValue ? void 0 : await readSecretValue("New value: ");
|
|
6131
|
+
if (value === "") throw new ApiError(400, "no_value", "no value provided");
|
|
6132
|
+
const field = current.auth_type === "environment_variable" ? "value" : "token";
|
|
6133
|
+
console.log(secretLine(await updateSecret(vaultId, secretId, { [field]: value, hosts: opts.host })));
|
|
6134
|
+
})
|
|
6135
|
+
);
|
|
6136
|
+
vaultSecret.command("rm <vaultId> <secretId>").description("delete a secret").action(
|
|
6137
|
+
run(async (vaultId, secretId) => {
|
|
6138
|
+
await api("DELETE", `/vaults/${encodeURIComponent(vaultId)}/secrets/${encodeURIComponent(secretId)}`);
|
|
6139
|
+
console.log(`Deleted ${secretId}`);
|
|
6140
|
+
})
|
|
6141
|
+
);
|
|
6142
|
+
vaultSecret.command("validate <vaultId> <secretId>").description("check that an MCP OAuth grant still works").action(
|
|
6143
|
+
run(async (vaultId, secretId) => {
|
|
6144
|
+
const { result } = await api("POST", `/vaults/${encodeURIComponent(vaultId)}/secrets/${encodeURIComponent(secretId)}/validate`);
|
|
6145
|
+
console.log(`${secretId}: ${result}`);
|
|
6146
|
+
})
|
|
6147
|
+
);
|
|
4871
6148
|
var harness = program.command("harness").description("workspace harnesses: init, verify, push");
|
|
4872
6149
|
harness.command("init [dir]").description("write a harness.json profile skeleton in the given directory (default: .)").action(run(async (dir) => {
|
|
4873
6150
|
harnessInit(dir ?? ".");
|
|
@@ -4891,7 +6168,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
|
|
|
4891
6168
|
}));
|
|
4892
6169
|
var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
|
|
4893
6170
|
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) => {
|
|
4894
|
-
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-
|
|
6171
|
+
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-H3AP64UZ.js");
|
|
4895
6172
|
if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
|
|
4896
6173
|
const scopes = parseContentScopes(opts.capture);
|
|
4897
6174
|
const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
|
|
@@ -4920,13 +6197,13 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
|
|
|
4920
6197
|
]);
|
|
4921
6198
|
console.log(`Tracing ${trace.id} as ${opts.source} until ${grant.expires_at}`);
|
|
4922
6199
|
console.log(scopes.length ? ` content: ${scopes.join(", ")} (redacted here before it leaves)` : " metadata only");
|
|
4923
|
-
if (opts.hooks) console.log(` hooks: ${installClaudeHooks(
|
|
6200
|
+
if (opts.hooks) console.log(` hooks: ${installClaudeHooks(resolve9(typeof opts.hooks === "string" ? opts.hooks : ".claude/settings.local.json"))}`);
|
|
4924
6201
|
console.log(` a session started elsewhere joins it with metadata {"trace_id": "${trace.id}"}`);
|
|
4925
6202
|
console.log(` sky trace show ${trace.id}`);
|
|
4926
6203
|
}));
|
|
4927
6204
|
traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
|
|
4928
6205
|
try {
|
|
4929
|
-
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-
|
|
6206
|
+
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-H3AP64UZ.js");
|
|
4930
6207
|
const chunks = [];
|
|
4931
6208
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
4932
6209
|
const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
@@ -4939,14 +6216,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
|
|
|
4939
6216
|
}
|
|
4940
6217
|
});
|
|
4941
6218
|
traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
|
|
4942
|
-
const { readState, flush, grantSender } = await import("./trace-
|
|
6219
|
+
const { readState, flush, grantSender } = await import("./trace-H3AP64UZ.js");
|
|
4943
6220
|
const state = readState();
|
|
4944
6221
|
if (!state) throw new Error("no trace is running here");
|
|
4945
6222
|
const outcome = await flush(grantSender(state), { attempts: 4 });
|
|
4946
6223
|
console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
|
|
4947
6224
|
}));
|
|
4948
6225
|
traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
|
|
4949
|
-
const { readState, spoolStatus } = await import("./trace-
|
|
6226
|
+
const { readState, spoolStatus } = await import("./trace-H3AP64UZ.js");
|
|
4950
6227
|
const state = readState();
|
|
4951
6228
|
if (!state) {
|
|
4952
6229
|
console.log("No trace is running here.");
|
|
@@ -4956,7 +6233,7 @@ traceCmd.command("status").description("the running trace and its spool").action
|
|
|
4956
6233
|
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`);
|
|
4957
6234
|
}));
|
|
4958
6235
|
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) => {
|
|
4959
|
-
const { readState } = await import("./trace-
|
|
6236
|
+
const { readState } = await import("./trace-H3AP64UZ.js");
|
|
4960
6237
|
const ref = id ?? readState()?.trace_id;
|
|
4961
6238
|
if (!ref) throw new Error("name a trace, or start one here");
|
|
4962
6239
|
const bundle = await api("GET", `/traces/${ref}`);
|
|
@@ -4983,7 +6260,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
|
|
|
4983
6260
|
for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
|
|
4984
6261
|
}));
|
|
4985
6262
|
traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
|
|
4986
|
-
const { readState, record, flush, grantSender, clearState } = await import("./trace-
|
|
6263
|
+
const { readState, record, flush, grantSender, clearState } = await import("./trace-H3AP64UZ.js");
|
|
4987
6264
|
const state = readState();
|
|
4988
6265
|
if (!state) throw new Error("no trace is running here");
|
|
4989
6266
|
if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
|
|
@@ -4993,7 +6270,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
|
|
|
4993
6270
|
console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
|
|
4994
6271
|
}));
|
|
4995
6272
|
traceCmd.command("sync [payload]").description("upload this machine's Claude Code and Codex sessions 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("--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) => {
|
|
4996
|
-
const lib = await import("./trace-library-
|
|
6273
|
+
const lib = await import("./trace-library-LGPKAQTY.js");
|
|
4997
6274
|
if (opts.hook) {
|
|
4998
6275
|
try {
|
|
4999
6276
|
let raw = payload ?? "";
|
|
@@ -5041,10 +6318,10 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
|
|
|
5041
6318
|
console.log(`A repository's .agentsky.json "workspace" wins over these rules.`);
|
|
5042
6319
|
}
|
|
5043
6320
|
if (opts.installSchedule) {
|
|
5044
|
-
const { join:
|
|
6321
|
+
const { join: join15 } = await import("path");
|
|
5045
6322
|
const { homedir: homedir8 } = await import("os");
|
|
5046
6323
|
const { execFileSync } = await import("child_process");
|
|
5047
|
-
const log =
|
|
6324
|
+
const log = join15(homedir8(), ".config", "sky", "trace-sync.log");
|
|
5048
6325
|
const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
|
|
5049
6326
|
try {
|
|
5050
6327
|
execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
|
|
@@ -5054,11 +6331,11 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
|
|
|
5054
6331
|
console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
|
|
5055
6332
|
}
|
|
5056
6333
|
if (opts.status) {
|
|
5057
|
-
const { readFileSync:
|
|
5058
|
-
const { join:
|
|
6334
|
+
const { readFileSync: readFileSync16 } = await import("fs");
|
|
6335
|
+
const { join: join15 } = await import("path");
|
|
5059
6336
|
let state = { sessions: {} };
|
|
5060
6337
|
try {
|
|
5061
|
-
state = JSON.parse(
|
|
6338
|
+
state = JSON.parse(readFileSync16(join15(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
|
|
5062
6339
|
} catch {
|
|
5063
6340
|
}
|
|
5064
6341
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -5091,7 +6368,7 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
|
|
|
5091
6368
|
})();
|
|
5092
6369
|
});
|
|
5093
6370
|
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) => {
|
|
5094
|
-
const { libraryRequest } = await import("./trace-library-
|
|
6371
|
+
const { libraryRequest } = await import("./trace-library-LGPKAQTY.js");
|
|
5095
6372
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
5096
6373
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
5097
6374
|
const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
|
|
@@ -5108,7 +6385,7 @@ traceCmd.command("search <query...>").description("search every session in a wor
|
|
|
5108
6385
|
if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
|
|
5109
6386
|
}));
|
|
5110
6387
|
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) => {
|
|
5111
|
-
const { libraryRequest } = await import("./trace-library-
|
|
6388
|
+
const { libraryRequest } = await import("./trace-library-LGPKAQTY.js");
|
|
5112
6389
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
5113
6390
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
5114
6391
|
const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
|