@agentsky/sky 2.24.0 → 2.25.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-277AZSSG.js → chunk-D3WG4BMK.js} +7 -2
- package/dist/{chunk-RAFHOB5S.js → chunk-LPUGEMCV.js} +1 -1
- package/dist/{chunk-UHMKEYJN.js → chunk-QROAAJN2.js} +7 -3
- package/dist/index.js +2147 -1850
- package/dist/{src-FB5IETJP.js → src-BMXKAMOB.js} +1 -1
- package/dist/{trace-CFHCOGKB.js → trace-HV4E2BPX.js} +2 -2
- package/dist/{trace-library-2VHTJCOY.js → trace-library-HJWI7KM7.js} +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
postEvents,
|
|
10
10
|
resolveSessionRef,
|
|
11
11
|
streamEvents,
|
|
12
|
-
uploadFile
|
|
13
|
-
|
|
12
|
+
uploadFile,
|
|
13
|
+
writeResponseFile
|
|
14
|
+
} from "./chunk-QROAAJN2.js";
|
|
14
15
|
import {
|
|
15
16
|
CLI_VERSION,
|
|
16
17
|
USER_AGENT,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
currentUniverse,
|
|
22
23
|
loadConfig,
|
|
23
24
|
saveConfig
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-LPUGEMCV.js";
|
|
25
26
|
import {
|
|
26
27
|
AGENT_TYPES,
|
|
27
28
|
MODEL_SUBSCRIPTION_PLAN_PHRASE,
|
|
@@ -43,11 +44,11 @@ import {
|
|
|
43
44
|
skillRefSchema,
|
|
44
45
|
splitMcpServers,
|
|
45
46
|
supersededBy
|
|
46
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-D3WG4BMK.js";
|
|
47
48
|
import "./chunk-MLKGABMK.js";
|
|
48
49
|
|
|
49
50
|
// src/index.ts
|
|
50
|
-
import { existsSync as existsSync13, readFileSync as
|
|
51
|
+
import { existsSync as existsSync13, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
|
|
51
52
|
import { basename as basename9, resolve as resolve9 } from "path";
|
|
52
53
|
import { createInterface as createInterface6 } from "readline";
|
|
53
54
|
import { Command } from "commander";
|
|
@@ -467,1886 +468,1898 @@ async function unsubscribeMarketing() {
|
|
|
467
468
|
return { marketing: notification_preferences.email.marketing };
|
|
468
469
|
}
|
|
469
470
|
|
|
470
|
-
// src/
|
|
471
|
-
|
|
472
|
-
|
|
471
|
+
// src/import-local.ts
|
|
472
|
+
import { closeSync as closeSync3, existsSync as existsSync4, openSync as openSync3, readdirSync as readdirSync4, readFileSync as readFileSync6, readSync as readSync3, statSync as statSync5 } from "fs";
|
|
473
|
+
import { homedir as homedir3 } from "os";
|
|
474
|
+
import { join as join6 } from "path";
|
|
475
|
+
import { createHash as createHash3 } from "crypto";
|
|
476
|
+
|
|
477
|
+
// src/import-files.ts
|
|
478
|
+
import { isUtf8 } from "buffer";
|
|
479
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
480
|
+
import { homedir as homedir2 } from "os";
|
|
481
|
+
import { dirname as dirname3, extname as extname2, isAbsolute, join as join4, relative as relative2, resolve as resolve2, sep as sep2, basename as basename4 } from "path";
|
|
482
|
+
|
|
483
|
+
// src/import-workspace.ts
|
|
484
|
+
import { spawnSync } from "child_process";
|
|
485
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, statSync as statSync3 } from "fs";
|
|
486
|
+
import { homedir } from "os";
|
|
487
|
+
import { basename as basename3, dirname as dirname2, join as join3, relative, sep } from "path";
|
|
488
|
+
import { parse as parseToml } from "smol-toml";
|
|
489
|
+
var MAX_TREE = 5e3;
|
|
490
|
+
var SKILL_LINE = /Base directory for this skill: (\/[^\n\r"'\\<>]+)/g;
|
|
491
|
+
function git(cwd, ...args) {
|
|
492
|
+
const run2 = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 1e4, maxBuffer: 32 * 1024 * 1024 });
|
|
493
|
+
return run2.status === 0 ? run2.stdout : null;
|
|
473
494
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
495
|
+
var isFile = (path) => {
|
|
496
|
+
try {
|
|
497
|
+
return statSync3(path).isFile();
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
var isDir = (path) => {
|
|
503
|
+
try {
|
|
504
|
+
return statSync3(path).isDirectory();
|
|
505
|
+
} catch {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
function sharedInstructionPaths(home = homedir()) {
|
|
510
|
+
const rules = join3(home, ".agents", "rules");
|
|
511
|
+
let ruleFiles = [];
|
|
512
|
+
try {
|
|
513
|
+
ruleFiles = readdirSync2(rules).filter((name) => name.endsWith(".md")).sort().map((name) => join3(rules, name));
|
|
514
|
+
} catch {
|
|
515
|
+
}
|
|
516
|
+
return [join3(home, "AGENTS.md"), join3(home, "MEMORY.md"), ...ruleFiles].filter(isFile);
|
|
477
517
|
}
|
|
478
|
-
function
|
|
479
|
-
if (
|
|
480
|
-
|
|
518
|
+
function strings(value, out, depth = 0) {
|
|
519
|
+
if (depth > 12) return;
|
|
520
|
+
if (typeof value === "string") out.push(value);
|
|
521
|
+
else if (Array.isArray(value)) for (const item of value) strings(item, out, depth + 1);
|
|
522
|
+
else if (value && typeof value === "object") for (const item of Object.values(value)) strings(item, out, depth + 1);
|
|
481
523
|
}
|
|
482
|
-
function
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
524
|
+
function skillRoots(cwd, home) {
|
|
525
|
+
const claude = process.env.CLAUDE_CONFIG_DIR ?? join3(home, ".claude");
|
|
526
|
+
const codex = process.env.CODEX_HOME ?? join3(home, ".codex");
|
|
527
|
+
return [
|
|
528
|
+
...cwd ? [join3(cwd, ".claude", "skills"), join3(cwd, ".agents", "skills")] : [],
|
|
529
|
+
join3(home, ".agents", "skills"),
|
|
530
|
+
join3(claude, "skills"),
|
|
531
|
+
join3(codex, "skills"),
|
|
532
|
+
join3(home, ".hermes", "skills")
|
|
533
|
+
];
|
|
490
534
|
}
|
|
491
|
-
function
|
|
492
|
-
|
|
535
|
+
function pluginSkill(plugin, skill, home) {
|
|
536
|
+
const walk = (dir, depth, underPlugin) => {
|
|
537
|
+
if (depth > 7) return null;
|
|
538
|
+
let entries;
|
|
539
|
+
try {
|
|
540
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
541
|
+
} catch {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
for (const entry2 of entries) {
|
|
545
|
+
if (!entry2.isDirectory() || entry2.name === "node_modules" || entry2.name === ".git") continue;
|
|
546
|
+
const path = join3(dir, entry2.name);
|
|
547
|
+
if (entry2.name === skill && basename3(dir) === "skills" && (underPlugin || path.includes(`${sep}${plugin}${sep}`)) && isFile(join3(path, "SKILL.md"))) return realpathSync(path);
|
|
548
|
+
const found = walk(path, depth + 1, underPlugin);
|
|
549
|
+
if (found) return found;
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
};
|
|
553
|
+
return walk(join3(process.env.CLAUDE_CONFIG_DIR ?? join3(home, ".claude"), "plugins"), 0, false) ?? walk(join3(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions", "skills-plugin"), 0, true);
|
|
493
554
|
}
|
|
494
|
-
function
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
555
|
+
function findSkill(name, cwd, home) {
|
|
556
|
+
const [scope, skill] = name.includes(":") ? name.split(":", 2) : [null, name];
|
|
557
|
+
if (scope && skill) return pluginSkill(scope, skill, home);
|
|
558
|
+
for (const root of skillRoots(cwd, home)) {
|
|
559
|
+
const direct = join3(root, name);
|
|
560
|
+
if (isFile(join3(direct, "SKILL.md"))) return realpathSync(direct);
|
|
500
561
|
}
|
|
501
|
-
|
|
502
|
-
if (page.partial) lines.push(`partial: ${page.coverage ?? "not every source was searched"}`);
|
|
503
|
-
if (page.nextCursor) lines.push(`more: --cursor ${page.nextCursor}`);
|
|
504
|
-
return lines.join("\n");
|
|
562
|
+
return null;
|
|
505
563
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
capabilities: capabilitiesSchema.removeDefault().optional(),
|
|
527
|
-
// omitted = leave the agent's as they are
|
|
528
|
-
prompt: external_exports.string().optional(),
|
|
529
|
-
prompt_file: external_exports.string().optional(),
|
|
530
|
-
instructions: external_exports.array(fileInstructionSchema).optional(),
|
|
531
|
-
skills: external_exports.array(skillRefSchema).optional(),
|
|
532
|
-
customData: external_exports.array(customDataSchema).optional(),
|
|
533
|
-
/** "deny" makes the agent work alone: its sessions may not hire helpers. */
|
|
534
|
-
hiring_policy: external_exports.enum(["allow", "deny"]).optional(),
|
|
535
|
-
/** The environment its sessions boot in when they name none. */
|
|
536
|
-
environment: external_exports.string().min(1).optional(),
|
|
537
|
-
/** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
|
|
538
|
-
vcpus: external_exports.number().int().optional(),
|
|
539
|
-
memory_mb: external_exports.number().int().optional()
|
|
540
|
-
});
|
|
541
|
-
function readAgentFile(path) {
|
|
542
|
-
const raw = readFileSync3(path, "utf8");
|
|
543
|
-
const parsed = agentFileSchema.safeParse(parseToml(raw));
|
|
544
|
-
if (!parsed.success) {
|
|
545
|
-
const issue = parsed.error.issues[0];
|
|
546
|
-
throw new Error(`${path}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? "invalid"}`);
|
|
564
|
+
function loadedSkills(records, touched, cwd, home = homedir()) {
|
|
565
|
+
const dirs = /* @__PURE__ */ new Map();
|
|
566
|
+
const missing = /* @__PURE__ */ new Set();
|
|
567
|
+
const add = (dir, name = basename3(dir)) => {
|
|
568
|
+
if (!isFile(join3(dir, "SKILL.md"))) return false;
|
|
569
|
+
const real = realpathSync(dir);
|
|
570
|
+
if (![...dirs.values()].includes(real)) dirs.set(name, real);
|
|
571
|
+
return true;
|
|
572
|
+
};
|
|
573
|
+
const called = [];
|
|
574
|
+
for (const record of records) {
|
|
575
|
+
const texts = [];
|
|
576
|
+
strings(record, texts);
|
|
577
|
+
for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].trim().replace(/[.,;:)]+$/, ""));
|
|
578
|
+
const content = record?.message?.content;
|
|
579
|
+
if (!Array.isArray(content)) continue;
|
|
580
|
+
for (const block of content) {
|
|
581
|
+
const b = block;
|
|
582
|
+
if (b?.type === "tool_use" && b.name === "Skill" && typeof b.input?.skill === "string") called.push(b.input.skill.replace(/^\//, ""));
|
|
583
|
+
}
|
|
547
584
|
}
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
);
|
|
585
|
+
for (const path of touched) if (basename3(path) === "SKILL.md") add(dirname2(path));
|
|
586
|
+
for (const name of called) {
|
|
587
|
+
if (!name || dirs.has(name) || [...dirs.keys()].some((key) => key.endsWith(`:${name}`) || name.endsWith(`:${key}`))) continue;
|
|
588
|
+
const dir = findSkill(name, cwd, home);
|
|
589
|
+
if (!dir || !add(dir, name)) missing.add(name);
|
|
553
590
|
}
|
|
554
|
-
|
|
555
|
-
const inline = (ref, inlineContent) => {
|
|
556
|
-
if (inlineContent !== void 0) return inlineContent;
|
|
557
|
-
if (!ref) return void 0;
|
|
558
|
-
return readFileSync3(resolve2(base, ref), "utf8");
|
|
559
|
-
};
|
|
560
|
-
const spec = {
|
|
561
|
-
name: file.name,
|
|
562
|
-
description: file.description,
|
|
563
|
-
agentType: file.type,
|
|
564
|
-
llm: file.llm,
|
|
565
|
-
reasoningEffort: file.reasoning_effort,
|
|
566
|
-
displayName: file.name,
|
|
567
|
-
capabilities: file.capabilities,
|
|
568
|
-
prompt: inline(file.prompt_file, file.prompt),
|
|
569
|
-
instructions: file.instructions?.map((f) => {
|
|
570
|
-
const content = inline(f.file, f.content);
|
|
571
|
-
if (content === void 0) throw new Error(`instruction ${f.name}: content or file required`);
|
|
572
|
-
return { name: f.name, content };
|
|
573
|
-
}),
|
|
574
|
-
skills: file.skills,
|
|
575
|
-
customData: file.customData,
|
|
576
|
-
hiringPolicy: file.hiring_policy,
|
|
577
|
-
environmentId: file.environment,
|
|
578
|
-
vcpus: file.vcpus,
|
|
579
|
-
memoryMb: file.memory_mb
|
|
580
|
-
};
|
|
581
|
-
return { file, spec };
|
|
591
|
+
return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
|
|
582
592
|
}
|
|
583
|
-
function
|
|
584
|
-
|
|
593
|
+
function readJson(path) {
|
|
594
|
+
try {
|
|
595
|
+
const value = JSON.parse(readFileSync3(path, "utf8"));
|
|
596
|
+
return value && typeof value === "object" ? value : null;
|
|
597
|
+
} catch {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
585
600
|
}
|
|
586
|
-
function
|
|
601
|
+
function names(value) {
|
|
602
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
603
|
+
const keys2 = Object.keys(value).sort();
|
|
604
|
+
return keys2.length ? keys2 : void 0;
|
|
605
|
+
}
|
|
606
|
+
function safeUrl(raw) {
|
|
607
|
+
try {
|
|
608
|
+
const url = new URL(raw);
|
|
609
|
+
if (!/^https?:$/.test(url.protocol)) return null;
|
|
610
|
+
const query = [.../* @__PURE__ */ new Set([...url.searchParams.keys()])].sort();
|
|
611
|
+
return { url: `${url.protocol}//${url.host}${url.pathname}`, ...query.length ? { query } : {} };
|
|
612
|
+
} catch {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function serverEntry(name, def, scope) {
|
|
617
|
+
if (!def || typeof def !== "object") return null;
|
|
618
|
+
const env = names(def.env);
|
|
619
|
+
const headers = names(def.headers ?? def.http_headers);
|
|
620
|
+
if (typeof def.url === "string") {
|
|
621
|
+
const url = safeUrl(def.url);
|
|
622
|
+
if (!url) return null;
|
|
623
|
+
const type = def.type === "sse" ? "sse" : "http";
|
|
624
|
+
return { name, scope, transport: type, ...url, ...headers ? { headers } : {}, ...env ? { env } : {} };
|
|
625
|
+
}
|
|
626
|
+
if (typeof def.command !== "string" || !def.command) return null;
|
|
627
|
+
const args = Array.isArray(def.args) ? def.args.filter((arg) => typeof arg === "string") : [];
|
|
628
|
+
return { name, scope, transport: "stdio", command: def.command, ...args.length ? { args } : {}, ...env ? { env } : {} };
|
|
629
|
+
}
|
|
630
|
+
function mcpServers(harness2, cwd, home = homedir()) {
|
|
587
631
|
const out = [];
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (openString) {
|
|
594
|
-
if (line.includes(openString)) openString = null;
|
|
595
|
-
out.push(line);
|
|
596
|
-
continue;
|
|
632
|
+
const push = (servers, scope) => {
|
|
633
|
+
if (!servers || typeof servers !== "object") return;
|
|
634
|
+
for (const [name, def] of Object.entries(servers)) {
|
|
635
|
+
const entry2 = serverEntry(name, def, scope);
|
|
636
|
+
if (entry2 && !out.some((existing) => existing.name === name)) out.push(entry2);
|
|
597
637
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
638
|
+
};
|
|
639
|
+
if (harness2 === "claude_code") {
|
|
640
|
+
const config = readJson(join3(process.env.CLAUDE_CONFIG_DIR ? dirname2(process.env.CLAUDE_CONFIG_DIR) : home, ".claude.json"));
|
|
641
|
+
const projects = config?.projects && typeof config.projects === "object" ? config.projects : {};
|
|
642
|
+
const root = cwd ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || cwd : "";
|
|
643
|
+
if (cwd) push(projects[cwd]?.mcpServers ?? projects[root]?.mcpServers, "local");
|
|
644
|
+
if (root) push(readJson(join3(root, ".mcp.json"))?.mcpServers, "project");
|
|
645
|
+
push(config?.mcpServers, "user");
|
|
646
|
+
}
|
|
647
|
+
if (harness2 === "codex") {
|
|
648
|
+
try {
|
|
649
|
+
const config = parseToml(readFileSync3(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "config.toml"), "utf8"));
|
|
650
|
+
push(config.mcp_servers, "user");
|
|
651
|
+
} catch {
|
|
605
652
|
}
|
|
606
|
-
if (topLevel && nameAt < 0 && /^\s*name\s*=/.test(line)) nameAt = out.length;
|
|
607
|
-
const opener = /=\s*("""|''')/.exec(line);
|
|
608
|
-
if (opener && !line.slice(opener.index + opener[0].length).includes(opener[1])) openString = opener[1];
|
|
609
|
-
out.push(line);
|
|
610
653
|
}
|
|
611
|
-
|
|
612
|
-
const next = out.join("\n");
|
|
613
|
-
if (parseToml(next).id !== id) throw new Error("could not stamp the agent id into the file");
|
|
614
|
-
return next;
|
|
654
|
+
return out;
|
|
615
655
|
}
|
|
616
|
-
function
|
|
617
|
-
|
|
618
|
-
writeFileSync2(
|
|
619
|
-
path,
|
|
620
|
-
`# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
|
|
621
|
-
name = "${name}"
|
|
622
|
-
# type = "pi" # pi (default) | hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode
|
|
623
|
-
# llm = "deepseek-flash" # must be compatible with type
|
|
624
|
-
# reasoning_effort = "high" # per harness \u2014 ${reasoningEffortDocLine()}
|
|
625
|
-
# capabilities = []
|
|
626
|
-
# hiring_policy = "deny" # its sessions work alone; "allow" (default) lets them hire helpers
|
|
627
|
-
# environment = "env_..." # the environment its sessions boot in when they name none
|
|
628
|
-
# [[skills]] # a skill from \`sky skills upload <dir>\`
|
|
629
|
-
# type = "skill"
|
|
630
|
-
# skillId = "skill_..."
|
|
631
|
-
|
|
632
|
-
prompt = """
|
|
633
|
-
You are ${name}, a helpful long-lived agent.
|
|
634
|
-
"""
|
|
635
|
-
`
|
|
636
|
-
);
|
|
656
|
+
function remoteWithoutCredentials(remote) {
|
|
657
|
+
return remote.trim().replace(/\/\/[^@/]+@/, "//").replace(/^[^@/]+@([^:]+):/, "$1:");
|
|
637
658
|
}
|
|
638
|
-
function
|
|
659
|
+
function workspaceState(cwd, harness2, skills2, home = homedir(), title = null) {
|
|
660
|
+
const present = !!cwd && isDir(cwd);
|
|
661
|
+
const root = present ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || null : null;
|
|
662
|
+
let gitState = null;
|
|
663
|
+
let tree = [];
|
|
664
|
+
let patch = null;
|
|
665
|
+
if (root) {
|
|
666
|
+
const dirty = (git(root, "status", "--porcelain=v1", "--untracked-files=normal") ?? "").split("\n").filter(Boolean).map((line) => line.slice(3));
|
|
667
|
+
const ahead = git(root, "rev-list", "--count", "@{upstream}..HEAD");
|
|
668
|
+
const remote = git(root, "remote", "get-url", "origin");
|
|
669
|
+
const branch = git(root, "rev-parse", "--abbrev-ref", "HEAD")?.trim() || null;
|
|
670
|
+
gitState = {
|
|
671
|
+
remote: remote ? remoteWithoutCredentials(remote) : null,
|
|
672
|
+
branch: branch === "HEAD" ? null : branch,
|
|
673
|
+
commit: git(root, "rev-parse", "HEAD")?.trim() || null,
|
|
674
|
+
dirty: dirty.slice(0, 500),
|
|
675
|
+
ahead: ahead === null ? null : Number(ahead.trim()) || 0
|
|
676
|
+
};
|
|
677
|
+
tree = (git(cwd, "-c", "core.quotePath=false", "ls-files") ?? "").split("\n").filter(Boolean);
|
|
678
|
+
if (dirty.length) patch = git(cwd, "diff", "HEAD", "--no-color", "--no-ext-diff", "--relative", "--", ".") || null;
|
|
679
|
+
}
|
|
639
680
|
return {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
681
|
+
manifest: {
|
|
682
|
+
format: "agentsky.import-workspace",
|
|
683
|
+
format_version: 1,
|
|
684
|
+
harness: harness2,
|
|
685
|
+
title: title?.trim().slice(0, 120) || null,
|
|
686
|
+
cwd: cwd ? cwd.replace(home, "~") : null,
|
|
687
|
+
project: cwd ? basename3(root ?? cwd) : null,
|
|
688
|
+
git: gitState,
|
|
689
|
+
tree: tree.slice(0, MAX_TREE),
|
|
690
|
+
tree_truncated: tree.length > MAX_TREE,
|
|
691
|
+
skills: [...skills2.found.map((skill) => ({ name: skill.name, found: true })), ...skills2.missing.map((name) => ({ name, found: false }))],
|
|
692
|
+
mcp_servers: mcpServers(harness2, cwd, home)
|
|
693
|
+
},
|
|
694
|
+
patch
|
|
648
695
|
};
|
|
649
696
|
}
|
|
650
|
-
function
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
697
|
+
function skillFiles(skill) {
|
|
698
|
+
const out = [];
|
|
699
|
+
const walk = (dir, depth) => {
|
|
700
|
+
if (depth > 8 || out.length > 400) return;
|
|
701
|
+
let entries;
|
|
702
|
+
try {
|
|
703
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
704
|
+
} catch {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
for (const entry2 of entries) {
|
|
708
|
+
if (entry2.name === "node_modules" || entry2.name === ".git" || entry2.name === "__pycache__" || entry2.name === ".venv") continue;
|
|
709
|
+
const path = join3(dir, entry2.name);
|
|
710
|
+
if (entry2.isDirectory()) walk(path, depth + 1);
|
|
711
|
+
else if (entry2.isFile()) out.push({ path, name: `skills/${skill.name}/${relative(skill.dir, path).split(sep).join("/")}` });
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
if (existsSync2(skill.dir)) walk(skill.dir, 0);
|
|
715
|
+
return out;
|
|
716
|
+
}
|
|
717
|
+
function codexThreadName(sessionId, home = homedir()) {
|
|
718
|
+
let name = null;
|
|
719
|
+
try {
|
|
720
|
+
for (const line of readFileSync3(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "session_index.jsonl"), "utf8").split("\n")) {
|
|
721
|
+
if (!line.includes(sessionId)) continue;
|
|
722
|
+
try {
|
|
723
|
+
const entry2 = JSON.parse(line);
|
|
724
|
+
if (entry2.id === sessionId && typeof entry2.thread_name === "string" && entry2.thread_name.trim()) name = entry2.thread_name.trim();
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
667
729
|
}
|
|
668
|
-
return
|
|
730
|
+
return name;
|
|
669
731
|
}
|
|
670
732
|
|
|
671
|
-
// src/
|
|
672
|
-
var
|
|
673
|
-
var
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
733
|
+
// src/import-files.ts
|
|
734
|
+
var MAX_ATTACHED_FILE_BYTES = 10 * 1024 * 1024;
|
|
735
|
+
var SECRET_FILE = /(^|\/)(\.env(\..*)?|\.netrc|\.npmrc|\.pypirc|\.git-credentials|\.?credentials(\.json)?|auth\.json|\.claude\.json|id_(rsa|ed25519|ecdsa)[^/]*|.*\.(pem|key|p12|pfx|keystore|jks|kdbx|sqlite|db))$|\/\.ssh\/|\/\.aws\/|\/\.gnupg\/|\/\.docker\/config\.json$|\/Keychains\//i;
|
|
736
|
+
var SKIP_DIR = /(^|\/)(node_modules|\.git|\.next|dist|build|__pycache__|\.venv)(\/|$)/;
|
|
737
|
+
var INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", "HERMES.md", "GEMINI.md", ".cursorrules"];
|
|
738
|
+
var DOCUMENTS = {
|
|
739
|
+
".pdf": "application/pdf",
|
|
740
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
741
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
742
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
743
|
+
".doc": "application/msword",
|
|
744
|
+
".rtf": "application/rtf",
|
|
745
|
+
".odt": "application/vnd.oasis.opendocument.text"
|
|
746
|
+
};
|
|
747
|
+
var IMAGES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
748
|
+
var TEXT = {
|
|
749
|
+
".md": "text/markdown",
|
|
750
|
+
".mdx": "text/markdown",
|
|
751
|
+
".txt": "text/plain",
|
|
752
|
+
".csv": "text/csv",
|
|
753
|
+
".tsv": "text/tab-separated-values",
|
|
754
|
+
".html": "text/html",
|
|
755
|
+
".htm": "text/html",
|
|
756
|
+
".css": "text/css",
|
|
757
|
+
".scss": "text/css",
|
|
758
|
+
".json": "application/json",
|
|
759
|
+
".jsonl": "application/x-ndjson",
|
|
760
|
+
".ndjson": "application/x-ndjson",
|
|
761
|
+
".yaml": "application/yaml",
|
|
762
|
+
".yml": "application/yaml",
|
|
763
|
+
".toml": "application/toml",
|
|
764
|
+
".xml": "application/xml",
|
|
765
|
+
".sql": "application/sql",
|
|
766
|
+
".svg": "image/svg+xml",
|
|
767
|
+
".js": "application/javascript",
|
|
768
|
+
".mjs": "application/javascript",
|
|
769
|
+
".cjs": "application/javascript",
|
|
770
|
+
".py": "text/x-python",
|
|
771
|
+
".sh": "text/x-shellscript",
|
|
772
|
+
".bash": "text/x-shellscript",
|
|
773
|
+
".zsh": "text/x-shellscript"
|
|
774
|
+
};
|
|
775
|
+
var PLAIN_TEXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".jsx", ".mts", ".cts", ".go", ".rs", ".java", ".kt", ".swift", ".rb", ".php", ".c", ".h", ".cc", ".cpp", ".hpp", ".cs", ".scala", ".lua", ".r", ".m", ".tex", ".bib", ".ipynb", ".prisma", ".graphql", ".proto", ".ini", ".cfg", ".conf", ".log", ".rst", ".vue", ".svelte", ".astro", ".tf", ".hcl", ".gradle", ".lock", ".patch", ".diff"]);
|
|
776
|
+
var PLAIN_NAMES = /* @__PURE__ */ new Set(["Dockerfile", "Makefile", "Procfile", "Gemfile", "Rakefile", "LICENSE", "README", ".cursorrules", ".gitignore", ".dockerignore", ".editorconfig", ".prettierrc", ".eslintrc"]);
|
|
777
|
+
function classify(path) {
|
|
778
|
+
const ext = extname2(path).toLowerCase();
|
|
779
|
+
if (DOCUMENTS[ext]) return { kind: "document", mediaType: DOCUMENTS[ext] };
|
|
780
|
+
if (IMAGES[ext]) return { kind: "image", mediaType: IMAGES[ext] };
|
|
781
|
+
if (TEXT[ext]) return { kind: "text", mediaType: TEXT[ext] };
|
|
782
|
+
if (PLAIN_TEXT.has(ext) || PLAIN_NAMES.has(basename4(path))) return { kind: "text", mediaType: "text/plain" };
|
|
783
|
+
return null;
|
|
680
784
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
785
|
+
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;
|
|
786
|
+
var PATH_KEYS = ["file_path", "path", "notebook_path", "filePath", "target_file"];
|
|
787
|
+
var obj = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
788
|
+
var str = (value) => typeof value === "string" ? value : "";
|
|
789
|
+
function shellText(input) {
|
|
790
|
+
if (typeof input === "string") return input;
|
|
791
|
+
const i = obj(input);
|
|
792
|
+
if (!i) return "";
|
|
793
|
+
const command = Array.isArray(i.command) ? i.command.filter((part) => typeof part === "string").join(" ") : str(i.command);
|
|
794
|
+
return [command, str(i.cmd), str(i.script)].filter(Boolean).join("\n");
|
|
795
|
+
}
|
|
796
|
+
function touchedPaths(records, cwd, home = homedir2()) {
|
|
797
|
+
const out = /* @__PURE__ */ new Set();
|
|
798
|
+
const add = (raw, base) => {
|
|
799
|
+
if (!raw || raw.length > 1e3) return;
|
|
800
|
+
const path = raw.replace(/^~(?=\/)/, home);
|
|
801
|
+
if (isAbsolute(path)) out.add(resolve2(path));
|
|
802
|
+
else if (base) out.add(resolve2(base, path));
|
|
803
|
+
};
|
|
804
|
+
const call = (name, input) => {
|
|
805
|
+
const i = obj(input);
|
|
806
|
+
const base = str(i?.workdir) || str(i?.cwd) || cwd;
|
|
807
|
+
if (i) for (const key of PATH_KEYS) add(str(i[key]), base);
|
|
808
|
+
const patch = typeof input === "string" ? input : str(i?.input) || str(i?.patch);
|
|
809
|
+
for (const match of patch.matchAll(/^\*\*\* (?:Add|Update) File: (.+)$/gm)) add(match[1].trim(), base);
|
|
810
|
+
if (!/bash|shell|exec|terminal|command|run/i.test(name)) return;
|
|
811
|
+
for (const match of shellText(input).replace(/\\n/g, "\n").matchAll(SHELL_READ)) add(match[1], base);
|
|
691
812
|
};
|
|
813
|
+
for (const raw of records) {
|
|
814
|
+
const record = obj(raw);
|
|
815
|
+
if (!record) continue;
|
|
816
|
+
const content = obj(record.message)?.content;
|
|
817
|
+
if (record.type === "assistant" && Array.isArray(content)) {
|
|
818
|
+
for (const block of content) {
|
|
819
|
+
const b = obj(block);
|
|
820
|
+
if (b?.type === "tool_use") call(str(b.name), b.input);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const payload = obj(record.payload);
|
|
824
|
+
if (record.type === "response_item" && (payload?.type === "function_call" || payload?.type === "custom_tool_call")) {
|
|
825
|
+
let input = payload.arguments ?? payload.input ?? "";
|
|
826
|
+
if (typeof input === "string") {
|
|
827
|
+
try {
|
|
828
|
+
input = JSON.parse(input);
|
|
829
|
+
} catch {
|
|
830
|
+
input = payload.arguments ?? payload.input;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
call(str(payload.name), input);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return [...out];
|
|
692
837
|
}
|
|
693
|
-
|
|
694
|
-
|
|
838
|
+
function gitRoot(dir) {
|
|
839
|
+
for (let d = dir; ; d = dirname3(d)) {
|
|
840
|
+
if (existsSync3(join4(d, ".git"))) return d;
|
|
841
|
+
if (d === dirname3(d)) return null;
|
|
842
|
+
}
|
|
695
843
|
}
|
|
696
|
-
|
|
697
|
-
|
|
844
|
+
function instructionPaths(cwd, harness2, home = homedir2()) {
|
|
845
|
+
const out = [];
|
|
846
|
+
if (cwd && existsSync3(cwd)) {
|
|
847
|
+
const root = gitRoot(cwd) ?? cwd;
|
|
848
|
+
for (let d = resolve2(cwd); ; d = dirname3(d)) {
|
|
849
|
+
for (const name of INSTRUCTION_FILES) out.push(join4(d, name));
|
|
850
|
+
if (d === root || d === dirname3(d)) break;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (harness2 === "claude_code") out.push(join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "CLAUDE.md"));
|
|
854
|
+
if (harness2 === "codex") out.push(join4(process.env.CODEX_HOME ?? join4(home, ".codex"), "AGENTS.md"));
|
|
855
|
+
out.push(...sharedInstructionPaths(home));
|
|
856
|
+
return out.filter((path) => {
|
|
857
|
+
try {
|
|
858
|
+
return statSync4(path).isFile();
|
|
859
|
+
} catch {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
});
|
|
698
863
|
}
|
|
699
|
-
|
|
700
|
-
|
|
864
|
+
function resourceName(path, cwd, home = homedir2()) {
|
|
865
|
+
const inside = cwd ? relative2(resolve2(cwd), path) : "";
|
|
866
|
+
if (cwd && inside && !inside.startsWith("..") && !isAbsolute(inside)) return `workspace/${inside.split(sep2).join("/")}`;
|
|
867
|
+
const underHome = relative2(home, path);
|
|
868
|
+
const shown = !underHome.startsWith("..") && !isAbsolute(underHome) ? `~/${underHome}` : path.replace(/^\/+/, "");
|
|
869
|
+
return `files/${shown.split(sep2).join("/")}`;
|
|
701
870
|
}
|
|
702
|
-
|
|
703
|
-
|
|
871
|
+
var ORDER = { text: 0, document: 1, image: 2 };
|
|
872
|
+
function resourceLine(name, mediaType, data) {
|
|
873
|
+
return redactLine(JSON.stringify({ type: "import.resource", name, media_type: mediaType, data: data.toString("base64") }), { requireStructured: true }).line + "\n";
|
|
704
874
|
}
|
|
705
|
-
|
|
706
|
-
|
|
875
|
+
function collectSessionFiles(records, opts) {
|
|
876
|
+
const home = opts.home ?? homedir2();
|
|
877
|
+
const exclude = new Set((opts.exclude ?? []).map((path) => resolve2(path)));
|
|
878
|
+
const leftOut = [];
|
|
879
|
+
const candidates = [];
|
|
880
|
+
const seen = /* @__PURE__ */ new Set();
|
|
881
|
+
const touched = touchedPaths(records, opts.cwd, home);
|
|
882
|
+
const skills2 = loadedSkills(records, touched, opts.cwd, home);
|
|
883
|
+
const consider = (path, name) => {
|
|
884
|
+
if (seen.has(path) || exclude.has(path)) return;
|
|
885
|
+
seen.add(path);
|
|
886
|
+
if (SECRET_FILE.test(path) || SKIP_DIR.test(path)) {
|
|
887
|
+
leftOut.push({ path, reason: "credential or build file" });
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
let size;
|
|
891
|
+
try {
|
|
892
|
+
const stat = statSync4(path);
|
|
893
|
+
if (!stat.isFile()) return;
|
|
894
|
+
size = stat.size;
|
|
895
|
+
} catch {
|
|
896
|
+
leftOut.push({ path, reason: "missing" });
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const type = classify(path);
|
|
900
|
+
if (!type) {
|
|
901
|
+
leftOut.push({ path, reason: "unsupported type" });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (size > MAX_ATTACHED_FILE_BYTES) {
|
|
905
|
+
leftOut.push({ path, reason: "too large" });
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
candidates.push({ path, name, ...type, size });
|
|
909
|
+
};
|
|
910
|
+
for (const path of [...instructionPaths(opts.cwd, opts.harness, home), ...touched]) consider(path, resourceName(path, opts.cwd, home));
|
|
911
|
+
for (const skill of skills2.found) for (const file of skillFiles(skill)) consider(file.path, file.name);
|
|
912
|
+
candidates.sort((a, b) => ORDER[a.kind] - ORDER[b.kind] || a.size - b.size);
|
|
913
|
+
const lines = [];
|
|
914
|
+
const attached = [];
|
|
915
|
+
let bytes = 0;
|
|
916
|
+
const state = workspaceState(opts.cwd, opts.harness, skills2, home, opts.title ?? null);
|
|
917
|
+
const described = !!opts.cwd || !!state.manifest.title || state.manifest.skills.length > 0 || state.manifest.mcp_servers.length > 0;
|
|
918
|
+
const generated = [
|
|
919
|
+
...described ? [{ name: "agentsky/workspace.json", mediaType: "application/json", data: Buffer.from(JSON.stringify(state.manifest, null, 2)) }] : [],
|
|
920
|
+
...state.patch ? [{ name: "agentsky/uncommitted.patch", mediaType: "text/x-diff", data: Buffer.from(state.patch) }] : []
|
|
921
|
+
];
|
|
922
|
+
for (const file of generated) {
|
|
923
|
+
let line;
|
|
924
|
+
try {
|
|
925
|
+
line = resourceLine(file.name, file.mediaType, file.data);
|
|
926
|
+
} catch {
|
|
927
|
+
leftOut.push({ path: file.name, reason: "could not be redacted safely" });
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const size = Buffer.byteLength(line);
|
|
931
|
+
if (bytes + size > opts.budget) {
|
|
932
|
+
leftOut.push({ path: file.name, reason: "over the upload budget" });
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
bytes += size;
|
|
936
|
+
lines.push(line);
|
|
937
|
+
attached.push({ name: file.name, kind: "text", bytes: file.data.length });
|
|
938
|
+
}
|
|
939
|
+
for (const file of candidates) {
|
|
940
|
+
let data;
|
|
941
|
+
try {
|
|
942
|
+
data = readFileSync4(file.path);
|
|
943
|
+
} catch {
|
|
944
|
+
leftOut.push({ path: file.path, reason: "missing" });
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (file.kind === "text" && data.includes(0)) {
|
|
948
|
+
leftOut.push({ path: file.path, reason: "not valid UTF-8 text" });
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
let line;
|
|
952
|
+
try {
|
|
953
|
+
line = resourceLine(file.name, file.mediaType, data);
|
|
954
|
+
} catch {
|
|
955
|
+
leftOut.push({ path: file.path, reason: file.kind === "text" && !isUtf8(data) ? "not valid UTF-8 text" : "could not be redacted safely" });
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const size = Buffer.byteLength(line);
|
|
959
|
+
if (bytes + size > opts.budget) {
|
|
960
|
+
leftOut.push({ path: file.path, reason: "over the upload budget" });
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
bytes += size;
|
|
964
|
+
lines.push(line);
|
|
965
|
+
attached.push({ name: file.name, kind: file.kind, bytes: data.length });
|
|
966
|
+
}
|
|
967
|
+
const setup = {
|
|
968
|
+
skills: skills2.found.map((skill) => skill.name),
|
|
969
|
+
missingSkills: skills2.missing,
|
|
970
|
+
mcpServers: state.manifest.mcp_servers.map((server) => server.name),
|
|
971
|
+
git: state.manifest.git !== null
|
|
972
|
+
};
|
|
973
|
+
return { lines, bytes, attached, leftOut, setup };
|
|
707
974
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
975
|
+
|
|
976
|
+
// src/import-environment.ts
|
|
977
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
978
|
+
import { createHash as createHash2 } from "crypto";
|
|
979
|
+
import { closeSync as closeSync2, createWriteStream, lstatSync, mkdtempSync, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, readSync as readSync2, rmSync as rmSync2 } from "fs";
|
|
980
|
+
import { tmpdir } from "os";
|
|
981
|
+
import { join as join5 } from "path";
|
|
982
|
+
import { isUtf8 as isUtf82 } from "buffer";
|
|
983
|
+
import { createGzip } from "zlib";
|
|
984
|
+
|
|
985
|
+
// ../../packages/spec/src/import-layout.ts
|
|
986
|
+
import { gzipSync } from "zlib";
|
|
987
|
+
function folderName(workspace2, sourceCwd) {
|
|
988
|
+
const raw = workspace2?.project || (sourceCwd ? sourceCwd.split("/").filter(Boolean).pop() : "") || "";
|
|
989
|
+
const cleaned = raw.replace(/[^A-Za-z0-9._ -]/g, "-").replace(/^[^A-Za-z0-9_]+/, "").replace(/\.\.+/g, ".").slice(0, 80).trim();
|
|
990
|
+
return cleaned || "imported-session";
|
|
991
|
+
}
|
|
992
|
+
function envName(server, field) {
|
|
993
|
+
return `${server}_${field}`.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
994
|
+
}
|
|
995
|
+
function ownServer(server, platform) {
|
|
996
|
+
try {
|
|
997
|
+
return server.transport !== "stdio" && platform.ownHost.test(new URL(server.url ?? "").hostname);
|
|
998
|
+
} catch {
|
|
999
|
+
return false;
|
|
713
1000
|
}
|
|
714
1001
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1002
|
+
function mcpJson(servers, platform) {
|
|
1003
|
+
const entries = servers.filter((server) => !ownServer(server, platform)).map((server) => {
|
|
1004
|
+
if (server.transport === "stdio") {
|
|
1005
|
+
return [server.name, {
|
|
1006
|
+
command: server.command,
|
|
1007
|
+
...server.args?.length ? { args: server.args } : {},
|
|
1008
|
+
...server.env?.length ? { env: Object.fromEntries(server.env.map((name) => [name, `\${${name}:-}`])) } : {}
|
|
1009
|
+
}];
|
|
1010
|
+
}
|
|
1011
|
+
const query = server.query?.length ? `?${server.query.map((name) => `${encodeURIComponent(name)}=\${${envName(server.name, name)}:-}`).join("&")}` : "";
|
|
1012
|
+
return [server.name, {
|
|
1013
|
+
type: server.transport,
|
|
1014
|
+
url: `${server.url}${query}`,
|
|
1015
|
+
...server.headers?.length ? { headers: Object.fromEntries(server.headers.map((name) => [name, `\${${envName(server.name, name)}:-}`])) } : {}
|
|
1016
|
+
}];
|
|
1017
|
+
});
|
|
1018
|
+
return JSON.stringify({ mcpServers: Object.fromEntries(entries) }, null, 2) + "\n";
|
|
1019
|
+
}
|
|
1020
|
+
function bullet(items, none) {
|
|
1021
|
+
return items.length ? items.map((item) => `- ${item}`).join("\n") : `- ${none}`;
|
|
1022
|
+
}
|
|
1023
|
+
function readme(folder, workspace2, counts, skills2, mcpWritten, platform) {
|
|
1024
|
+
const git2 = workspace2?.git;
|
|
1025
|
+
const servers = workspace2?.mcp_servers ?? [];
|
|
1026
|
+
const missingSkills = (workspace2?.skills ?? []).filter((skill) => !skill.found).map((skill) => skill.name);
|
|
1027
|
+
const secrets2 = servers.filter((server) => !ownServer(server, platform)).flatMap((server) => [
|
|
1028
|
+
...server.env ?? [],
|
|
1029
|
+
...(server.headers ?? []).map((name) => envName(server.name, name)),
|
|
1030
|
+
...(server.query ?? []).map((name) => envName(server.name, name))
|
|
1031
|
+
]);
|
|
1032
|
+
const where = workspace2?.cwd ? ` (${workspace2.cwd})` : "";
|
|
719
1033
|
return [
|
|
720
|
-
|
|
721
|
-
agent2.tagline,
|
|
722
|
-
...agent2.description && agent2.description !== agent2.tagline ? ["", agent2.description] : [],
|
|
1034
|
+
"# Imported working copy",
|
|
723
1035
|
"",
|
|
724
|
-
`
|
|
725
|
-
|
|
726
|
-
...section("Does", agent2.does),
|
|
727
|
-
...section("Does not", agent2.does_not),
|
|
728
|
-
...section("Examples", agent2.examples),
|
|
1036
|
+
`This folder (/workspace/${folder}) holds what the imported conversation worked with on the person's computer.`,
|
|
1037
|
+
"It is historical context: verify a path before acting on it, and follow the person's current message.",
|
|
729
1038
|
"",
|
|
730
|
-
|
|
1039
|
+
"## Files",
|
|
1040
|
+
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\`)` : ""}.`,
|
|
1041
|
+
...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/.`] : [],
|
|
1042
|
+
...counts.attachments ? [`- ${counts.attachments} attachments of the conversation under ${platform.notesDir}/attachments/.`] : [],
|
|
1043
|
+
"- 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.",
|
|
1044
|
+
"",
|
|
1045
|
+
"## Repository",
|
|
1046
|
+
...git2 ? [
|
|
1047
|
+
`- Remote: ${git2.remote ?? "none"}`,
|
|
1048
|
+
`- Branch: ${git2.branch ?? "detached"} at commit ${git2.commit ?? "unknown"}${git2.ahead ? ` (${git2.ahead} commits not pushed)` : ""}`,
|
|
1049
|
+
...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."],
|
|
1050
|
+
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.`
|
|
1051
|
+
] : ["- The working directory was not a git repository."],
|
|
1052
|
+
"",
|
|
1053
|
+
"## Skills",
|
|
1054
|
+
bullet(skills2.map((name) => `${name}: .claude/skills/${name}/SKILL.md (also .agents/skills/${name}/)`), "No skills were loaded."),
|
|
1055
|
+
...missingSkills.length ? ["", `Loaded but no longer on that computer, so not carried: ${missingSkills.join(", ")}.`] : [],
|
|
1056
|
+
"",
|
|
1057
|
+
"## Tool servers (MCP)",
|
|
1058
|
+
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."),
|
|
1059
|
+
...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.`] : [],
|
|
1060
|
+
""
|
|
731
1061
|
].join("\n");
|
|
732
1062
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
import { spawn } from "child_process";
|
|
736
|
-
import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
737
|
-
import { homedir } from "os";
|
|
738
|
-
import { basename as basename3, join as join3 } from "path";
|
|
739
|
-
import { createInterface } from "readline";
|
|
740
|
-
var PROMPT_CAP = 1e5;
|
|
741
|
-
var HARNESSES = ["claude_code", "codex", "openclaw"];
|
|
742
|
-
function claudeConfigRoot() {
|
|
743
|
-
return process.env.CLAUDE_CONFIG_DIR ?? join3(homedir(), ".claude");
|
|
744
|
-
}
|
|
745
|
-
function readIfExists(path) {
|
|
1063
|
+
function parseWorkspace(file) {
|
|
1064
|
+
if (!file) return null;
|
|
746
1065
|
try {
|
|
747
|
-
|
|
1066
|
+
const value = JSON.parse(file.data.toString("utf8"));
|
|
1067
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
748
1068
|
} catch {
|
|
749
|
-
return
|
|
1069
|
+
return null;
|
|
750
1070
|
}
|
|
751
1071
|
}
|
|
752
|
-
function
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1072
|
+
function importLayout(files2, sourceCwd, platform, wholeFolder) {
|
|
1073
|
+
if (!files2.length && !wholeFolder) return null;
|
|
1074
|
+
const workspace2 = parseWorkspace(files2.find((file) => file.name === "agentsky/workspace.json"));
|
|
1075
|
+
const folder = folderName(workspace2, sourceCwd);
|
|
1076
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1077
|
+
const skills2 = /* @__PURE__ */ new Set();
|
|
1078
|
+
const counts = { project: wholeFolder?.files ?? 0, outside: 0, attachments: 0, wholeFolder: Boolean(wholeFolder) };
|
|
1079
|
+
for (const file of files2) {
|
|
1080
|
+
const [kind, ...rest] = file.name.split("/");
|
|
1081
|
+
const path = rest.join("/");
|
|
1082
|
+
if (kind === "workspace") {
|
|
1083
|
+
if (wholeFolder) continue;
|
|
1084
|
+
entries.set(path, file.data);
|
|
1085
|
+
counts.project += 1;
|
|
1086
|
+
} else if (kind === "files") {
|
|
1087
|
+
entries.set(`${platform.notesDir}/files/${path.replace(/^~\//, "home/")}`, file.data);
|
|
1088
|
+
counts.outside += 1;
|
|
1089
|
+
} else if (kind === "imported-attachments") {
|
|
1090
|
+
entries.set(`${platform.notesDir}/attachments/${path}`, file.data);
|
|
1091
|
+
counts.attachments += 1;
|
|
1092
|
+
} else if (kind === "agentsky") entries.set(`${platform.notesDir}/${path}`, file.data);
|
|
1093
|
+
else if (kind === "skills" && rest.length > 1) {
|
|
1094
|
+
const skill = rest[0].split(":").pop();
|
|
1095
|
+
const inside = rest.slice(1).join("/");
|
|
1096
|
+
skills2.add(skill);
|
|
1097
|
+
entries.set(`.claude/skills/${skill}/${inside}`, file.data);
|
|
1098
|
+
entries.set(`.agents/skills/${skill}/${inside}`, file.data);
|
|
1099
|
+
}
|
|
759
1100
|
}
|
|
1101
|
+
const servers = (workspace2?.mcp_servers ?? []).filter((server) => server && typeof server.name === "string" && (server.transport === "stdio" ? typeof server.command === "string" : typeof server.url === "string"));
|
|
1102
|
+
const mcpWritten = servers.length > 0 && !entries.has(".mcp.json") && !wholeFolder?.paths.has(".mcp.json");
|
|
1103
|
+
if (mcpWritten) entries.set(".mcp.json", Buffer.from(mcpJson(servers, platform)));
|
|
1104
|
+
entries.set(`${platform.notesDir}/README.md`, Buffer.from(readme(folder, workspace2 ? { ...workspace2, mcp_servers: servers } : null, counts, [...skills2], mcpWritten, platform)));
|
|
1105
|
+
if (wholeFolder) for (const path of wholeFolder.paths) entries.delete(path);
|
|
1106
|
+
return { folder, entries, skills: [...skills2], mcpServers: servers.map((server) => server.name) };
|
|
760
1107
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1108
|
+
var TAR_BLOCK = 512;
|
|
1109
|
+
var TAR_END = Buffer.alloc(TAR_BLOCK * 2);
|
|
1110
|
+
function header(name, size, type) {
|
|
1111
|
+
const block = Buffer.alloc(TAR_BLOCK);
|
|
1112
|
+
block.write(name, 0, 100, "utf8");
|
|
1113
|
+
block.write("0000644\0", 100);
|
|
1114
|
+
block.write("0000000\0", 108);
|
|
1115
|
+
block.write("0000000\0", 116);
|
|
1116
|
+
block.write(size.toString(8).padStart(11, "0") + "\0", 124);
|
|
1117
|
+
block.write("00000000000\0", 136);
|
|
1118
|
+
block.write(" ", 148);
|
|
1119
|
+
block.write(type, 156);
|
|
1120
|
+
block.write("ustar\x0000", 257);
|
|
1121
|
+
let sum = 0;
|
|
1122
|
+
for (const byte of block) sum += byte;
|
|
1123
|
+
block.write(sum.toString(8).padStart(6, "0") + "\0 ", 148);
|
|
1124
|
+
return block;
|
|
1125
|
+
}
|
|
1126
|
+
function paxRecord(key, value) {
|
|
1127
|
+
const body = ` ${key}=${value}
|
|
1128
|
+
`;
|
|
1129
|
+
let length = Buffer.byteLength(body);
|
|
1130
|
+
for (; ; ) {
|
|
1131
|
+
const total = String(length).length + length;
|
|
1132
|
+
if (String(total).length + length === total) return Buffer.from(`${total}${body}`);
|
|
1133
|
+
length += 1;
|
|
770
1134
|
}
|
|
771
|
-
return void 0;
|
|
772
1135
|
}
|
|
773
|
-
function
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
);
|
|
781
|
-
Object.assign(merged, projectConfig?.mcpServers ?? {});
|
|
782
|
-
return merged;
|
|
1136
|
+
function tarPadding(size) {
|
|
1137
|
+
const rest = size % TAR_BLOCK;
|
|
1138
|
+
return Buffer.alloc(rest ? TAR_BLOCK - rest : 0);
|
|
1139
|
+
}
|
|
1140
|
+
function tarHeader(path, size) {
|
|
1141
|
+
if (Buffer.byteLength(path) <= 100) return header(path, size, "0");
|
|
1142
|
+
const pax = paxRecord("path", path);
|
|
1143
|
+
return Buffer.concat([header("PaxHeader", pax.length, "x"), pax, tarPadding(pax.length), header(path, size, "0")]);
|
|
783
1144
|
}
|
|
784
|
-
function assemblePrompt(parts) {
|
|
785
|
-
const joined = parts.map((p) => `# \u2500\u2500 ${p.label} \u2500\u2500
|
|
786
1145
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1146
|
+
// src/import-environment.ts
|
|
1147
|
+
var IMPORT_PLATFORM = { name: "AgentSky", ownHost: /(^|\.)(agentsky\.dev|asteroidbelt\.ai)$/i, notesDir: ".agentsky-import" };
|
|
1148
|
+
var MAX_ENVIRONMENT_FILES = 2e5;
|
|
1149
|
+
var MAX_ENVIRONMENT_BYTES = 2 * 1024 ** 3;
|
|
1150
|
+
var MAX_REDACTED_TEXT_BYTES = 10 * 1024 * 1024;
|
|
1151
|
+
var SKIPPED = /* @__PURE__ */ new Set([".git", "node_modules", ".venv", "venv", "__pycache__", ".next", "dist", "build", "target", ".DS_Store", ".cache", ".turbo"]);
|
|
1152
|
+
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;
|
|
1153
|
+
function listFiles(cwd) {
|
|
1154
|
+
const git2 = spawnSync2("git", ["-C", cwd, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 512 * 1024 * 1024 });
|
|
1155
|
+
if (git2.status === 0) return git2.stdout.split("\0").filter(Boolean).filter((path) => !path.split("/").some((part) => part === ".git"));
|
|
1156
|
+
const out = [];
|
|
1157
|
+
const walk = (dir, prefix) => {
|
|
1158
|
+
let entries;
|
|
1159
|
+
try {
|
|
1160
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
1161
|
+
} catch {
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
for (const entry2 of entries) {
|
|
1165
|
+
if (SKIPPED.has(entry2.name)) continue;
|
|
1166
|
+
const rel = prefix ? `${prefix}/${entry2.name}` : entry2.name;
|
|
1167
|
+
if (entry2.isDirectory()) walk(join5(dir, entry2.name), rel);
|
|
1168
|
+
else out.push(rel);
|
|
1169
|
+
if (out.length > MAX_ENVIRONMENT_FILES) return;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
walk(cwd, "");
|
|
1173
|
+
return out;
|
|
791
1174
|
}
|
|
792
|
-
function
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1175
|
+
function textOf(path, size) {
|
|
1176
|
+
if (size > MAX_REDACTED_TEXT_BYTES) return null;
|
|
1177
|
+
const data = readFileSync5(path);
|
|
1178
|
+
if (data.subarray(0, 8e3).includes(0) || !isUtf82(data)) return null;
|
|
1179
|
+
return data;
|
|
1180
|
+
}
|
|
1181
|
+
async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
|
|
1182
|
+
const listed = listFiles(cwd);
|
|
1183
|
+
if (listed.length > MAX_ENVIRONMENT_FILES) throw new Error(`The working folder has more than ${MAX_ENVIRONMENT_FILES} files; import it with --no-environment.`);
|
|
1184
|
+
const scratch = mkdtempSync(join5(tmpdir(), "sky-environment-"));
|
|
1185
|
+
const path = join5(scratch, "environment.tar.gz");
|
|
1186
|
+
const leftOut = [];
|
|
1187
|
+
const written2 = /* @__PURE__ */ new Set();
|
|
1188
|
+
const hash = createHash2("sha256");
|
|
1189
|
+
let bytes = 0;
|
|
1190
|
+
let redacted2 = 0;
|
|
1191
|
+
const gzip = createGzip({ level: 6 });
|
|
1192
|
+
const sink = createWriteStream(path);
|
|
1193
|
+
gzip.on("data", (chunk) => {
|
|
1194
|
+
bytes += chunk.length;
|
|
1195
|
+
hash.update(chunk);
|
|
1196
|
+
});
|
|
1197
|
+
gzip.pipe(sink);
|
|
1198
|
+
const done = new Promise((resolve10, reject) => {
|
|
1199
|
+
sink.on("finish", () => resolve10());
|
|
1200
|
+
sink.on("error", reject);
|
|
1201
|
+
gzip.on("error", reject);
|
|
1202
|
+
});
|
|
1203
|
+
const write = (chunk) => new Promise((resolve10, reject) => {
|
|
1204
|
+
if (bytes > MAX_ENVIRONMENT_BYTES) {
|
|
1205
|
+
reject(new Error(`The working folder packs to more than ${MAX_ENVIRONMENT_BYTES / 1024 ** 3} GiB; import it with --no-environment.`));
|
|
1206
|
+
return;
|
|
807
1207
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1208
|
+
if (gzip.write(chunk)) resolve10();
|
|
1209
|
+
else gzip.once("drain", resolve10);
|
|
1210
|
+
});
|
|
1211
|
+
try {
|
|
1212
|
+
for (const rel of listed) {
|
|
1213
|
+
const full = join5(cwd, ...rel.split("/"));
|
|
1214
|
+
if (SECRET_FILE2.test(rel)) {
|
|
1215
|
+
leftOut.push({ path: rel, reason: "credential file" });
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
let stat;
|
|
1219
|
+
try {
|
|
1220
|
+
stat = lstatSync(full);
|
|
1221
|
+
} catch {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
if (!stat.isFile()) {
|
|
1225
|
+
leftOut.push({ path: rel, reason: "not a regular file" });
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
const text = textOf(full, stat.size);
|
|
1229
|
+
if (text) {
|
|
1230
|
+
const masked = redactSecretValues(text.toString("utf8"));
|
|
1231
|
+
if (Object.keys(masked.hits).length) redacted2 += 1;
|
|
1232
|
+
const data = Buffer.from(masked.text, "utf8");
|
|
1233
|
+
await write(tarHeader(rel, data.length));
|
|
1234
|
+
await write(data);
|
|
1235
|
+
await write(tarPadding(data.length));
|
|
1236
|
+
} else {
|
|
1237
|
+
await write(tarHeader(rel, stat.size));
|
|
1238
|
+
const fd = openSync2(full, "r");
|
|
1239
|
+
try {
|
|
1240
|
+
const chunk = Buffer.alloc(1024 * 1024);
|
|
1241
|
+
let left = stat.size;
|
|
1242
|
+
for (let offset = 0; left > 0; ) {
|
|
1243
|
+
const read = readSync2(fd, chunk, 0, Math.min(chunk.length, left), offset);
|
|
1244
|
+
if (read <= 0) throw new Error(`${rel} changed while it was packed; import again.`);
|
|
1245
|
+
await write(Buffer.from(chunk.subarray(0, read)));
|
|
1246
|
+
offset += read;
|
|
1247
|
+
left -= read;
|
|
1248
|
+
}
|
|
1249
|
+
} finally {
|
|
1250
|
+
closeSync2(fd);
|
|
1251
|
+
}
|
|
1252
|
+
await write(tarPadding(stat.size));
|
|
820
1253
|
}
|
|
1254
|
+
written2.add(rel);
|
|
1255
|
+
}
|
|
1256
|
+
const layout = importLayout(resources, cwd, platform, { files: written2.size, paths: written2 });
|
|
1257
|
+
for (const [entry2, data] of layout?.entries ?? []) {
|
|
1258
|
+
await write(tarHeader(entry2, data.length));
|
|
1259
|
+
await write(data);
|
|
1260
|
+
await write(tarPadding(data.length));
|
|
821
1261
|
}
|
|
1262
|
+
await write(TAR_END);
|
|
1263
|
+
gzip.end();
|
|
1264
|
+
await done;
|
|
1265
|
+
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.`);
|
|
1266
|
+
return {
|
|
1267
|
+
path,
|
|
1268
|
+
folder: layout?.folder ?? "imported-session",
|
|
1269
|
+
bytes,
|
|
1270
|
+
sha256: hash.digest("hex"),
|
|
1271
|
+
files: written2.size + (layout?.entries.size ?? 0),
|
|
1272
|
+
redacted: redacted2,
|
|
1273
|
+
leftOut,
|
|
1274
|
+
dispose: () => rmSync2(scratch, { recursive: true, force: true })
|
|
1275
|
+
};
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
gzip.destroy();
|
|
1278
|
+
sink.destroy();
|
|
1279
|
+
rmSync2(scratch, { recursive: true, force: true });
|
|
1280
|
+
throw error;
|
|
822
1281
|
}
|
|
823
|
-
return {
|
|
824
|
-
harness: "claude_code",
|
|
825
|
-
configRoot,
|
|
826
|
-
prompt,
|
|
827
|
-
promptSources: sources,
|
|
828
|
-
truncated,
|
|
829
|
-
model: claudeModel(configRoot),
|
|
830
|
-
keptServers: kept,
|
|
831
|
-
droppedServers: dropped,
|
|
832
|
-
secretCandidates
|
|
833
|
-
};
|
|
834
1282
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1283
|
+
function resourcesOf(lines) {
|
|
1284
|
+
return lines.flatMap((line) => {
|
|
1285
|
+
try {
|
|
1286
|
+
const record = JSON.parse(line);
|
|
1287
|
+
if (record.type !== "import.resource" || typeof record.name !== "string" || typeof record.media_type !== "string" || typeof record.data !== "string") return [];
|
|
1288
|
+
return [{ name: record.name, mediaType: record.media_type, data: Buffer.from(record.data, "base64") }];
|
|
1289
|
+
} catch {
|
|
1290
|
+
return [];
|
|
839
1291
|
}
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
if (existsSync3(claudeConfigRoot())) return "claude_code";
|
|
843
|
-
if (existsSync3(join3(homedir(), ".codex"))) return "codex";
|
|
844
|
-
if (existsSync3(join3(homedir(), ".openclaw"))) return "openclaw";
|
|
845
|
-
throw new Error(
|
|
846
|
-
"no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
|
|
847
|
-
);
|
|
1292
|
+
});
|
|
848
1293
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
);
|
|
1294
|
+
|
|
1295
|
+
// src/import-local.ts
|
|
1296
|
+
function redactLine2(raw) {
|
|
1297
|
+
return redactLine(raw).line;
|
|
854
1298
|
}
|
|
855
|
-
function
|
|
856
|
-
|
|
1299
|
+
function decodeWorkspace(encoded) {
|
|
1300
|
+
try {
|
|
1301
|
+
return decodeURIComponent(encoded.replace(/-/g, "%").replace(/_/g, "%5F"));
|
|
1302
|
+
} catch {
|
|
1303
|
+
return encoded;
|
|
1304
|
+
}
|
|
857
1305
|
}
|
|
858
|
-
function
|
|
859
|
-
const
|
|
860
|
-
|
|
1306
|
+
function readPrefix(filePath) {
|
|
1307
|
+
const fd = openSync3(filePath, "r");
|
|
1308
|
+
try {
|
|
1309
|
+
const bytes = Buffer.alloc(256 * 1024);
|
|
1310
|
+
return bytes.subarray(0, readSync3(fd, bytes, 0, bytes.length, 0)).toString("utf8");
|
|
1311
|
+
} finally {
|
|
1312
|
+
closeSync3(fd);
|
|
1313
|
+
}
|
|
861
1314
|
}
|
|
862
|
-
function
|
|
863
|
-
writeFileSync3(linkPath(configRoot), `${JSON.stringify(link2, null, 2)}
|
|
864
|
-
`);
|
|
865
|
-
}
|
|
866
|
-
var DEFAULT_VAULT_NAME = "default";
|
|
867
|
-
async function fetchDefaultVaultId() {
|
|
868
|
-
const { vaults } = await api(
|
|
869
|
-
"GET",
|
|
870
|
-
"/vaults"
|
|
871
|
-
);
|
|
872
|
-
return vaults.find((v) => v.display_name === DEFAULT_VAULT_NAME)?.id ?? null;
|
|
873
|
-
}
|
|
874
|
-
async function ensureDefaultVaultId() {
|
|
875
|
-
const existing = await fetchDefaultVaultId();
|
|
876
|
-
if (existing) return existing;
|
|
1315
|
+
function sniffTitle(filePath) {
|
|
877
1316
|
try {
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1317
|
+
const data = readPrefix(filePath);
|
|
1318
|
+
for (const raw of data.split("\n")) {
|
|
1319
|
+
if (!raw.trim()) continue;
|
|
1320
|
+
let line;
|
|
1321
|
+
try {
|
|
1322
|
+
line = JSON.parse(redactLine2(raw));
|
|
1323
|
+
} catch {
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (!line || typeof line !== "object") continue;
|
|
1327
|
+
const rec = line;
|
|
1328
|
+
if (rec.type === "user" && rec.message && typeof rec.message === "object") {
|
|
1329
|
+
const msg = rec.message;
|
|
1330
|
+
if (Array.isArray(msg.content)) {
|
|
1331
|
+
for (const block of msg.content) {
|
|
1332
|
+
if (typeof block === "object" && block !== null) {
|
|
1333
|
+
const b = block;
|
|
1334
|
+
if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
|
|
1335
|
+
return b.text.trim().slice(0, 120);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (typeof msg.content === "string" && msg.content.trim()) {
|
|
1341
|
+
return msg.content.trim().slice(0, 120);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (rec.type === "event_msg" && rec.payload && typeof rec.payload === "object") {
|
|
1345
|
+
const payload = rec.payload;
|
|
1346
|
+
if (payload.type === "user_message" && typeof payload.message === "string") return redactLine2(payload.message).slice(0, 120);
|
|
1347
|
+
}
|
|
1348
|
+
if (rec.type === "response_item" && rec.payload && typeof rec.payload === "object") {
|
|
1349
|
+
const payload = rec.payload;
|
|
1350
|
+
if (payload.type === "message" && payload.role === "user" && Array.isArray(payload.content)) {
|
|
1351
|
+
const text = payload.content.find((part) => typeof part?.text === "string")?.text;
|
|
1352
|
+
if (typeof text === "string" && text.trim()) return redactLine2(text.trim()).slice(0, 120);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (rec.type === "session_meta") continue;
|
|
1356
|
+
if (rec.type === "user_turn" || rec.type === "message" && rec.role === "user") {
|
|
1357
|
+
const content = rec.content;
|
|
1358
|
+
if (typeof content === "string" && content.trim()) return content.trim().slice(0, 120);
|
|
1359
|
+
}
|
|
886
1360
|
}
|
|
887
|
-
|
|
1361
|
+
return null;
|
|
1362
|
+
} catch {
|
|
1363
|
+
return null;
|
|
888
1364
|
}
|
|
889
1365
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1366
|
+
function listClaudeCodeSessions(limit) {
|
|
1367
|
+
const root = process.env.CLAUDE_CONFIG_DIR ?? join6(homedir3(), ".claude");
|
|
1368
|
+
const projectsDir = join6(root, "projects");
|
|
1369
|
+
if (!existsSync4(projectsDir)) return [];
|
|
1370
|
+
const sessions = [];
|
|
1371
|
+
try {
|
|
1372
|
+
for (const encoded of readdirSync4(projectsDir)) {
|
|
1373
|
+
const projectDir = join6(projectsDir, encoded);
|
|
1374
|
+
let entries;
|
|
1375
|
+
try {
|
|
1376
|
+
entries = readdirSync4(projectDir);
|
|
1377
|
+
} catch {
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
const workspace2 = decodeWorkspace(encoded);
|
|
1381
|
+
for (const entry2 of entries) {
|
|
1382
|
+
if (!entry2.endsWith(".jsonl")) continue;
|
|
1383
|
+
const sessionId = entry2.slice(0, -6);
|
|
1384
|
+
const mainFile = join6(projectDir, entry2);
|
|
1385
|
+
let stat;
|
|
1386
|
+
try {
|
|
1387
|
+
stat = statSync5(mainFile);
|
|
1388
|
+
} catch {
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (!stat.isFile()) continue;
|
|
1392
|
+
let discoveryError;
|
|
1393
|
+
const subagentFiles = [];
|
|
1394
|
+
const subagentDir = join6(projectDir, sessionId, "subagents");
|
|
1395
|
+
if (existsSync4(subagentDir)) {
|
|
1396
|
+
try {
|
|
1397
|
+
const subFiles = readdirSync4(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
1398
|
+
for (const f of subFiles) {
|
|
1399
|
+
subagentFiles.push(join6(subagentDir, f));
|
|
1400
|
+
}
|
|
1401
|
+
} catch {
|
|
1402
|
+
discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
sessions.push({
|
|
1406
|
+
harness: "claude_code",
|
|
1407
|
+
sessionId,
|
|
1408
|
+
workspace: workspace2,
|
|
1409
|
+
title: sniffTitle(mainFile),
|
|
1410
|
+
mtimeMs: stat.mtimeMs,
|
|
1411
|
+
files: [mainFile, ...subagentFiles],
|
|
1412
|
+
...discoveryError ? { discoveryError } : {}
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
909
1415
|
}
|
|
1416
|
+
} catch {
|
|
1417
|
+
}
|
|
1418
|
+
return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
1419
|
+
}
|
|
1420
|
+
function listCodexSessions(limit) {
|
|
1421
|
+
const sessionsDir = join6(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join6(homedir3(), ".codex"), "sessions");
|
|
1422
|
+
const records = [];
|
|
1423
|
+
function walk(dir) {
|
|
1424
|
+
let entries;
|
|
910
1425
|
try {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
|
|
1426
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
1427
|
+
} catch {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
for (const entry2 of entries) {
|
|
1431
|
+
const full = join6(dir, entry2.name);
|
|
1432
|
+
if (entry2.isDirectory()) {
|
|
1433
|
+
walk(full);
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (!entry2.isFile() || !entry2.name.startsWith("rollout-") || !entry2.name.endsWith(".jsonl")) continue;
|
|
1437
|
+
try {
|
|
1438
|
+
const first = readPrefix(full).split("\n", 1)[0];
|
|
1439
|
+
const meta = JSON.parse(first);
|
|
1440
|
+
if (meta.type !== "session_meta" || typeof meta.payload?.id !== "string") continue;
|
|
1441
|
+
const data = meta.payload;
|
|
1442
|
+
const parent = data.forked_from_id ?? data.source?.spawn?.parent_thread_id ?? data.source?.subagent?.spawn?.parent_thread_id;
|
|
1443
|
+
records.push({
|
|
1444
|
+
session: {
|
|
1445
|
+
harness: "codex",
|
|
1446
|
+
sessionId: data.id,
|
|
1447
|
+
workspace: typeof data.cwd === "string" ? data.cwd : dir,
|
|
1448
|
+
title: sniffTitle(full),
|
|
1449
|
+
mtimeMs: statSync5(full).mtimeMs,
|
|
1450
|
+
files: [full]
|
|
1451
|
+
},
|
|
1452
|
+
...typeof parent === "string" ? { parent } : {}
|
|
1453
|
+
});
|
|
1454
|
+
} catch {
|
|
1455
|
+
continue;
|
|
929
1456
|
}
|
|
930
1457
|
}
|
|
931
1458
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
)
|
|
1459
|
+
walk(sessionsDir);
|
|
1460
|
+
function descendants(id, visited) {
|
|
1461
|
+
if (visited.has(id)) return [];
|
|
1462
|
+
visited.add(id);
|
|
1463
|
+
return records.filter((item) => item.parent === id).flatMap((item) => [
|
|
1464
|
+
...item.session.files,
|
|
1465
|
+
...descendants(item.session.sessionId, visited)
|
|
1466
|
+
]);
|
|
937
1467
|
}
|
|
1468
|
+
return records.filter((item) => !item.parent).map(({ session: session2 }) => ({
|
|
1469
|
+
...session2,
|
|
1470
|
+
files: [.../* @__PURE__ */ new Set([...session2.files, ...descendants(session2.sessionId, /* @__PURE__ */ new Set())])]
|
|
1471
|
+
})).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
938
1472
|
}
|
|
939
|
-
function
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1473
|
+
function listRecentSessions(harness2, limit) {
|
|
1474
|
+
const claudeSessions = harness2 === "codex" ? [] : listClaudeCodeSessions(limit);
|
|
1475
|
+
const codexSessions = harness2 === "claude_code" ? [] : listCodexSessions(limit);
|
|
1476
|
+
return [...claudeSessions, ...codexSessions].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
1477
|
+
}
|
|
1478
|
+
function agentUseBase() {
|
|
1479
|
+
const env = process.env.AGENT_USE_URL?.trim().replace(/\/$/, "");
|
|
1480
|
+
if (env) return env;
|
|
1481
|
+
const api2 = apiBase().replace(/\/$/, "");
|
|
1482
|
+
const derived = api2.replace(/\/api\/v1$/, "").replace(/\/v1$/, "").replace("api.agentsky.dev", "agentsky.dev");
|
|
1483
|
+
if (derived.includes("localhost:3000")) return derived.replace("localhost:3000", "localhost:3002");
|
|
1484
|
+
return derived;
|
|
1485
|
+
}
|
|
1486
|
+
function currentSessionEnv() {
|
|
1487
|
+
const claudeId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
1488
|
+
if (claudeId) return { harness: "claude_code", sessionId: claudeId };
|
|
1489
|
+
const codexId = process.env.CODEX_THREAD_ID;
|
|
1490
|
+
if (codexId) return { harness: "codex", sessionId: codexId };
|
|
1491
|
+
return void 0;
|
|
1492
|
+
}
|
|
1493
|
+
async function mintTrace(base, harness2) {
|
|
1494
|
+
const token = apiToken();
|
|
1495
|
+
if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
|
|
1496
|
+
const res = await fetch(`${base}/api/traces`, {
|
|
1497
|
+
method: "POST",
|
|
1498
|
+
redirect: "error",
|
|
1499
|
+
signal: AbortSignal.timeout(15e3),
|
|
1500
|
+
headers: {
|
|
1501
|
+
"content-type": "application/json",
|
|
1502
|
+
authorization: `Bearer ${token}`,
|
|
1503
|
+
"user-agent": USER_AGENT
|
|
1504
|
+
},
|
|
1505
|
+
body: JSON.stringify({ harness: harness2 })
|
|
946
1506
|
});
|
|
1507
|
+
if (!res.ok) {
|
|
1508
|
+
const body = await res.json().catch(() => null);
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
typeof body?.error === "string" ? body.error : `Failed to create import (${res.status}).`
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
return await res.json();
|
|
947
1514
|
}
|
|
948
|
-
function
|
|
949
|
-
|
|
1515
|
+
function loadSummaryFile(summaryPath) {
|
|
1516
|
+
let raw;
|
|
950
1517
|
try {
|
|
951
|
-
|
|
1518
|
+
raw = readFileSync6(summaryPath, "utf8");
|
|
952
1519
|
} catch {
|
|
1520
|
+
throw new Error(`Cannot read summary file: ${summaryPath}`);
|
|
953
1521
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
const keptNames = Object.keys(local.keptServers);
|
|
960
|
-
const summary = [
|
|
961
|
-
local.promptSources.length ? `${local.promptSources.join(" + ")} (${kb(local.prompt)})` : "no instructions found",
|
|
962
|
-
`model ${local.model ?? "(harness default)"}`,
|
|
963
|
-
`${keptNames.length} MCP server${keptNames.length === 1 ? "" : "s"}${keptNames.length ? ` (${keptNames.join(", ")})` : ""}`
|
|
964
|
-
].join(" \xB7 ");
|
|
965
|
-
console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename3(process.cwd())})`);
|
|
966
|
-
console.log(`Will clone: ${summary}`);
|
|
967
|
-
if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
|
|
968
|
-
if (local.droppedServers.length) {
|
|
969
|
-
console.log(
|
|
970
|
-
`Left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
|
|
971
|
-
);
|
|
972
|
-
}
|
|
973
|
-
if (opts.dryRun) {
|
|
974
|
-
console.log("Dry run \u2014 nothing sent.");
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
if (!opts.yes && !await confirm("Launch this as a new cloud agent?")) {
|
|
978
|
-
console.log("Aborted \u2014 nothing sent.");
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
const { servers: mcpServers2, skipped } = mcpServersFromKept(local.keptServers);
|
|
982
|
-
if (skipped.length) {
|
|
983
|
-
console.log(` note: local (stdio) MCP servers have no cloud equivalent and were skipped: ${skipped.join(", ")}`);
|
|
984
|
-
}
|
|
985
|
-
const body = {
|
|
986
|
-
agentType: local.harness,
|
|
987
|
-
...local.model ? { llm: local.model } : {},
|
|
988
|
-
...local.prompt ? { prompt: local.prompt } : {},
|
|
989
|
-
...mcpServers2.length ? { mcpServers: mcpServers2 } : {}
|
|
990
|
-
};
|
|
991
|
-
let sentLlm = local.model ?? null;
|
|
992
|
-
let created;
|
|
993
|
-
try {
|
|
994
|
-
({ agent: created } = await api("POST", "/agents", body));
|
|
995
|
-
} catch (error) {
|
|
996
|
-
if (error instanceof ApiError && error.status === 422 && local.model) {
|
|
997
|
-
const successor = supersededBy(local.model);
|
|
998
|
-
console.log(
|
|
999
|
-
successor ? ` note: ${local.model} is superseded \u2014 using ${successor}` : ` note: ${local.model} isn't available for ${local.harness} \u2014 using the harness default`
|
|
1000
|
-
);
|
|
1001
|
-
sentLlm = successor;
|
|
1002
|
-
({ agent: created } = await api("POST", "/agents", {
|
|
1003
|
-
...body,
|
|
1004
|
-
llm: successor ?? void 0
|
|
1005
|
-
}));
|
|
1006
|
-
} else {
|
|
1007
|
-
throw error;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
console.log("Starting its runtime\u2026");
|
|
1011
|
-
await api("POST", "/sessions", { agent: created.id });
|
|
1012
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1013
|
-
writeLink(local.configRoot, {
|
|
1014
|
-
baseUrl: apiBase(),
|
|
1015
|
-
agentId: created.id,
|
|
1016
|
-
clonedAt: now,
|
|
1017
|
-
lastSyncedAt: now,
|
|
1018
|
-
llm: sentLlm,
|
|
1019
|
-
mcpServers: keptNames
|
|
1020
|
-
});
|
|
1021
|
-
const url = `${apiBase()}/agents/${created.id}`;
|
|
1022
|
-
console.log(`Cloned \u2192 ${created.id} ${url}`);
|
|
1023
|
-
console.log(
|
|
1024
|
-
`Linked this machine (${linkPath(local.configRoot)}). Push prompt changes anytime by running \`sky clone\` again.`
|
|
1025
|
-
);
|
|
1026
|
-
await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
|
|
1027
|
-
if (opts.open) openBrowser(url);
|
|
1028
|
-
}
|
|
1029
|
-
async function syncClone(local, link2, opts) {
|
|
1030
|
-
const since = link2.lastSyncedAt ?? link2.clonedAt;
|
|
1031
|
-
console.log(
|
|
1032
|
-
`Syncing ${link2.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
|
|
1033
|
-
);
|
|
1034
|
-
if (opts.dryRun) {
|
|
1035
|
-
console.log(
|
|
1036
|
-
` would push prompt (${kb(local.prompt)}) to ${link2.agentId}. Dry run \u2014 nothing sent.`
|
|
1037
|
-
);
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
let agent2;
|
|
1041
|
-
try {
|
|
1042
|
-
({ agent: agent2 } = await api(
|
|
1043
|
-
"GET",
|
|
1044
|
-
`/agents/${encodeURIComponent(link2.agentId)}`
|
|
1045
|
-
));
|
|
1046
|
-
} catch (error) {
|
|
1047
|
-
if (error instanceof ApiError && error.status === 404) {
|
|
1048
|
-
throw new Error(
|
|
1049
|
-
`linked agent ${link2.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
|
|
1050
|
-
);
|
|
1051
|
-
}
|
|
1052
|
-
throw error;
|
|
1053
|
-
}
|
|
1054
|
-
if (agent2.agentType !== local.harness) {
|
|
1055
|
-
throw new Error(
|
|
1056
|
-
`linked agent ${link2.agentId} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
const result = await api(
|
|
1060
|
-
"PUT",
|
|
1061
|
-
`/agents/${encodeURIComponent(link2.agentId)}/prompt`,
|
|
1062
|
-
{ prompt: local.prompt }
|
|
1063
|
-
);
|
|
1064
|
-
console.log(
|
|
1065
|
-
result.unchanged ? " prompt unchanged" : ` prompt updated (v${result.version}${result.applied === "live" ? ", hot-reloaded" : ", applies on next boot"})`
|
|
1066
|
-
);
|
|
1067
|
-
const drift = [];
|
|
1068
|
-
const localModel = local.model ?? null;
|
|
1069
|
-
const linkedModel = link2.llm ?? agent2.llm;
|
|
1070
|
-
if (localModel && linkedModel && localModel !== linkedModel) {
|
|
1071
|
-
drift.push(`model changed ${linkedModel} \u2192 ${localModel}`);
|
|
1072
|
-
}
|
|
1073
|
-
const before = new Set(link2.mcpServers ?? []);
|
|
1074
|
-
const nowKept = Object.keys(local.keptServers);
|
|
1075
|
-
const newServers = nowKept.filter((n) => !before.has(n));
|
|
1076
|
-
const goneServers = [...before].filter((n) => !nowKept.includes(n));
|
|
1077
|
-
if (newServers.length) drift.push(`${newServers.length} new MCP server${newServers.length === 1 ? "" : "s"} (${newServers.join(", ")})`);
|
|
1078
|
-
if (goneServers.length) drift.push(`${goneServers.length} removed MCP server${goneServers.length === 1 ? "" : "s"} (${goneServers.join(", ")})`);
|
|
1079
|
-
if (drift.length) {
|
|
1080
|
-
console.log(` skipped (set at launch): ${drift.join("; ")}`);
|
|
1081
|
-
console.log(" \u2192 a running agent can't change these; run `sky clone --new` to launch a fresh agent.");
|
|
1082
|
-
}
|
|
1083
|
-
if (local.droppedServers.length) {
|
|
1084
|
-
console.log(
|
|
1085
|
-
` left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
|
|
1086
|
-
);
|
|
1087
|
-
}
|
|
1088
|
-
writeLink(local.configRoot, { ...link2, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1089
|
-
console.log(`Synced ${link2.agentId}.`);
|
|
1090
|
-
await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
|
|
1091
|
-
if (opts.open) openBrowser(`${apiBase()}/agents/${link2.agentId}`);
|
|
1092
|
-
}
|
|
1093
|
-
async function clone(opts) {
|
|
1094
|
-
const harness2 = detectHarness(opts.harness);
|
|
1095
|
-
const local = readLocal(harness2);
|
|
1096
|
-
const link2 = opts.new ? void 0 : readLink(local.configRoot);
|
|
1097
|
-
if (link2) await syncClone(local, link2, opts);
|
|
1098
|
-
else await createClone(local, opts);
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// src/import-local.ts
|
|
1102
|
-
import { closeSync as closeSync3, existsSync as existsSync6, openSync as openSync3, readdirSync as readdirSync4, readFileSync as readFileSync8, readSync as readSync3, statSync as statSync5 } from "fs";
|
|
1103
|
-
import { homedir as homedir4 } from "os";
|
|
1104
|
-
import { join as join7 } from "path";
|
|
1105
|
-
import { createHash as createHash3 } from "crypto";
|
|
1106
|
-
|
|
1107
|
-
// src/import-files.ts
|
|
1108
|
-
import { isUtf8 } from "buffer";
|
|
1109
|
-
import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
1110
|
-
import { homedir as homedir3 } from "os";
|
|
1111
|
-
import { dirname as dirname4, extname as extname2, isAbsolute, join as join5, relative as relative2, resolve as resolve4, sep as sep2, basename as basename5 } from "path";
|
|
1112
|
-
|
|
1113
|
-
// src/import-workspace.ts
|
|
1114
|
-
import { spawnSync } from "child_process";
|
|
1115
|
-
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
|
|
1116
|
-
import { homedir as homedir2 } from "os";
|
|
1117
|
-
import { basename as basename4, dirname as dirname3, join as join4, relative, sep } from "path";
|
|
1118
|
-
import { parse as parseToml2 } from "smol-toml";
|
|
1119
|
-
var MAX_TREE = 5e3;
|
|
1120
|
-
var SKILL_LINE = /Base directory for this skill: (\/[^\n\r"'\\<>]+)/g;
|
|
1121
|
-
function git(cwd, ...args) {
|
|
1122
|
-
const run2 = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 1e4, maxBuffer: 32 * 1024 * 1024 });
|
|
1123
|
-
return run2.status === 0 ? run2.stdout : null;
|
|
1124
|
-
}
|
|
1125
|
-
var isFile = (path) => {
|
|
1126
|
-
try {
|
|
1127
|
-
return statSync3(path).isFile();
|
|
1128
|
-
} catch {
|
|
1129
|
-
return false;
|
|
1130
|
-
}
|
|
1131
|
-
};
|
|
1132
|
-
var isDir = (path) => {
|
|
1133
|
-
try {
|
|
1134
|
-
return statSync3(path).isDirectory();
|
|
1135
|
-
} catch {
|
|
1136
|
-
return false;
|
|
1137
|
-
}
|
|
1138
|
-
};
|
|
1139
|
-
function sharedInstructionPaths(home = homedir2()) {
|
|
1140
|
-
const rules = join4(home, ".agents", "rules");
|
|
1141
|
-
let ruleFiles = [];
|
|
1142
|
-
try {
|
|
1143
|
-
ruleFiles = readdirSync2(rules).filter((name) => name.endsWith(".md")).sort().map((name) => join4(rules, name));
|
|
1144
|
-
} catch {
|
|
1145
|
-
}
|
|
1146
|
-
return [join4(home, "AGENTS.md"), join4(home, "MEMORY.md"), ...ruleFiles].filter(isFile);
|
|
1147
|
-
}
|
|
1148
|
-
function strings(value, out, depth = 0) {
|
|
1149
|
-
if (depth > 12) return;
|
|
1150
|
-
if (typeof value === "string") out.push(value);
|
|
1151
|
-
else if (Array.isArray(value)) for (const item of value) strings(item, out, depth + 1);
|
|
1152
|
-
else if (value && typeof value === "object") for (const item of Object.values(value)) strings(item, out, depth + 1);
|
|
1153
|
-
}
|
|
1154
|
-
function skillRoots(cwd, home) {
|
|
1155
|
-
const claude = process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude");
|
|
1156
|
-
const codex = process.env.CODEX_HOME ?? join4(home, ".codex");
|
|
1157
|
-
return [
|
|
1158
|
-
...cwd ? [join4(cwd, ".claude", "skills"), join4(cwd, ".agents", "skills")] : [],
|
|
1159
|
-
join4(home, ".agents", "skills"),
|
|
1160
|
-
join4(claude, "skills"),
|
|
1161
|
-
join4(codex, "skills"),
|
|
1162
|
-
join4(home, ".hermes", "skills")
|
|
1163
|
-
];
|
|
1164
|
-
}
|
|
1165
|
-
function pluginSkill(plugin, skill, home) {
|
|
1166
|
-
const walk = (dir, depth, underPlugin) => {
|
|
1167
|
-
if (depth > 7) return null;
|
|
1168
|
-
let entries;
|
|
1169
|
-
try {
|
|
1170
|
-
entries = readdirSync2(dir, { withFileTypes: true });
|
|
1171
|
-
} catch {
|
|
1172
|
-
return null;
|
|
1173
|
-
}
|
|
1174
|
-
for (const entry2 of entries) {
|
|
1175
|
-
if (!entry2.isDirectory() || entry2.name === "node_modules" || entry2.name === ".git") continue;
|
|
1176
|
-
const path = join4(dir, entry2.name);
|
|
1177
|
-
if (entry2.name === skill && basename4(dir) === "skills" && (underPlugin || path.includes(`${sep}${plugin}${sep}`)) && isFile(join4(path, "SKILL.md"))) return realpathSync(path);
|
|
1178
|
-
const found = walk(path, depth + 1, underPlugin);
|
|
1179
|
-
if (found) return found;
|
|
1180
|
-
}
|
|
1181
|
-
return null;
|
|
1182
|
-
};
|
|
1183
|
-
return walk(join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "plugins"), 0, false) ?? walk(join4(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions", "skills-plugin"), 0, true);
|
|
1184
|
-
}
|
|
1185
|
-
function findSkill(name, cwd, home) {
|
|
1186
|
-
const [scope, skill] = name.includes(":") ? name.split(":", 2) : [null, name];
|
|
1187
|
-
if (scope && skill) return pluginSkill(scope, skill, home);
|
|
1188
|
-
for (const root of skillRoots(cwd, home)) {
|
|
1189
|
-
const direct = join4(root, name);
|
|
1190
|
-
if (isFile(join4(direct, "SKILL.md"))) return realpathSync(direct);
|
|
1191
|
-
}
|
|
1192
|
-
return null;
|
|
1193
|
-
}
|
|
1194
|
-
function loadedSkills(records, touched, cwd, home = homedir2()) {
|
|
1195
|
-
const dirs = /* @__PURE__ */ new Map();
|
|
1196
|
-
const missing = /* @__PURE__ */ new Set();
|
|
1197
|
-
const add = (dir, name = basename4(dir)) => {
|
|
1198
|
-
if (!isFile(join4(dir, "SKILL.md"))) return false;
|
|
1199
|
-
const real = realpathSync(dir);
|
|
1200
|
-
if (![...dirs.values()].includes(real)) dirs.set(name, real);
|
|
1201
|
-
return true;
|
|
1202
|
-
};
|
|
1203
|
-
const called = [];
|
|
1204
|
-
for (const record of records) {
|
|
1205
|
-
const texts = [];
|
|
1206
|
-
strings(record, texts);
|
|
1207
|
-
for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].trim().replace(/[.,;:)]+$/, ""));
|
|
1208
|
-
const content = record?.message?.content;
|
|
1209
|
-
if (!Array.isArray(content)) continue;
|
|
1210
|
-
for (const block of content) {
|
|
1211
|
-
const b = block;
|
|
1212
|
-
if (b?.type === "tool_use" && b.name === "Skill" && typeof b.input?.skill === "string") called.push(b.input.skill.replace(/^\//, ""));
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
for (const path of touched) if (basename4(path) === "SKILL.md") add(dirname3(path));
|
|
1216
|
-
for (const name of called) {
|
|
1217
|
-
if (!name || dirs.has(name) || [...dirs.keys()].some((key) => key.endsWith(`:${name}`) || name.endsWith(`:${key}`))) continue;
|
|
1218
|
-
const dir = findSkill(name, cwd, home);
|
|
1219
|
-
if (!dir || !add(dir, name)) missing.add(name);
|
|
1220
|
-
}
|
|
1221
|
-
return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
|
|
1222
|
-
}
|
|
1223
|
-
function readJson2(path) {
|
|
1224
|
-
try {
|
|
1225
|
-
const value = JSON.parse(readFileSync5(path, "utf8"));
|
|
1226
|
-
return value && typeof value === "object" ? value : null;
|
|
1227
|
-
} catch {
|
|
1228
|
-
return null;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
function names(value) {
|
|
1232
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1233
|
-
const keys2 = Object.keys(value).sort();
|
|
1234
|
-
return keys2.length ? keys2 : void 0;
|
|
1235
|
-
}
|
|
1236
|
-
function safeUrl(raw) {
|
|
1237
|
-
try {
|
|
1238
|
-
const url = new URL(raw);
|
|
1239
|
-
if (!/^https?:$/.test(url.protocol)) return null;
|
|
1240
|
-
const query = [.../* @__PURE__ */ new Set([...url.searchParams.keys()])].sort();
|
|
1241
|
-
return { url: `${url.protocol}//${url.host}${url.pathname}`, ...query.length ? { query } : {} };
|
|
1242
|
-
} catch {
|
|
1243
|
-
return null;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
function serverEntry(name, def, scope) {
|
|
1247
|
-
if (!def || typeof def !== "object") return null;
|
|
1248
|
-
const env = names(def.env);
|
|
1249
|
-
const headers = names(def.headers ?? def.http_headers);
|
|
1250
|
-
if (typeof def.url === "string") {
|
|
1251
|
-
const url = safeUrl(def.url);
|
|
1252
|
-
if (!url) return null;
|
|
1253
|
-
const type = def.type === "sse" ? "sse" : "http";
|
|
1254
|
-
return { name, scope, transport: type, ...url, ...headers ? { headers } : {}, ...env ? { env } : {} };
|
|
1255
|
-
}
|
|
1256
|
-
if (typeof def.command !== "string" || !def.command) return null;
|
|
1257
|
-
const args = Array.isArray(def.args) ? def.args.filter((arg) => typeof arg === "string") : [];
|
|
1258
|
-
return { name, scope, transport: "stdio", command: def.command, ...args.length ? { args } : {}, ...env ? { env } : {} };
|
|
1259
|
-
}
|
|
1260
|
-
function mcpServers(harness2, cwd, home = homedir2()) {
|
|
1261
|
-
const out = [];
|
|
1262
|
-
const push = (servers, scope) => {
|
|
1263
|
-
if (!servers || typeof servers !== "object") return;
|
|
1264
|
-
for (const [name, def] of Object.entries(servers)) {
|
|
1265
|
-
const entry2 = serverEntry(name, def, scope);
|
|
1266
|
-
if (entry2 && !out.some((existing) => existing.name === name)) out.push(entry2);
|
|
1267
|
-
}
|
|
1268
|
-
};
|
|
1269
|
-
if (harness2 === "claude_code") {
|
|
1270
|
-
const config = readJson2(join4(process.env.CLAUDE_CONFIG_DIR ? dirname3(process.env.CLAUDE_CONFIG_DIR) : home, ".claude.json"));
|
|
1271
|
-
const projects = config?.projects && typeof config.projects === "object" ? config.projects : {};
|
|
1272
|
-
const root = cwd ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || cwd : "";
|
|
1273
|
-
if (cwd) push(projects[cwd]?.mcpServers ?? projects[root]?.mcpServers, "local");
|
|
1274
|
-
if (root) push(readJson2(join4(root, ".mcp.json"))?.mcpServers, "project");
|
|
1275
|
-
push(config?.mcpServers, "user");
|
|
1276
|
-
}
|
|
1277
|
-
if (harness2 === "codex") {
|
|
1278
|
-
try {
|
|
1279
|
-
const config = parseToml2(readFileSync5(join4(process.env.CODEX_HOME ?? join4(home, ".codex"), "config.toml"), "utf8"));
|
|
1280
|
-
push(config.mcp_servers, "user");
|
|
1281
|
-
} catch {
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
return out;
|
|
1285
|
-
}
|
|
1286
|
-
function remoteWithoutCredentials(remote) {
|
|
1287
|
-
return remote.trim().replace(/\/\/[^@/]+@/, "//").replace(/^[^@/]+@([^:]+):/, "$1:");
|
|
1288
|
-
}
|
|
1289
|
-
function workspaceState(cwd, harness2, skills2, home = homedir2(), title = null) {
|
|
1290
|
-
const present = !!cwd && isDir(cwd);
|
|
1291
|
-
const root = present ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || null : null;
|
|
1292
|
-
let gitState = null;
|
|
1293
|
-
let tree = [];
|
|
1294
|
-
let patch = null;
|
|
1295
|
-
if (root) {
|
|
1296
|
-
const dirty = (git(root, "status", "--porcelain=v1", "--untracked-files=normal") ?? "").split("\n").filter(Boolean).map((line) => line.slice(3));
|
|
1297
|
-
const ahead = git(root, "rev-list", "--count", "@{upstream}..HEAD");
|
|
1298
|
-
const remote = git(root, "remote", "get-url", "origin");
|
|
1299
|
-
const branch = git(root, "rev-parse", "--abbrev-ref", "HEAD")?.trim() || null;
|
|
1300
|
-
gitState = {
|
|
1301
|
-
remote: remote ? remoteWithoutCredentials(remote) : null,
|
|
1302
|
-
branch: branch === "HEAD" ? null : branch,
|
|
1303
|
-
commit: git(root, "rev-parse", "HEAD")?.trim() || null,
|
|
1304
|
-
dirty: dirty.slice(0, 500),
|
|
1305
|
-
ahead: ahead === null ? null : Number(ahead.trim()) || 0
|
|
1306
|
-
};
|
|
1307
|
-
tree = (git(cwd, "-c", "core.quotePath=false", "ls-files") ?? "").split("\n").filter(Boolean);
|
|
1308
|
-
if (dirty.length) patch = git(cwd, "diff", "HEAD", "--no-color", "--no-ext-diff", "--relative", "--", ".") || null;
|
|
1522
|
+
let pkg;
|
|
1523
|
+
try {
|
|
1524
|
+
pkg = JSON.parse(raw);
|
|
1525
|
+
} catch {
|
|
1526
|
+
throw new Error("Summary file is not valid JSON.");
|
|
1309
1527
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
cwd: cwd ? cwd.replace(home, "~") : null,
|
|
1317
|
-
project: cwd ? basename4(root ?? cwd) : null,
|
|
1318
|
-
git: gitState,
|
|
1319
|
-
tree: tree.slice(0, MAX_TREE),
|
|
1320
|
-
tree_truncated: tree.length > MAX_TREE,
|
|
1321
|
-
skills: [...skills2.found.map((skill) => ({ name: skill.name, found: true })), ...skills2.missing.map((name) => ({ name, found: false }))],
|
|
1322
|
-
mcp_servers: mcpServers(harness2, cwd, home)
|
|
1323
|
-
},
|
|
1324
|
-
patch
|
|
1325
|
-
};
|
|
1528
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) throw new Error("Summary file must be a JSON object.");
|
|
1529
|
+
const p = pkg;
|
|
1530
|
+
if (p.format !== "asteroids.session-summary") throw new Error('Summary file must have format "asteroids.session-summary".');
|
|
1531
|
+
if (p.format_version !== 1) throw new Error("Summary file must have format_version 1.");
|
|
1532
|
+
const redacted2 = redactLine2(raw.trimEnd());
|
|
1533
|
+
return redacted2;
|
|
1326
1534
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
for (const entry2 of entries) {
|
|
1338
|
-
if (entry2.name === "node_modules" || entry2.name === ".git" || entry2.name === "__pycache__" || entry2.name === ".venv") continue;
|
|
1339
|
-
const path = join4(dir, entry2.name);
|
|
1340
|
-
if (entry2.isDirectory()) walk(path, depth + 1);
|
|
1341
|
-
else if (entry2.isFile()) out.push({ path, name: `skills/${skill.name}/${relative(skill.dir, path).split(sep).join("/")}` });
|
|
1342
|
-
}
|
|
1343
|
-
};
|
|
1344
|
-
if (existsSync4(skill.dir)) walk(skill.dir, 0);
|
|
1345
|
-
return out;
|
|
1535
|
+
var MAX_IMPORT_BYTES = 30 * 1024 * 1024;
|
|
1536
|
+
var SUMMARY_RESERVE_BYTES = 128 * 1024;
|
|
1537
|
+
var MAX_LISTED_ARTIFACTS = 99;
|
|
1538
|
+
function leftOutLine(left) {
|
|
1539
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1540
|
+
for (const file of left) counts.set(file.reason, (counts.get(file.reason) ?? 0) + 1);
|
|
1541
|
+
const files2 = [...counts].map(([reason, n]) => `${n} ${n === 1 ? "file" : "files"} (${reason})`).join(", ");
|
|
1542
|
+
return `Not transferred: ${files2 ? `${files2}; ` : ""}runtime state such as running processes, installed tools and environment variables; and reasoning the source harness stored unreadably.`;
|
|
1346
1543
|
}
|
|
1347
|
-
function
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1544
|
+
function prepareImport(session2, summaryPath, options = {}) {
|
|
1545
|
+
if (session2.discoveryError) throw new Error(session2.discoveryError);
|
|
1546
|
+
const maximum = 16 * 1024 * 1024;
|
|
1547
|
+
let inputBytes = 0;
|
|
1548
|
+
let outputBytes = 0;
|
|
1549
|
+
const lines = [];
|
|
1550
|
+
const digest = createHash3("sha256");
|
|
1551
|
+
let nativeId;
|
|
1552
|
+
let redactionCount = 0;
|
|
1553
|
+
let encryptedReasoningCount = 0;
|
|
1554
|
+
let compactionCount = 0;
|
|
1555
|
+
let unreadableCount = 0;
|
|
1556
|
+
const source = new SessionSourceCollector();
|
|
1557
|
+
const records = [];
|
|
1558
|
+
for (const path of session2.files) {
|
|
1559
|
+
const stat = statSync5(path);
|
|
1560
|
+
if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
|
|
1561
|
+
const text = readFileSync6(path, "utf8");
|
|
1562
|
+
for (const raw of text.split("\n")) {
|
|
1563
|
+
if (!raw.trim()) continue;
|
|
1564
|
+
let record;
|
|
1352
1565
|
try {
|
|
1353
|
-
|
|
1354
|
-
if (entry2.id === sessionId && typeof entry2.thread_name === "string" && entry2.thread_name.trim()) name = entry2.thread_name.trim();
|
|
1566
|
+
record = JSON.parse(raw);
|
|
1355
1567
|
} catch {
|
|
1568
|
+
unreadableCount++;
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("Transcript records must be JSON objects.");
|
|
1572
|
+
const rec = record;
|
|
1573
|
+
if (String(rec.type).startsWith("import.")) throw new Error("Native transcripts cannot supply import protocol records.");
|
|
1574
|
+
if (path === session2.files[0]) source.push(rec);
|
|
1575
|
+
records.push(rec);
|
|
1576
|
+
if (rec.type === "response_item") {
|
|
1577
|
+
const payload = rec.payload;
|
|
1578
|
+
if (payload?.type === "reasoning" && typeof payload?.encrypted_content === "string" && payload.encrypted_content) {
|
|
1579
|
+
encryptedReasoningCount++;
|
|
1580
|
+
}
|
|
1356
1581
|
}
|
|
1582
|
+
if (rec.type === "compacted") {
|
|
1583
|
+
compactionCount++;
|
|
1584
|
+
}
|
|
1585
|
+
const id = rec.type === "session_meta" ? rec.payload?.id : rec.sessionId;
|
|
1586
|
+
if (!nativeId && typeof id === "string") nativeId = id;
|
|
1587
|
+
const result = redactLine(raw.replace(/\r$/, ""), { requireStructured: true });
|
|
1588
|
+
if (Object.keys(result.hits).length > 0) redactionCount++;
|
|
1589
|
+
const line = result.line + "\n";
|
|
1590
|
+
outputBytes += Buffer.byteLength(line);
|
|
1591
|
+
if (outputBytes > maximum) throw new Error("Redacted transcript exceeds the 16 MiB limit.");
|
|
1592
|
+
digest.update(line);
|
|
1593
|
+
lines.push(line);
|
|
1357
1594
|
}
|
|
1358
|
-
} catch {
|
|
1359
1595
|
}
|
|
1360
|
-
|
|
1596
|
+
if (!lines.length) throw new Error("The selected transcript is empty.");
|
|
1597
|
+
const sourceId = nativeId ?? session2.sessionId;
|
|
1598
|
+
if (!/^[A-Za-z0-9_-]{1,200}$/.test(sourceId) || redactLine2(JSON.stringify(sourceId)) !== JSON.stringify(sourceId)) throw new Error("The source conversation has an invalid identifier.");
|
|
1599
|
+
const meta = source.result();
|
|
1600
|
+
const files2 = options.files === false ? { lines: [], bytes: 0, attached: [], leftOut: [], setup: { skills: [], missingSkills: [], mcpServers: [], git: false } } : collectSessionFiles(records, {
|
|
1601
|
+
harness: session2.harness,
|
|
1602
|
+
cwd: meta.cwd ?? (session2.harness === "claude_code" ? session2.workspace : ""),
|
|
1603
|
+
budget: MAX_IMPORT_BYTES - outputBytes - SUMMARY_RESERVE_BYTES,
|
|
1604
|
+
exclude: session2.files,
|
|
1605
|
+
title: meta.title ?? (session2.harness === "codex" ? codexThreadName(session2.sessionId) : null)
|
|
1606
|
+
});
|
|
1607
|
+
const attachedNote = files2.attached.length ? ` The ${files2.attached.length} local ${files2.attached.length === 1 ? "file" : "files"} it read, wrote or ran under, the skills it loaded and its workspace description (repository state and tool servers, agentsky/workspace.json) are attached as well (listed in artifacts).` : "";
|
|
1608
|
+
let summaryJson;
|
|
1609
|
+
if (summaryPath) {
|
|
1610
|
+
summaryJson = loadSummaryFile(summaryPath);
|
|
1611
|
+
} else {
|
|
1612
|
+
const summary = {
|
|
1613
|
+
format: "asteroids.session-summary",
|
|
1614
|
+
format_version: 1,
|
|
1615
|
+
source_harness: session2.harness,
|
|
1616
|
+
source_harness_version: meta.harness_version?.slice(0, 100) ?? null,
|
|
1617
|
+
source_session_id: sourceId,
|
|
1618
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1619
|
+
summary: `The complete selected local conversation is attached as a transcript.${attachedNote} Review it before continuing; this automatic handoff does not infer the user's unfinished objective or claim any work is complete.`,
|
|
1620
|
+
context: {
|
|
1621
|
+
objective: "Review the imported conversation and continue its latest unfinished authorized objective.",
|
|
1622
|
+
constraints: [],
|
|
1623
|
+
decisions: [],
|
|
1624
|
+
completed: [],
|
|
1625
|
+
pending: ["Read the complete imported transcript, reconcile the latest human instructions and completed work, then continue."],
|
|
1626
|
+
verification: [],
|
|
1627
|
+
blockers: [leftOutLine(files2.leftOut), ...files2.setup.missingSkills.length ? [`Skills it loaded that are not on this computer any more: ${files2.setup.missingSkills.slice(0, 20).join(", ")}.`] : []],
|
|
1628
|
+
artifacts: files2.attached.slice(0, MAX_LISTED_ARTIFACTS).map((file) => ({ path: file.name, description: `Attached from the local ${file.kind === "text" ? "workspace" : file.kind} the conversation used (${file.bytes} bytes).` })),
|
|
1629
|
+
provenance: { native_checkpoint_sha256: digest.digest("hex"), selected_native_session_id: sourceId, context_bytes: outputBytes }
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
summaryJson = JSON.stringify(summary);
|
|
1633
|
+
}
|
|
1634
|
+
const body = lines.join("") + files2.lines.join("") + JSON.stringify({ type: "import.summary", package: JSON.parse(summaryJson) }) + "\n" + JSON.stringify({ type: "import.end", records: lines.length + files2.lines.length + 1 }) + "\n";
|
|
1635
|
+
const gaps = [];
|
|
1636
|
+
if (encryptedReasoningCount > 0) gaps.push({ kind: "encrypted_reasoning", count: encryptedReasoningCount });
|
|
1637
|
+
if (compactionCount > 0) gaps.push({ kind: "compaction", count: compactionCount });
|
|
1638
|
+
if (unreadableCount > 0) gaps.push({ kind: "unreadable", count: unreadableCount });
|
|
1639
|
+
const cwd = meta.cwd ?? (session2.harness === "claude_code" ? session2.workspace : "");
|
|
1640
|
+
return { body, transcriptLines: lines.length, outputBytes, redactionCount, gaps, attachedFiles: files2.attached, leftOutFiles: files2.leftOut, setup: files2.setup, cwd, resourceLines: files2.lines };
|
|
1361
1641
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
const ext = extname2(path).toLowerCase();
|
|
1409
|
-
if (DOCUMENTS[ext]) return { kind: "document", mediaType: DOCUMENTS[ext] };
|
|
1410
|
-
if (IMAGES[ext]) return { kind: "image", mediaType: IMAGES[ext] };
|
|
1411
|
-
if (TEXT[ext]) return { kind: "text", mediaType: TEXT[ext] };
|
|
1412
|
-
if (PLAIN_TEXT.has(ext) || PLAIN_NAMES.has(basename5(path))) return { kind: "text", mediaType: "text/plain" };
|
|
1413
|
-
return null;
|
|
1642
|
+
async function importSession(opts) {
|
|
1643
|
+
const base = (opts.agentUseUrl ?? agentUseBase()).replace(/\/$/, "");
|
|
1644
|
+
const destination2 = new URL(base);
|
|
1645
|
+
if (destination2.username || destination2.password || destination2.protocol !== "https:" && !(destination2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(destination2.hostname))) {
|
|
1646
|
+
throw new Error("Import requires HTTPS, or a loopback development server.");
|
|
1647
|
+
}
|
|
1648
|
+
opts.onProgress?.("Reading transcript and redacting secrets locally\u2026");
|
|
1649
|
+
const { body, transcriptLines, outputBytes, redactionCount, gaps, attachedFiles, leftOutFiles, setup, cwd, resourceLines } = prepareImport(opts.session, opts.summaryPath, { files: opts.files });
|
|
1650
|
+
let traceId;
|
|
1651
|
+
let key;
|
|
1652
|
+
if (opts.importKey && opts.grantTraceId) {
|
|
1653
|
+
traceId = opts.grantTraceId;
|
|
1654
|
+
key = opts.importKey;
|
|
1655
|
+
} else {
|
|
1656
|
+
opts.onProgress?.("Minting import key\u2026");
|
|
1657
|
+
({ traceId, key } = await mintTrace(base, opts.session.harness));
|
|
1658
|
+
}
|
|
1659
|
+
opts.onProgress?.("Uploading the complete transcript\u2026");
|
|
1660
|
+
const response = await fetch(`${base}/api/traces/ingest`, {
|
|
1661
|
+
method: "POST",
|
|
1662
|
+
redirect: "error",
|
|
1663
|
+
signal: AbortSignal.timeout(12e4),
|
|
1664
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/x-ndjson", "x-source-harness": opts.session.harness, "user-agent": USER_AGENT },
|
|
1665
|
+
body
|
|
1666
|
+
});
|
|
1667
|
+
if (!response.ok) {
|
|
1668
|
+
const result2 = await response.json().catch(() => null);
|
|
1669
|
+
throw new Error(typeof result2?.error === "string" ? result2.error : result2?.error?.message ?? `Upload failed (${response.status}).`);
|
|
1670
|
+
}
|
|
1671
|
+
const result = await response.json();
|
|
1672
|
+
const finalTraceId = result.traceId ?? traceId;
|
|
1673
|
+
const environment = opts.files === false || opts.environment === false || !cwd || !existsSync4(cwd) ? null : await uploadEnvironment(base, key, finalTraceId, cwd, resourceLines, opts.onProgress).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
|
|
1674
|
+
return {
|
|
1675
|
+
traceId: finalTraceId,
|
|
1676
|
+
url: `${base}/playground/new?import=${encodeURIComponent(finalTraceId)}`,
|
|
1677
|
+
harness: opts.session.harness,
|
|
1678
|
+
sessionId: opts.session.sessionId,
|
|
1679
|
+
transcriptFiles: opts.session.files.length,
|
|
1680
|
+
bytes: outputBytes,
|
|
1681
|
+
redactionCount,
|
|
1682
|
+
gaps,
|
|
1683
|
+
attachedFiles,
|
|
1684
|
+
leftOutFiles,
|
|
1685
|
+
setup,
|
|
1686
|
+
environment
|
|
1687
|
+
};
|
|
1414
1688
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1689
|
+
async function grantCall(base, key, path, body) {
|
|
1690
|
+
const response = await fetch(`${base}${path}`, {
|
|
1691
|
+
method: "POST",
|
|
1692
|
+
redirect: "error",
|
|
1693
|
+
signal: AbortSignal.timeout(3e5),
|
|
1694
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json", "user-agent": USER_AGENT },
|
|
1695
|
+
body: JSON.stringify(body)
|
|
1696
|
+
});
|
|
1697
|
+
const answer = await response.json().catch(() => null);
|
|
1698
|
+
if (!response.ok) throw new Error(typeof answer?.error === "string" ? answer.error : answer?.error?.message ?? `The environment upload failed (${response.status}).`);
|
|
1699
|
+
return answer;
|
|
1425
1700
|
}
|
|
1426
|
-
function
|
|
1427
|
-
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
const path =
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
const patch = typeof input === "string" ? input : str(i?.input) || str(i?.patch);
|
|
1439
|
-
for (const match of patch.matchAll(/^\*\*\* (?:Add|Update) File: (.+)$/gm)) add(match[1].trim(), base);
|
|
1440
|
-
if (!/bash|shell|exec|terminal|command|run/i.test(name)) return;
|
|
1441
|
-
for (const match of shellText(input).replace(/\\n/g, "\n").matchAll(SHELL_READ)) add(match[1], base);
|
|
1442
|
-
};
|
|
1443
|
-
for (const raw of records) {
|
|
1444
|
-
const record = obj(raw);
|
|
1445
|
-
if (!record) continue;
|
|
1446
|
-
const content = obj(record.message)?.content;
|
|
1447
|
-
if (record.type === "assistant" && Array.isArray(content)) {
|
|
1448
|
-
for (const block of content) {
|
|
1449
|
-
const b = obj(block);
|
|
1450
|
-
if (b?.type === "tool_use") call(str(b.name), b.input);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
const payload = obj(record.payload);
|
|
1454
|
-
if (record.type === "response_item" && (payload?.type === "function_call" || payload?.type === "custom_tool_call")) {
|
|
1455
|
-
let input = payload.arguments ?? payload.input ?? "";
|
|
1456
|
-
if (typeof input === "string") {
|
|
1457
|
-
try {
|
|
1458
|
-
input = JSON.parse(input);
|
|
1459
|
-
} catch {
|
|
1460
|
-
input = payload.arguments ?? payload.input;
|
|
1461
|
-
}
|
|
1701
|
+
async function uploadEnvironment(base, key, traceId, cwd, resourceLines, onProgress) {
|
|
1702
|
+
onProgress?.("Packing the working folder (secrets redacted locally)\u2026");
|
|
1703
|
+
const packed = await packEnvironment(cwd, resourcesOf(resourceLines));
|
|
1704
|
+
try {
|
|
1705
|
+
const path = `/api/traces/${encodeURIComponent(traceId)}/environment`;
|
|
1706
|
+
const { upload } = await grantCall(base, key, path, { name: packed.folder, size: packed.bytes, sha256: packed.sha256, files: packed.files });
|
|
1707
|
+
let shown = -1;
|
|
1708
|
+
await transfer(upload, packed.path, (sent, total) => {
|
|
1709
|
+
const percent = Math.floor(sent / total * 100);
|
|
1710
|
+
if (percent >= shown + 10 || sent === total) {
|
|
1711
|
+
shown = percent;
|
|
1712
|
+
onProgress?.(`Uploading the working folder\u2026 ${percent}% of ${(total / 1024 / 1024).toFixed(1)} MiB`);
|
|
1462
1713
|
}
|
|
1463
|
-
|
|
1464
|
-
}
|
|
1714
|
+
});
|
|
1715
|
+
await grantCall(base, key, `${path}/complete`, {});
|
|
1716
|
+
return { folder: packed.folder, files: packed.files, bytes: packed.bytes, redacted: packed.redacted, leftOut: packed.leftOut.length };
|
|
1717
|
+
} finally {
|
|
1718
|
+
packed.dispose();
|
|
1465
1719
|
}
|
|
1466
|
-
return [...out];
|
|
1467
1720
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1721
|
+
|
|
1722
|
+
// src/context.ts
|
|
1723
|
+
async function contextRequest(path, universe2) {
|
|
1724
|
+
try {
|
|
1725
|
+
return await api("GET", `/api${path}`, void 0, { base: agentUseBase(), universe: universe2 });
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
if (!(error instanceof ApiError) || ![401, 403, 409].includes(error.status)) throw error;
|
|
1728
|
+
return api("GET", path, void 0, { universe: universe2 });
|
|
1472
1729
|
}
|
|
1473
1730
|
}
|
|
1474
|
-
function
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
const root = gitRoot(cwd) ?? cwd;
|
|
1478
|
-
for (let d = resolve4(cwd); ; d = dirname4(d)) {
|
|
1479
|
-
for (const name of INSTRUCTION_FILES) out.push(join5(d, name));
|
|
1480
|
-
if (d === root || d === dirname4(d)) break;
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
if (harness2 === "claude_code") out.push(join5(process.env.CLAUDE_CONFIG_DIR ?? join5(home, ".claude"), "CLAUDE.md"));
|
|
1484
|
-
if (harness2 === "codex") out.push(join5(process.env.CODEX_HOME ?? join5(home, ".codex"), "AGENTS.md"));
|
|
1485
|
-
out.push(...sharedInstructionPaths(home));
|
|
1486
|
-
return out.filter((path) => {
|
|
1487
|
-
try {
|
|
1488
|
-
return statSync4(path).isFile();
|
|
1489
|
-
} catch {
|
|
1490
|
-
return false;
|
|
1491
|
-
}
|
|
1492
|
-
});
|
|
1731
|
+
async function searchContext(query, opts, universe2) {
|
|
1732
|
+
const page = await contextRequest(`/context/search${searchQuery(query, opts)}`, universe2);
|
|
1733
|
+
return { ...page, hits: page.hits.map((entry2) => "hit" in entry2 ? entry2.hit : entry2) };
|
|
1493
1734
|
}
|
|
1494
|
-
function
|
|
1495
|
-
|
|
1496
|
-
if (cwd && inside && !inside.startsWith("..") && !isAbsolute(inside)) return `workspace/${inside.split(sep2).join("/")}`;
|
|
1497
|
-
const underHome = relative2(home, path);
|
|
1498
|
-
const shown = !underHome.startsWith("..") && !isAbsolute(underHome) ? `~/${underHome}` : path.replace(/^\/+/, "");
|
|
1499
|
-
return `files/${shown.split(sep2).join("/")}`;
|
|
1735
|
+
function memoryBody(operation, version) {
|
|
1736
|
+
return { ...operation, expectedVersion: version };
|
|
1500
1737
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
return
|
|
1738
|
+
function memoryIndex(value) {
|
|
1739
|
+
if (!/^\d+$/.test(value.trim())) throw new Error(`the entry number must be a whole number, got ${JSON.stringify(value)}`);
|
|
1740
|
+
return Number(value.trim());
|
|
1504
1741
|
}
|
|
1505
|
-
function
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
const stat = statSync4(path);
|
|
1523
|
-
if (!stat.isFile()) return;
|
|
1524
|
-
size = stat.size;
|
|
1525
|
-
} catch {
|
|
1526
|
-
leftOut.push({ path, reason: "missing" });
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
const type = classify(path);
|
|
1530
|
-
if (!type) {
|
|
1531
|
-
leftOut.push({ path, reason: "unsupported type" });
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
if (size > MAX_ATTACHED_FILE_BYTES) {
|
|
1535
|
-
leftOut.push({ path, reason: "too large" });
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
candidates.push({ path, name, ...type, size });
|
|
1539
|
-
};
|
|
1540
|
-
for (const path of [...instructionPaths(opts.cwd, opts.harness, home), ...touched]) consider(path, resourceName(path, opts.cwd, home));
|
|
1541
|
-
for (const skill of skills2.found) for (const file of skillFiles(skill)) consider(file.path, file.name);
|
|
1542
|
-
candidates.sort((a, b) => ORDER[a.kind] - ORDER[b.kind] || a.size - b.size);
|
|
1742
|
+
function renderMemory(document) {
|
|
1743
|
+
if (!document.entries.length) return "no saved memory \u2014 `sky memory save <text>` adds an entry";
|
|
1744
|
+
return document.entries.map((entry2) => `${String(entry2.index).padStart(3)} ${entry2.text}`).join("\n");
|
|
1745
|
+
}
|
|
1746
|
+
function searchQuery(query, opts) {
|
|
1747
|
+
const params = new URLSearchParams({ q: query });
|
|
1748
|
+
params.set("scope", opts.scope ?? "everything");
|
|
1749
|
+
if (opts.limit) params.set("limit", opts.limit);
|
|
1750
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
1751
|
+
if (opts.since) params.set("since", opts.since);
|
|
1752
|
+
if (opts.until) params.set("until", opts.until);
|
|
1753
|
+
return `?${params.toString()}`;
|
|
1754
|
+
}
|
|
1755
|
+
function refArgument(ref) {
|
|
1756
|
+
return `'${JSON.stringify(ref).replace(/'/g, "'\\''")}'`;
|
|
1757
|
+
}
|
|
1758
|
+
function renderContextPage(page) {
|
|
1543
1759
|
const lines = [];
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
const described = !!opts.cwd || !!state.manifest.title || state.manifest.skills.length > 0 || state.manifest.mcp_servers.length > 0;
|
|
1548
|
-
const generated = [
|
|
1549
|
-
...described ? [{ name: "agentsky/workspace.json", mediaType: "application/json", data: Buffer.from(JSON.stringify(state.manifest, null, 2)) }] : [],
|
|
1550
|
-
...state.patch ? [{ name: "agentsky/uncommitted.patch", mediaType: "text/x-diff", data: Buffer.from(state.patch) }] : []
|
|
1551
|
-
];
|
|
1552
|
-
for (const file of generated) {
|
|
1553
|
-
let line;
|
|
1554
|
-
try {
|
|
1555
|
-
line = resourceLine(file.name, file.mediaType, file.data);
|
|
1556
|
-
} catch {
|
|
1557
|
-
leftOut.push({ path: file.name, reason: "could not be redacted safely" });
|
|
1558
|
-
continue;
|
|
1559
|
-
}
|
|
1560
|
-
const size = Buffer.byteLength(line);
|
|
1561
|
-
if (bytes + size > opts.budget) {
|
|
1562
|
-
leftOut.push({ path: file.name, reason: "over the upload budget" });
|
|
1563
|
-
continue;
|
|
1564
|
-
}
|
|
1565
|
-
bytes += size;
|
|
1566
|
-
lines.push(line);
|
|
1567
|
-
attached.push({ name: file.name, kind: "text", bytes: file.data.length });
|
|
1760
|
+
for (const hit of page.hits) {
|
|
1761
|
+
lines.push(`${hit.path ?? hit.title}${hit.line ? `:${hit.line}` : ""}: ${hit.snippet.replace(/\s+/g, " ").slice(0, 160)}`);
|
|
1762
|
+
lines.push(` sky context read ${refArgument(hit.ref)}`);
|
|
1568
1763
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1764
|
+
if (!page.hits.length) lines.push("no matches on this page");
|
|
1765
|
+
const missed = (page.sources ?? []).filter((source) => source.status === "failed" || source.status === "partial").map((source) => source.source);
|
|
1766
|
+
if (page.partial) lines.push(`partial: ${missed.length ? `${missed.join(", ")} not fully searched` : "not every source was searched"}`);
|
|
1767
|
+
if (page.nextCursor) lines.push(`more: --cursor ${page.nextCursor}`);
|
|
1768
|
+
return lines.join("\n");
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// src/agent-file.ts
|
|
1772
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
1773
|
+
import { dirname as dirname4, resolve as resolve3 } from "path";
|
|
1774
|
+
import { parse as parseToml2 } from "smol-toml";
|
|
1775
|
+
var fileInstructionSchema = external_exports.object({
|
|
1776
|
+
name: external_exports.string(),
|
|
1777
|
+
content: external_exports.string().optional(),
|
|
1778
|
+
file: external_exports.string().optional()
|
|
1779
|
+
});
|
|
1780
|
+
var agentFileSchema = external_exports.object({
|
|
1781
|
+
name: external_exports.string().min(1),
|
|
1782
|
+
id: external_exports.string().optional(),
|
|
1783
|
+
// stamped by launch/deploy; identifies the live agent
|
|
1784
|
+
slug: external_exports.string().optional(),
|
|
1785
|
+
// pre-RFC-0079 stamp; ignored (agents are id-addressed)
|
|
1786
|
+
description: external_exports.string().optional(),
|
|
1787
|
+
type: external_exports.enum(AGENT_TYPES).optional(),
|
|
1788
|
+
llm: external_exports.string().optional(),
|
|
1789
|
+
reasoning_effort: external_exports.enum(REASONING_EFFORT_VALUES).optional(),
|
|
1790
|
+
capabilities: capabilitiesSchema.removeDefault().optional(),
|
|
1791
|
+
// omitted = leave the agent's as they are
|
|
1792
|
+
prompt: external_exports.string().optional(),
|
|
1793
|
+
prompt_file: external_exports.string().optional(),
|
|
1794
|
+
instructions: external_exports.array(fileInstructionSchema).optional(),
|
|
1795
|
+
skills: external_exports.array(skillRefSchema).optional(),
|
|
1796
|
+
customData: external_exports.array(customDataSchema).optional(),
|
|
1797
|
+
/** "deny" makes the agent work alone: its sessions may not hire helpers. */
|
|
1798
|
+
hiring_policy: external_exports.enum(["allow", "deny"]).optional(),
|
|
1799
|
+
/** The environment its sessions boot in when they name none. */
|
|
1800
|
+
environment: external_exports.string().min(1).optional(),
|
|
1801
|
+
/** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
|
|
1802
|
+
vcpus: external_exports.number().int().optional(),
|
|
1803
|
+
memory_mb: external_exports.number().int().optional()
|
|
1804
|
+
});
|
|
1805
|
+
function readAgentFile(path) {
|
|
1806
|
+
const raw = readFileSync7(path, "utf8");
|
|
1807
|
+
const parsed = agentFileSchema.safeParse(parseToml2(raw));
|
|
1808
|
+
if (!parsed.success) {
|
|
1809
|
+
const issue = parsed.error.issues[0];
|
|
1810
|
+
throw new Error(`${path}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? "invalid"}`);
|
|
1811
|
+
}
|
|
1812
|
+
const file = parsed.data;
|
|
1813
|
+
if (file.reasoning_effort && file.type && !isModelEffortSupported(file.type, file.llm ?? defaultLlm(file.type), file.reasoning_effort)) {
|
|
1814
|
+
throw new Error(
|
|
1815
|
+
`${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
const base = dirname4(resolve3(path));
|
|
1819
|
+
const inline = (ref, inlineContent) => {
|
|
1820
|
+
if (inlineContent !== void 0) return inlineContent;
|
|
1821
|
+
if (!ref) return void 0;
|
|
1822
|
+
return readFileSync7(resolve3(base, ref), "utf8");
|
|
1823
|
+
};
|
|
1824
|
+
const spec = {
|
|
1825
|
+
name: file.name,
|
|
1826
|
+
description: file.description,
|
|
1827
|
+
agentType: file.type,
|
|
1828
|
+
llm: file.llm,
|
|
1829
|
+
reasoningEffort: file.reasoning_effort,
|
|
1830
|
+
displayName: file.name,
|
|
1831
|
+
capabilities: file.capabilities,
|
|
1832
|
+
prompt: inline(file.prompt_file, file.prompt),
|
|
1833
|
+
instructions: file.instructions?.map((f) => {
|
|
1834
|
+
const content = inline(f.file, f.content);
|
|
1835
|
+
if (content === void 0) throw new Error(`instruction ${f.name}: content or file required`);
|
|
1836
|
+
return { name: f.name, content };
|
|
1837
|
+
}),
|
|
1838
|
+
skills: file.skills,
|
|
1839
|
+
customData: file.customData,
|
|
1840
|
+
hiringPolicy: file.hiring_policy,
|
|
1841
|
+
environmentId: file.environment,
|
|
1842
|
+
vcpus: file.vcpus,
|
|
1843
|
+
memoryMb: file.memory_mb
|
|
1844
|
+
};
|
|
1845
|
+
return { file, spec };
|
|
1846
|
+
}
|
|
1847
|
+
function stampId(path, id) {
|
|
1848
|
+
writeFileSync2(path, stampIdText(readFileSync7(path, "utf8"), id));
|
|
1849
|
+
}
|
|
1850
|
+
function stampIdText(text, id) {
|
|
1851
|
+
const out = [];
|
|
1852
|
+
let topLevel = true;
|
|
1853
|
+
let openString = null;
|
|
1854
|
+
let stamped = false;
|
|
1855
|
+
let nameAt = -1;
|
|
1856
|
+
for (const line of text.split("\n")) {
|
|
1857
|
+
if (openString) {
|
|
1858
|
+
if (line.includes(openString)) openString = null;
|
|
1859
|
+
out.push(line);
|
|
1586
1860
|
continue;
|
|
1587
1861
|
}
|
|
1588
|
-
|
|
1589
|
-
if (
|
|
1590
|
-
|
|
1862
|
+
if (/^\s*\[/.test(line)) topLevel = false;
|
|
1863
|
+
if (topLevel && /^\s*(id|slug)\s*=/.test(line)) {
|
|
1864
|
+
if (/^\s*id\s*=/.test(line) && !stamped) {
|
|
1865
|
+
out.push(`id = ${JSON.stringify(id)}`);
|
|
1866
|
+
stamped = true;
|
|
1867
|
+
}
|
|
1591
1868
|
continue;
|
|
1592
1869
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1870
|
+
if (topLevel && nameAt < 0 && /^\s*name\s*=/.test(line)) nameAt = out.length;
|
|
1871
|
+
const opener = /=\s*("""|''')/.exec(line);
|
|
1872
|
+
if (opener && !line.slice(opener.index + opener[0].length).includes(opener[1])) openString = opener[1];
|
|
1873
|
+
out.push(line);
|
|
1596
1874
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
git: state.manifest.git !== null
|
|
1602
|
-
};
|
|
1603
|
-
return { lines, bytes, attached, leftOut, setup };
|
|
1875
|
+
if (!stamped) out.splice(nameAt + 1, 0, `id = ${JSON.stringify(id)}`);
|
|
1876
|
+
const next = out.join("\n");
|
|
1877
|
+
if (parseToml2(next).id !== id) throw new Error("could not stamp the agent id into the file");
|
|
1878
|
+
return next;
|
|
1604
1879
|
}
|
|
1880
|
+
function scaffoldAgentFile(path, name) {
|
|
1881
|
+
if (existsSync5(path)) throw new Error(`${path} already exists`);
|
|
1882
|
+
writeFileSync2(
|
|
1883
|
+
path,
|
|
1884
|
+
`# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
|
|
1885
|
+
name = "${name}"
|
|
1886
|
+
# type = "pi" # pi (default) | hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode
|
|
1887
|
+
# llm = "deepseek-flash" # must be compatible with type
|
|
1888
|
+
# reasoning_effort = "high" # per harness \u2014 ${reasoningEffortDocLine()}
|
|
1889
|
+
# capabilities = []
|
|
1890
|
+
# hiring_policy = "deny" # its sessions work alone; "allow" (default) lets them hire helpers
|
|
1891
|
+
# environment = "env_..." # the environment its sessions boot in when they name none
|
|
1892
|
+
# [[skills]] # a skill from \`sky skills upload <dir>\`
|
|
1893
|
+
# type = "skill"
|
|
1894
|
+
# skillId = "skill_..."
|
|
1605
1895
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
import { join as join6 } from "path";
|
|
1612
|
-
import { isUtf8 as isUtf82 } from "buffer";
|
|
1613
|
-
import { createGzip } from "zlib";
|
|
1614
|
-
|
|
1615
|
-
// ../../packages/spec/src/import-layout.ts
|
|
1616
|
-
import { gzipSync } from "zlib";
|
|
1617
|
-
function folderName(workspace2, sourceCwd) {
|
|
1618
|
-
const raw = workspace2?.project || (sourceCwd ? sourceCwd.split("/").filter(Boolean).pop() : "") || "";
|
|
1619
|
-
const cleaned = raw.replace(/[^A-Za-z0-9._ -]/g, "-").replace(/^[^A-Za-z0-9_]+/, "").replace(/\.\.+/g, ".").slice(0, 80).trim();
|
|
1620
|
-
return cleaned || "imported-session";
|
|
1896
|
+
prompt = """
|
|
1897
|
+
You are ${name}, a helpful long-lived agent.
|
|
1898
|
+
"""
|
|
1899
|
+
`
|
|
1900
|
+
);
|
|
1621
1901
|
}
|
|
1622
|
-
function
|
|
1623
|
-
return
|
|
1902
|
+
function deployPatch(spec) {
|
|
1903
|
+
return {
|
|
1904
|
+
displayName: spec.displayName,
|
|
1905
|
+
...spec.agentType ? { agentType: spec.agentType } : {},
|
|
1906
|
+
...spec.llm ? { llm: spec.llm } : {},
|
|
1907
|
+
...spec.capabilities ? { capabilities: spec.capabilities } : {},
|
|
1908
|
+
...spec.reasoningEffort ? { reasoningEffort: spec.reasoningEffort } : {},
|
|
1909
|
+
...spec.skills ? { skills: spec.skills } : {},
|
|
1910
|
+
...spec.hiringPolicy ? { hiringPolicy: spec.hiringPolicy } : {},
|
|
1911
|
+
...spec.environmentId ? { environmentId: spec.environmentId } : {}
|
|
1912
|
+
};
|
|
1624
1913
|
}
|
|
1625
|
-
function
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1914
|
+
function deployReport(id, spec, before, after, promptChanged) {
|
|
1915
|
+
if (spec.agentType && after.agentType !== spec.agentType) {
|
|
1916
|
+
throw new Error(`${id} is still ${after.agentType}: the API did not apply type = "${spec.agentType}"`);
|
|
1917
|
+
}
|
|
1918
|
+
if (spec.llm && after.llm !== spec.llm) {
|
|
1919
|
+
throw new Error(`${id} still runs ${after.llm}: the API did not apply llm = "${spec.llm}"`);
|
|
1630
1920
|
}
|
|
1921
|
+
const changed = [];
|
|
1922
|
+
if (promptChanged) changed.push("prompt");
|
|
1923
|
+
if (after.version !== before.version) changed.push("settings");
|
|
1924
|
+
if (!changed.length) return [`${id} is up to date \u2014 nothing to deploy (version ${after.version})`];
|
|
1925
|
+
const lines = [`Deployed ${id}: ${changed.join(" and ")} updated (version ${before.version} \u2192 ${after.version})`];
|
|
1926
|
+
if (after.agentType !== before.agentType || after.llm !== before.llm) {
|
|
1927
|
+
lines.push(` now ${after.agentType} \xB7 ${after.llm} for new sessions (was ${before.agentType} \xB7 ${before.llm})`);
|
|
1928
|
+
}
|
|
1929
|
+
if (spec.skills && after.version !== before.version) {
|
|
1930
|
+
lines.push(` Skills apply to new sessions; running sessions keep the skills they started with \u2014 start one with \`sky session create ${id}\`.`);
|
|
1931
|
+
}
|
|
1932
|
+
return lines;
|
|
1631
1933
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
return [server.name, {
|
|
1643
|
-
type: server.transport,
|
|
1644
|
-
url: `${server.url}${query}`,
|
|
1645
|
-
...server.headers?.length ? { headers: Object.fromEntries(server.headers.map((name) => [name, `\${${envName(server.name, name)}:-}`])) } : {}
|
|
1646
|
-
}];
|
|
1647
|
-
});
|
|
1648
|
-
return JSON.stringify({ mcpServers: Object.fromEntries(entries) }, null, 2) + "\n";
|
|
1934
|
+
|
|
1935
|
+
// src/catalog.ts
|
|
1936
|
+
var bare = (handle) => handle.replace(/^@/, "");
|
|
1937
|
+
var optionalAuth = () => ({ auth: Boolean(apiToken()) });
|
|
1938
|
+
function catalogQuery(filter) {
|
|
1939
|
+
const params = new URLSearchParams();
|
|
1940
|
+
if (filter.text?.trim()) params.set("q", filter.text.trim());
|
|
1941
|
+
if (filter.category) params.set("category", filter.category);
|
|
1942
|
+
const query = params.toString();
|
|
1943
|
+
return query ? `?${query}` : "";
|
|
1649
1944
|
}
|
|
1650
|
-
function
|
|
1651
|
-
return
|
|
1945
|
+
function listingBody(agentId, options) {
|
|
1946
|
+
return {
|
|
1947
|
+
agent_id: agentId,
|
|
1948
|
+
handle: bare(options.handle),
|
|
1949
|
+
tagline: options.tagline,
|
|
1950
|
+
...options.description ? { description: options.description } : {},
|
|
1951
|
+
...options.category?.length ? { categories: options.category } : {},
|
|
1952
|
+
...options.does?.length ? { does: options.does } : {},
|
|
1953
|
+
...options.doesNot?.length ? { does_not: options.doesNot } : {},
|
|
1954
|
+
...options.example?.length ? { examples: options.example } : {}
|
|
1955
|
+
};
|
|
1652
1956
|
}
|
|
1653
|
-
function
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1957
|
+
async function listCatalog(filter = {}) {
|
|
1958
|
+
return (await api("GET", `/catalog/agents${catalogQuery(filter)}`, void 0, optionalAuth())).agents;
|
|
1959
|
+
}
|
|
1960
|
+
async function showCatalogAgent(handle) {
|
|
1961
|
+
return (await api("GET", `/catalog/agents/${encodeURIComponent(bare(handle))}`, void 0, optionalAuth())).agent;
|
|
1962
|
+
}
|
|
1963
|
+
async function listCategories() {
|
|
1964
|
+
return (await api("GET", "/catalog/categories", void 0, { auth: false })).categories;
|
|
1965
|
+
}
|
|
1966
|
+
async function publishListing(agentId, options) {
|
|
1967
|
+
return (await api("POST", "/catalog/listings", listingBody(agentId, options))).agent;
|
|
1968
|
+
}
|
|
1969
|
+
async function removeListing(handle) {
|
|
1970
|
+
await api("DELETE", `/catalog/listings/${encodeURIComponent(bare(handle))}`);
|
|
1971
|
+
}
|
|
1972
|
+
async function setEngineStatus(engine, status, reason) {
|
|
1973
|
+
await api("PATCH", `/catalog/engines/${encodeURIComponent(engine)}`, { status, reason });
|
|
1974
|
+
const state = await api("GET", "/catalog/engines", void 0, { auth: false });
|
|
1975
|
+
if (state.delisted.includes(engine) !== (status === "delisted")) {
|
|
1976
|
+
throw new Error("The catalog does not show the requested state; check whether another operator changed it.");
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
var uses = (agent2) => `${agent2.stats.uses} ${agent2.stats.uses === 1 ? "use" : "uses"}`;
|
|
1980
|
+
var mine = (agent2) => agent2.visibility === "workspace" ? " (yours)" : "";
|
|
1981
|
+
function renderCatalogAgent(agent2) {
|
|
1982
|
+
const section = (title, lines) => lines.length ? [`${title}:`, ...lines.map((line) => ` - ${line}`)] : [];
|
|
1663
1983
|
return [
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
"It is historical context: verify a path before acting on it, and follow the person's current message.",
|
|
1668
|
-
"",
|
|
1669
|
-
"## Files",
|
|
1670
|
-
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\`)` : ""}.`,
|
|
1671
|
-
...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/.`] : [],
|
|
1672
|
-
...counts.attachments ? [`- ${counts.attachments} attachments of the conversation under ${platform.notesDir}/attachments/.`] : [],
|
|
1673
|
-
"- 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.",
|
|
1674
|
-
"",
|
|
1675
|
-
"## Repository",
|
|
1676
|
-
...git2 ? [
|
|
1677
|
-
`- Remote: ${git2.remote ?? "none"}`,
|
|
1678
|
-
`- Branch: ${git2.branch ?? "detached"} at commit ${git2.commit ?? "unknown"}${git2.ahead ? ` (${git2.ahead} commits not pushed)` : ""}`,
|
|
1679
|
-
...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."],
|
|
1680
|
-
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.`
|
|
1681
|
-
] : ["- The working directory was not a git repository."],
|
|
1984
|
+
`@${agent2.handle} ${agent2.name}${mine(agent2)}`,
|
|
1985
|
+
agent2.tagline,
|
|
1986
|
+
...agent2.description && agent2.description !== agent2.tagline ? ["", agent2.description] : [],
|
|
1682
1987
|
"",
|
|
1683
|
-
"
|
|
1684
|
-
|
|
1685
|
-
...
|
|
1988
|
+
`Categories: ${agent2.categories.join(", ") || "none"}`,
|
|
1989
|
+
`Used: ${uses(agent2)}`,
|
|
1990
|
+
...section("Does", agent2.does),
|
|
1991
|
+
...section("Does not", agent2.does_not),
|
|
1992
|
+
...section("Examples", agent2.examples),
|
|
1686
1993
|
"",
|
|
1687
|
-
|
|
1688
|
-
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."),
|
|
1689
|
-
...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.`] : [],
|
|
1690
|
-
""
|
|
1994
|
+
`Hire it: sky session create @${agent2.handle} --message "\u2026"`
|
|
1691
1995
|
].join("\n");
|
|
1692
1996
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1997
|
+
|
|
1998
|
+
// src/clone.ts
|
|
1999
|
+
import { spawn } from "child_process";
|
|
2000
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
2001
|
+
import { homedir as homedir4 } from "os";
|
|
2002
|
+
import { basename as basename5, join as join7 } from "path";
|
|
2003
|
+
import { createInterface } from "readline";
|
|
2004
|
+
var PROMPT_CAP = 1e5;
|
|
2005
|
+
var HARNESSES = ["claude_code", "codex", "openclaw"];
|
|
2006
|
+
function claudeConfigRoot() {
|
|
2007
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join7(homedir4(), ".claude");
|
|
2008
|
+
}
|
|
2009
|
+
function readIfExists(path) {
|
|
1695
2010
|
try {
|
|
1696
|
-
|
|
1697
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2011
|
+
return readFileSync8(path, "utf8");
|
|
1698
2012
|
} catch {
|
|
1699
|
-
return
|
|
2013
|
+
return void 0;
|
|
1700
2014
|
}
|
|
1701
2015
|
}
|
|
1702
|
-
function
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
for (const file of files2) {
|
|
1710
|
-
const [kind, ...rest] = file.name.split("/");
|
|
1711
|
-
const path = rest.join("/");
|
|
1712
|
-
if (kind === "workspace") {
|
|
1713
|
-
if (wholeFolder) continue;
|
|
1714
|
-
entries.set(path, file.data);
|
|
1715
|
-
counts.project += 1;
|
|
1716
|
-
} else if (kind === "files") {
|
|
1717
|
-
entries.set(`${platform.notesDir}/files/${path.replace(/^~\//, "home/")}`, file.data);
|
|
1718
|
-
counts.outside += 1;
|
|
1719
|
-
} else if (kind === "imported-attachments") {
|
|
1720
|
-
entries.set(`${platform.notesDir}/attachments/${path}`, file.data);
|
|
1721
|
-
counts.attachments += 1;
|
|
1722
|
-
} else if (kind === "agentsky") entries.set(`${platform.notesDir}/${path}`, file.data);
|
|
1723
|
-
else if (kind === "skills" && rest.length > 1) {
|
|
1724
|
-
const skill = rest[0].split(":").pop();
|
|
1725
|
-
const inside = rest.slice(1).join("/");
|
|
1726
|
-
skills2.add(skill);
|
|
1727
|
-
entries.set(`.claude/skills/${skill}/${inside}`, file.data);
|
|
1728
|
-
entries.set(`.agents/skills/${skill}/${inside}`, file.data);
|
|
1729
|
-
}
|
|
2016
|
+
function readJson2(path) {
|
|
2017
|
+
const raw = readIfExists(path);
|
|
2018
|
+
if (raw === void 0) return void 0;
|
|
2019
|
+
try {
|
|
2020
|
+
return JSON.parse(raw);
|
|
2021
|
+
} catch {
|
|
2022
|
+
return void 0;
|
|
1730
2023
|
}
|
|
1731
|
-
const servers = (workspace2?.mcp_servers ?? []).filter((server) => server && typeof server.name === "string" && (server.transport === "stdio" ? typeof server.command === "string" : typeof server.url === "string"));
|
|
1732
|
-
const mcpWritten = servers.length > 0 && !entries.has(".mcp.json") && !wholeFolder?.paths.has(".mcp.json");
|
|
1733
|
-
if (mcpWritten) entries.set(".mcp.json", Buffer.from(mcpJson(servers, platform)));
|
|
1734
|
-
entries.set(`${platform.notesDir}/README.md`, Buffer.from(readme(folder, workspace2 ? { ...workspace2, mcp_servers: servers } : null, counts, [...skills2], mcpWritten, platform)));
|
|
1735
|
-
if (wholeFolder) for (const path of wholeFolder.paths) entries.delete(path);
|
|
1736
|
-
return { folder, entries, skills: [...skills2], mcpServers: servers.map((server) => server.name) };
|
|
1737
|
-
}
|
|
1738
|
-
var TAR_BLOCK = 512;
|
|
1739
|
-
var TAR_END = Buffer.alloc(TAR_BLOCK * 2);
|
|
1740
|
-
function header(name, size, type) {
|
|
1741
|
-
const block = Buffer.alloc(TAR_BLOCK);
|
|
1742
|
-
block.write(name, 0, 100, "utf8");
|
|
1743
|
-
block.write("0000644\0", 100);
|
|
1744
|
-
block.write("0000000\0", 108);
|
|
1745
|
-
block.write("0000000\0", 116);
|
|
1746
|
-
block.write(size.toString(8).padStart(11, "0") + "\0", 124);
|
|
1747
|
-
block.write("00000000000\0", 136);
|
|
1748
|
-
block.write(" ", 148);
|
|
1749
|
-
block.write(type, 156);
|
|
1750
|
-
block.write("ustar\x0000", 257);
|
|
1751
|
-
let sum = 0;
|
|
1752
|
-
for (const byte of block) sum += byte;
|
|
1753
|
-
block.write(sum.toString(8).padStart(6, "0") + "\0 ", 148);
|
|
1754
|
-
return block;
|
|
1755
2024
|
}
|
|
1756
|
-
function
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2025
|
+
function claudeModel(configRoot) {
|
|
2026
|
+
const candidates = [
|
|
2027
|
+
join7(process.cwd(), ".claude", "settings.local.json"),
|
|
2028
|
+
join7(process.cwd(), ".claude", "settings.json"),
|
|
2029
|
+
join7(configRoot, "settings.json")
|
|
2030
|
+
];
|
|
2031
|
+
for (const path of candidates) {
|
|
2032
|
+
const model = readJson2(path)?.model;
|
|
2033
|
+
if (typeof model === "string" && model.trim()) return model.trim();
|
|
1764
2034
|
}
|
|
2035
|
+
return void 0;
|
|
1765
2036
|
}
|
|
1766
|
-
function
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
2037
|
+
function claudeMcpServers() {
|
|
2038
|
+
const merged = {};
|
|
2039
|
+
const userConfig = readJson2(join7(homedir4(), ".claude.json"));
|
|
2040
|
+
Object.assign(merged, userConfig?.mcpServers ?? {});
|
|
2041
|
+
Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
|
|
2042
|
+
const projectConfig = readJson2(
|
|
2043
|
+
join7(process.cwd(), ".mcp.json")
|
|
2044
|
+
);
|
|
2045
|
+
Object.assign(merged, projectConfig?.mcpServers ?? {});
|
|
2046
|
+
return merged;
|
|
1774
2047
|
}
|
|
2048
|
+
function assemblePrompt(parts) {
|
|
2049
|
+
const joined = parts.map((p) => `# \u2500\u2500 ${p.label} \u2500\u2500
|
|
1775
2050
|
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
var MAX_REDACTED_TEXT_BYTES = 10 * 1024 * 1024;
|
|
1781
|
-
var SKIPPED = /* @__PURE__ */ new Set([".git", "node_modules", ".venv", "venv", "__pycache__", ".next", "dist", "build", "target", ".DS_Store", ".cache", ".turbo"]);
|
|
1782
|
-
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;
|
|
1783
|
-
function listFiles(cwd) {
|
|
1784
|
-
const git2 = spawnSync2("git", ["-C", cwd, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 512 * 1024 * 1024 });
|
|
1785
|
-
if (git2.status === 0) return git2.stdout.split("\0").filter(Boolean).filter((path) => !path.split("/").some((part) => part === ".git"));
|
|
1786
|
-
const out = [];
|
|
1787
|
-
const walk = (dir, prefix) => {
|
|
1788
|
-
let entries;
|
|
1789
|
-
try {
|
|
1790
|
-
entries = readdirSync3(dir, { withFileTypes: true });
|
|
1791
|
-
} catch {
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
for (const entry2 of entries) {
|
|
1795
|
-
if (SKIPPED.has(entry2.name)) continue;
|
|
1796
|
-
const rel = prefix ? `${prefix}/${entry2.name}` : entry2.name;
|
|
1797
|
-
if (entry2.isDirectory()) walk(join6(dir, entry2.name), rel);
|
|
1798
|
-
else out.push(rel);
|
|
1799
|
-
if (out.length > MAX_ENVIRONMENT_FILES) return;
|
|
1800
|
-
}
|
|
1801
|
-
};
|
|
1802
|
-
walk(cwd, "");
|
|
1803
|
-
return out;
|
|
1804
|
-
}
|
|
1805
|
-
function textOf(path, size) {
|
|
1806
|
-
if (size > MAX_REDACTED_TEXT_BYTES) return null;
|
|
1807
|
-
const data = readFileSync7(path);
|
|
1808
|
-
if (data.subarray(0, 8e3).includes(0) || !isUtf82(data)) return null;
|
|
1809
|
-
return data;
|
|
2051
|
+
${p.content.trim()}`).join("\n\n").trim();
|
|
2052
|
+
if (joined.length <= PROMPT_CAP) return { prompt: joined, truncated: false };
|
|
2053
|
+
const notice = "\n\n[truncated by sky clone: local instructions exceeded the 100k prompt cap]";
|
|
2054
|
+
return { prompt: joined.slice(0, PROMPT_CAP - notice.length) + notice, truncated: true };
|
|
1810
2055
|
}
|
|
1811
|
-
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
const
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
if (
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
});
|
|
1841
|
-
try {
|
|
1842
|
-
for (const rel of listed) {
|
|
1843
|
-
const full = join6(cwd, ...rel.split("/"));
|
|
1844
|
-
if (SECRET_FILE2.test(rel)) {
|
|
1845
|
-
leftOut.push({ path: rel, reason: "credential file" });
|
|
1846
|
-
continue;
|
|
1847
|
-
}
|
|
1848
|
-
let stat;
|
|
1849
|
-
try {
|
|
1850
|
-
stat = lstatSync(full);
|
|
1851
|
-
} catch {
|
|
1852
|
-
continue;
|
|
1853
|
-
}
|
|
1854
|
-
if (!stat.isFile()) {
|
|
1855
|
-
leftOut.push({ path: rel, reason: "not a regular file" });
|
|
1856
|
-
continue;
|
|
1857
|
-
}
|
|
1858
|
-
const text = textOf(full, stat.size);
|
|
1859
|
-
if (text) {
|
|
1860
|
-
const masked = redactSecretValues(text.toString("utf8"));
|
|
1861
|
-
if (Object.keys(masked.hits).length) redacted2 += 1;
|
|
1862
|
-
const data = Buffer.from(masked.text, "utf8");
|
|
1863
|
-
await write(tarHeader(rel, data.length));
|
|
1864
|
-
await write(data);
|
|
1865
|
-
await write(tarPadding(data.length));
|
|
1866
|
-
} else {
|
|
1867
|
-
await write(tarHeader(rel, stat.size));
|
|
1868
|
-
const fd = openSync2(full, "r");
|
|
1869
|
-
try {
|
|
1870
|
-
const chunk = Buffer.alloc(1024 * 1024);
|
|
1871
|
-
let left = stat.size;
|
|
1872
|
-
for (let offset = 0; left > 0; ) {
|
|
1873
|
-
const read = readSync2(fd, chunk, 0, Math.min(chunk.length, left), offset);
|
|
1874
|
-
if (read <= 0) throw new Error(`${rel} changed while it was packed; import again.`);
|
|
1875
|
-
await write(Buffer.from(chunk.subarray(0, read)));
|
|
1876
|
-
offset += read;
|
|
1877
|
-
left -= read;
|
|
1878
|
-
}
|
|
1879
|
-
} finally {
|
|
1880
|
-
closeSync2(fd);
|
|
1881
|
-
}
|
|
1882
|
-
await write(tarPadding(stat.size));
|
|
1883
|
-
}
|
|
1884
|
-
written2.add(rel);
|
|
1885
|
-
}
|
|
1886
|
-
const layout = importLayout(resources, cwd, platform, { files: written2.size, paths: written2 });
|
|
1887
|
-
for (const [entry2, data] of layout?.entries ?? []) {
|
|
1888
|
-
await write(tarHeader(entry2, data.length));
|
|
1889
|
-
await write(data);
|
|
1890
|
-
await write(tarPadding(data.length));
|
|
2056
|
+
function readClaudeCode() {
|
|
2057
|
+
const configRoot = claudeConfigRoot();
|
|
2058
|
+
const parts = [];
|
|
2059
|
+
const sources = [];
|
|
2060
|
+
const global = readIfExists(join7(configRoot, "CLAUDE.md"));
|
|
2061
|
+
if (global?.trim()) {
|
|
2062
|
+
parts.push({ label: "Global CLAUDE.md", content: global });
|
|
2063
|
+
sources.push("global CLAUDE.md");
|
|
2064
|
+
}
|
|
2065
|
+
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
2066
|
+
const project2 = readIfExists(join7(process.cwd(), name));
|
|
2067
|
+
if (project2?.trim()) {
|
|
2068
|
+
parts.push({ label: `Project ${name}`, content: project2 });
|
|
2069
|
+
sources.push(`project ${name}`);
|
|
2070
|
+
break;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
const { prompt, truncated } = assemblePrompt(parts);
|
|
2074
|
+
const allServers = claudeMcpServers();
|
|
2075
|
+
const { kept, dropped } = splitMcpServers(allServers);
|
|
2076
|
+
const secretCandidates = [];
|
|
2077
|
+
for (const d of dropped) {
|
|
2078
|
+
const def = allServers[d.name];
|
|
2079
|
+
if (!def?.env) continue;
|
|
2080
|
+
for (const key of d.envKeys) {
|
|
2081
|
+
const value = def.env[key];
|
|
2082
|
+
if (value !== void 0 && value.trim()) {
|
|
2083
|
+
secretCandidates.push({ serverName: d.name, envKey: key, value, url: d.url });
|
|
2084
|
+
}
|
|
1891
2085
|
}
|
|
1892
|
-
await write(TAR_END);
|
|
1893
|
-
gzip.end();
|
|
1894
|
-
await done;
|
|
1895
|
-
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.`);
|
|
1896
|
-
return {
|
|
1897
|
-
path,
|
|
1898
|
-
folder: layout?.folder ?? "imported-session",
|
|
1899
|
-
bytes,
|
|
1900
|
-
sha256: hash.digest("hex"),
|
|
1901
|
-
files: written2.size + (layout?.entries.size ?? 0),
|
|
1902
|
-
redacted: redacted2,
|
|
1903
|
-
leftOut,
|
|
1904
|
-
dispose: () => rmSync2(scratch, { recursive: true, force: true })
|
|
1905
|
-
};
|
|
1906
|
-
} catch (error) {
|
|
1907
|
-
gzip.destroy();
|
|
1908
|
-
sink.destroy();
|
|
1909
|
-
rmSync2(scratch, { recursive: true, force: true });
|
|
1910
|
-
throw error;
|
|
1911
2086
|
}
|
|
2087
|
+
return {
|
|
2088
|
+
harness: "claude_code",
|
|
2089
|
+
configRoot,
|
|
2090
|
+
prompt,
|
|
2091
|
+
promptSources: sources,
|
|
2092
|
+
truncated,
|
|
2093
|
+
model: claudeModel(configRoot),
|
|
2094
|
+
keptServers: kept,
|
|
2095
|
+
droppedServers: dropped,
|
|
2096
|
+
secretCandidates
|
|
2097
|
+
};
|
|
1912
2098
|
}
|
|
1913
|
-
function
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
if (record.type !== "import.resource" || typeof record.name !== "string" || typeof record.media_type !== "string" || typeof record.data !== "string") return [];
|
|
1918
|
-
return [{ name: record.name, mediaType: record.media_type, data: Buffer.from(record.data, "base64") }];
|
|
1919
|
-
} catch {
|
|
1920
|
-
return [];
|
|
2099
|
+
function detectHarness(explicit) {
|
|
2100
|
+
if (explicit) {
|
|
2101
|
+
if (!HARNESSES.includes(explicit)) {
|
|
2102
|
+
throw new Error(`unknown harness ${explicit} \u2014 expected ${HARNESSES.join(" | ")}`);
|
|
1921
2103
|
}
|
|
1922
|
-
|
|
2104
|
+
return explicit;
|
|
2105
|
+
}
|
|
2106
|
+
if (existsSync6(claudeConfigRoot())) return "claude_code";
|
|
2107
|
+
if (existsSync6(join7(homedir4(), ".codex"))) return "codex";
|
|
2108
|
+
if (existsSync6(join7(homedir4(), ".openclaw"))) return "openclaw";
|
|
2109
|
+
throw new Error(
|
|
2110
|
+
"no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
|
|
2111
|
+
);
|
|
1923
2112
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2113
|
+
function readLocal(harness2) {
|
|
2114
|
+
if (harness2 === "claude_code") return readClaudeCode();
|
|
2115
|
+
throw new Error(
|
|
2116
|
+
`${harness2} cloning lands next \u2014 claude_code is the first supported harness`
|
|
2117
|
+
);
|
|
1928
2118
|
}
|
|
1929
|
-
function
|
|
1930
|
-
|
|
1931
|
-
return decodeURIComponent(encoded.replace(/-/g, "%").replace(/_/g, "%5F"));
|
|
1932
|
-
} catch {
|
|
1933
|
-
return encoded;
|
|
1934
|
-
}
|
|
2119
|
+
function linkPath(configRoot) {
|
|
2120
|
+
return join7(configRoot, "agentsky-link.json");
|
|
1935
2121
|
}
|
|
1936
|
-
function
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
const bytes = Buffer.alloc(256 * 1024);
|
|
1940
|
-
return bytes.subarray(0, readSync3(fd, bytes, 0, bytes.length, 0)).toString("utf8");
|
|
1941
|
-
} finally {
|
|
1942
|
-
closeSync3(fd);
|
|
1943
|
-
}
|
|
2122
|
+
function readLink(configRoot) {
|
|
2123
|
+
const link2 = readJson2(linkPath(configRoot));
|
|
2124
|
+
return link2?.agentId ? link2 : void 0;
|
|
1944
2125
|
}
|
|
1945
|
-
function
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
for (const raw of data.split("\n")) {
|
|
1949
|
-
if (!raw.trim()) continue;
|
|
1950
|
-
let line;
|
|
1951
|
-
try {
|
|
1952
|
-
line = JSON.parse(redactLine2(raw));
|
|
1953
|
-
} catch {
|
|
1954
|
-
continue;
|
|
1955
|
-
}
|
|
1956
|
-
if (!line || typeof line !== "object") continue;
|
|
1957
|
-
const rec = line;
|
|
1958
|
-
if (rec.type === "user" && rec.message && typeof rec.message === "object") {
|
|
1959
|
-
const msg = rec.message;
|
|
1960
|
-
if (Array.isArray(msg.content)) {
|
|
1961
|
-
for (const block of msg.content) {
|
|
1962
|
-
if (typeof block === "object" && block !== null) {
|
|
1963
|
-
const b = block;
|
|
1964
|
-
if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
|
|
1965
|
-
return b.text.trim().slice(0, 120);
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
if (typeof msg.content === "string" && msg.content.trim()) {
|
|
1971
|
-
return msg.content.trim().slice(0, 120);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
if (rec.type === "event_msg" && rec.payload && typeof rec.payload === "object") {
|
|
1975
|
-
const payload = rec.payload;
|
|
1976
|
-
if (payload.type === "user_message" && typeof payload.message === "string") return redactLine2(payload.message).slice(0, 120);
|
|
1977
|
-
}
|
|
1978
|
-
if (rec.type === "response_item" && rec.payload && typeof rec.payload === "object") {
|
|
1979
|
-
const payload = rec.payload;
|
|
1980
|
-
if (payload.type === "message" && payload.role === "user" && Array.isArray(payload.content)) {
|
|
1981
|
-
const text = payload.content.find((part) => typeof part?.text === "string")?.text;
|
|
1982
|
-
if (typeof text === "string" && text.trim()) return redactLine2(text.trim()).slice(0, 120);
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
if (rec.type === "session_meta") continue;
|
|
1986
|
-
if (rec.type === "user_turn" || rec.type === "message" && rec.role === "user") {
|
|
1987
|
-
const content = rec.content;
|
|
1988
|
-
if (typeof content === "string" && content.trim()) return content.trim().slice(0, 120);
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
return null;
|
|
1992
|
-
} catch {
|
|
1993
|
-
return null;
|
|
1994
|
-
}
|
|
2126
|
+
function writeLink(configRoot, link2) {
|
|
2127
|
+
writeFileSync3(linkPath(configRoot), `${JSON.stringify(link2, null, 2)}
|
|
2128
|
+
`);
|
|
1995
2129
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
const
|
|
1999
|
-
|
|
2000
|
-
|
|
2130
|
+
var DEFAULT_VAULT_NAME = "default";
|
|
2131
|
+
async function fetchDefaultVaultId() {
|
|
2132
|
+
const { vaults } = await api(
|
|
2133
|
+
"GET",
|
|
2134
|
+
"/vaults"
|
|
2135
|
+
);
|
|
2136
|
+
return vaults.find((v) => v.display_name === DEFAULT_VAULT_NAME)?.id ?? null;
|
|
2137
|
+
}
|
|
2138
|
+
async function ensureDefaultVaultId() {
|
|
2139
|
+
const existing = await fetchDefaultVaultId();
|
|
2140
|
+
if (existing) return existing;
|
|
2001
2141
|
try {
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
const workspace2 = decodeWorkspace(encoded);
|
|
2011
|
-
for (const entry2 of entries) {
|
|
2012
|
-
if (!entry2.endsWith(".jsonl")) continue;
|
|
2013
|
-
const sessionId = entry2.slice(0, -6);
|
|
2014
|
-
const mainFile = join7(projectDir, entry2);
|
|
2015
|
-
let stat;
|
|
2016
|
-
try {
|
|
2017
|
-
stat = statSync5(mainFile);
|
|
2018
|
-
} catch {
|
|
2019
|
-
continue;
|
|
2020
|
-
}
|
|
2021
|
-
if (!stat.isFile()) continue;
|
|
2022
|
-
let discoveryError;
|
|
2023
|
-
const subagentFiles = [];
|
|
2024
|
-
const subagentDir = join7(projectDir, sessionId, "subagents");
|
|
2025
|
-
if (existsSync6(subagentDir)) {
|
|
2026
|
-
try {
|
|
2027
|
-
const subFiles = readdirSync4(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
2028
|
-
for (const f of subFiles) {
|
|
2029
|
-
subagentFiles.push(join7(subagentDir, f));
|
|
2030
|
-
}
|
|
2031
|
-
} catch {
|
|
2032
|
-
discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
sessions.push({
|
|
2036
|
-
harness: "claude_code",
|
|
2037
|
-
sessionId,
|
|
2038
|
-
workspace: workspace2,
|
|
2039
|
-
title: sniffTitle(mainFile),
|
|
2040
|
-
mtimeMs: stat.mtimeMs,
|
|
2041
|
-
files: [mainFile, ...subagentFiles],
|
|
2042
|
-
...discoveryError ? { discoveryError } : {}
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2142
|
+
const { vault: vault2 } = await api("POST", "/vaults", {
|
|
2143
|
+
display_name: DEFAULT_VAULT_NAME
|
|
2144
|
+
});
|
|
2145
|
+
return vault2.id;
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
if (error instanceof ApiError && error.status === 409) {
|
|
2148
|
+
const raced = await fetchDefaultVaultId();
|
|
2149
|
+
if (raced) return raced;
|
|
2045
2150
|
}
|
|
2046
|
-
|
|
2151
|
+
throw error;
|
|
2047
2152
|
}
|
|
2048
|
-
return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
2049
2153
|
}
|
|
2050
|
-
function
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2154
|
+
async function confirmAndVaultSecrets(candidates, yes) {
|
|
2155
|
+
if (!candidates.length) return;
|
|
2156
|
+
if (yes) {
|
|
2157
|
+
console.log(
|
|
2158
|
+
` ${candidates.length} secret${candidates.length === 1 ? "" : "s"} from dropped MCP servers not saved (--yes skips vault confirmation).`
|
|
2159
|
+
);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
console.log("\nSecret-bearing MCP servers were left out of the clone payload.");
|
|
2163
|
+
console.log("You can save their keys to your workspace vault now (values are sent encrypted, never displayed).\n");
|
|
2164
|
+
let vaultId;
|
|
2165
|
+
let saved = 0;
|
|
2166
|
+
let skipped = 0;
|
|
2167
|
+
for (const c of candidates) {
|
|
2168
|
+
const chosen = await confirm(` Add ${c.envKey} (from ${c.serverName}) to your default vault?`);
|
|
2169
|
+
if (!chosen) {
|
|
2170
|
+
console.log(` \u2014 skipped ${c.envKey}`);
|
|
2171
|
+
skipped += 1;
|
|
2172
|
+
continue;
|
|
2059
2173
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
mtimeMs: statSync5(full).mtimeMs,
|
|
2080
|
-
files: [full]
|
|
2081
|
-
},
|
|
2082
|
-
...typeof parent === "string" ? { parent } : {}
|
|
2083
|
-
});
|
|
2084
|
-
} catch {
|
|
2085
|
-
continue;
|
|
2174
|
+
try {
|
|
2175
|
+
vaultId ??= await ensureDefaultVaultId();
|
|
2176
|
+
await api("POST", `/vaults/${encodeURIComponent(vaultId)}/secrets`, {
|
|
2177
|
+
display_name: `${c.envKey} (${c.serverName})`,
|
|
2178
|
+
auth: {
|
|
2179
|
+
type: "environment_variable",
|
|
2180
|
+
secret_name: c.envKey,
|
|
2181
|
+
value: c.value,
|
|
2182
|
+
allowed_hosts: c.url ? [c.url] : []
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
console.log(` \u2713 saved ${c.envKey}`);
|
|
2186
|
+
saved += 1;
|
|
2187
|
+
} catch (error) {
|
|
2188
|
+
if (error instanceof ApiError && error.status === 409) {
|
|
2189
|
+
console.log(` \u2014 ${c.envKey} already in vault`);
|
|
2190
|
+
} else {
|
|
2191
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2192
|
+
console.log(` \u2717 ${c.envKey} failed: ${msg}`);
|
|
2086
2193
|
}
|
|
2087
2194
|
}
|
|
2088
2195
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
...item.session.files,
|
|
2095
|
-
...descendants(item.session.sessionId, visited)
|
|
2096
|
-
]);
|
|
2196
|
+
if (saved || skipped) {
|
|
2197
|
+
console.log(
|
|
2198
|
+
`
|
|
2199
|
+
Vault: ${saved} saved, ${skipped} skipped. Attach the "default" vault to a session to use these keys.`
|
|
2200
|
+
);
|
|
2097
2201
|
}
|
|
2098
|
-
return records.filter((item) => !item.parent).map(({ session: session2 }) => ({
|
|
2099
|
-
...session2,
|
|
2100
|
-
files: [.../* @__PURE__ */ new Set([...session2.files, ...descendants(session2.sessionId, /* @__PURE__ */ new Set())])]
|
|
2101
|
-
})).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
2102
|
-
}
|
|
2103
|
-
function listRecentSessions(harness2, limit) {
|
|
2104
|
-
const claudeSessions = harness2 === "codex" ? [] : listClaudeCodeSessions(limit);
|
|
2105
|
-
const codexSessions = harness2 === "claude_code" ? [] : listCodexSessions(limit);
|
|
2106
|
-
return [...claudeSessions, ...codexSessions].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
2107
|
-
}
|
|
2108
|
-
function agentUseBase() {
|
|
2109
|
-
const env = process.env.AGENT_USE_URL?.trim().replace(/\/$/, "");
|
|
2110
|
-
if (env) return env;
|
|
2111
|
-
const api2 = apiBase().replace(/\/$/, "");
|
|
2112
|
-
const derived = api2.replace(/\/api\/v1$/, "").replace(/\/v1$/, "").replace("api.agentsky.dev", "agentsky.dev");
|
|
2113
|
-
if (derived.includes("localhost:3000")) return derived.replace("localhost:3000", "localhost:3002");
|
|
2114
|
-
return derived;
|
|
2115
|
-
}
|
|
2116
|
-
function currentSessionEnv() {
|
|
2117
|
-
const claudeId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
2118
|
-
if (claudeId) return { harness: "claude_code", sessionId: claudeId };
|
|
2119
|
-
const codexId = process.env.CODEX_THREAD_ID;
|
|
2120
|
-
if (codexId) return { harness: "codex", sessionId: codexId };
|
|
2121
|
-
return void 0;
|
|
2122
2202
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
headers: {
|
|
2131
|
-
"content-type": "application/json",
|
|
2132
|
-
authorization: `Bearer ${token}`,
|
|
2133
|
-
"user-agent": USER_AGENT
|
|
2134
|
-
},
|
|
2135
|
-
body: JSON.stringify({ harness: harness2 })
|
|
2203
|
+
function confirm(question) {
|
|
2204
|
+
return new Promise((done) => {
|
|
2205
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2206
|
+
rl.question(`${question} [Y/n] `, (answer) => {
|
|
2207
|
+
rl.close();
|
|
2208
|
+
done(/^(y(es)?)?$/i.test(answer.trim()));
|
|
2209
|
+
});
|
|
2136
2210
|
});
|
|
2137
|
-
if (!res.ok) {
|
|
2138
|
-
const body = await res.json().catch(() => null);
|
|
2139
|
-
throw new Error(
|
|
2140
|
-
typeof body?.error === "string" ? body.error : `Failed to create import (${res.status}).`
|
|
2141
|
-
);
|
|
2142
|
-
}
|
|
2143
|
-
return await res.json();
|
|
2144
2211
|
}
|
|
2145
|
-
function
|
|
2146
|
-
|
|
2147
|
-
try {
|
|
2148
|
-
raw = readFileSync8(summaryPath, "utf8");
|
|
2149
|
-
} catch {
|
|
2150
|
-
throw new Error(`Cannot read summary file: ${summaryPath}`);
|
|
2151
|
-
}
|
|
2152
|
-
let pkg;
|
|
2212
|
+
function openBrowser(url) {
|
|
2213
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2153
2214
|
try {
|
|
2154
|
-
|
|
2215
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
2155
2216
|
} catch {
|
|
2156
|
-
throw new Error("Summary file is not valid JSON.");
|
|
2157
2217
|
}
|
|
2158
|
-
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) throw new Error("Summary file must be a JSON object.");
|
|
2159
|
-
const p = pkg;
|
|
2160
|
-
if (p.format !== "asteroids.session-summary") throw new Error('Summary file must have format "asteroids.session-summary".');
|
|
2161
|
-
if (p.format_version !== 1) throw new Error("Summary file must have format_version 1.");
|
|
2162
|
-
const redacted2 = redactLine2(raw.trimEnd());
|
|
2163
|
-
return redacted2;
|
|
2164
2218
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
var MAX_LISTED_ARTIFACTS = 99;
|
|
2168
|
-
function leftOutLine(left) {
|
|
2169
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2170
|
-
for (const file of left) counts.set(file.reason, (counts.get(file.reason) ?? 0) + 1);
|
|
2171
|
-
const files2 = [...counts].map(([reason, n]) => `${n} ${n === 1 ? "file" : "files"} (${reason})`).join(", ");
|
|
2172
|
-
return `Not transferred: ${files2 ? `${files2}; ` : ""}runtime state such as running processes, installed tools and environment variables; and reasoning the source harness stored unreadably.`;
|
|
2219
|
+
function kb(text) {
|
|
2220
|
+
return `${Math.max(1, Math.round(text.length / 1024))} KB`;
|
|
2173
2221
|
}
|
|
2174
|
-
function
|
|
2175
|
-
|
|
2176
|
-
const
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2222
|
+
async function createClone(local, opts) {
|
|
2223
|
+
const keptNames = Object.keys(local.keptServers);
|
|
2224
|
+
const summary = [
|
|
2225
|
+
local.promptSources.length ? `${local.promptSources.join(" + ")} (${kb(local.prompt)})` : "no instructions found",
|
|
2226
|
+
`model ${local.model ?? "(harness default)"}`,
|
|
2227
|
+
`${keptNames.length} MCP server${keptNames.length === 1 ? "" : "s"}${keptNames.length ? ` (${keptNames.join(", ")})` : ""}`
|
|
2228
|
+
].join(" \xB7 ");
|
|
2229
|
+
console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename5(process.cwd())})`);
|
|
2230
|
+
console.log(`Will clone: ${summary}`);
|
|
2231
|
+
if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
|
|
2232
|
+
if (local.droppedServers.length) {
|
|
2233
|
+
console.log(
|
|
2234
|
+
`Left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
if (opts.dryRun) {
|
|
2238
|
+
console.log("Dry run \u2014 nothing sent.");
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
if (!opts.yes && !await confirm("Launch this as a new cloud agent?")) {
|
|
2242
|
+
console.log("Aborted \u2014 nothing sent.");
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
const { servers: mcpServers2, skipped } = mcpServersFromKept(local.keptServers);
|
|
2246
|
+
if (skipped.length) {
|
|
2247
|
+
console.log(` note: local (stdio) MCP servers have no cloud equivalent and were skipped: ${skipped.join(", ")}`);
|
|
2248
|
+
}
|
|
2249
|
+
const body = {
|
|
2250
|
+
agentType: local.harness,
|
|
2251
|
+
...local.model ? { llm: local.model } : {},
|
|
2252
|
+
...local.prompt ? { prompt: local.prompt } : {},
|
|
2253
|
+
...mcpServers2.length ? { mcpServers: mcpServers2 } : {}
|
|
2254
|
+
};
|
|
2255
|
+
let sentLlm = local.model ?? null;
|
|
2256
|
+
let created;
|
|
2257
|
+
try {
|
|
2258
|
+
({ agent: created } = await api("POST", "/agents", body));
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
if (error instanceof ApiError && error.status === 422 && local.model) {
|
|
2261
|
+
const successor = supersededBy(local.model);
|
|
2262
|
+
console.log(
|
|
2263
|
+
successor ? ` note: ${local.model} is superseded \u2014 using ${successor}` : ` note: ${local.model} isn't available for ${local.harness} \u2014 using the harness default`
|
|
2264
|
+
);
|
|
2265
|
+
sentLlm = successor;
|
|
2266
|
+
({ agent: created } = await api("POST", "/agents", {
|
|
2267
|
+
...body,
|
|
2268
|
+
llm: successor ?? void 0
|
|
2269
|
+
}));
|
|
2270
|
+
} else {
|
|
2271
|
+
throw error;
|
|
2224
2272
|
}
|
|
2225
2273
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2274
|
+
console.log("Starting its runtime\u2026");
|
|
2275
|
+
await api("POST", "/sessions", { agent: created.id });
|
|
2276
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2277
|
+
writeLink(local.configRoot, {
|
|
2278
|
+
baseUrl: apiBase(),
|
|
2279
|
+
agentId: created.id,
|
|
2280
|
+
clonedAt: now,
|
|
2281
|
+
lastSyncedAt: now,
|
|
2282
|
+
llm: sentLlm,
|
|
2283
|
+
mcpServers: keptNames
|
|
2236
2284
|
});
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2285
|
+
const url = `${apiBase()}/agents/${created.id}`;
|
|
2286
|
+
console.log(`Cloned \u2192 ${created.id} ${url}`);
|
|
2287
|
+
console.log(
|
|
2288
|
+
`Linked this machine (${linkPath(local.configRoot)}). Push prompt changes anytime by running \`sky clone\` again.`
|
|
2289
|
+
);
|
|
2290
|
+
await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
|
|
2291
|
+
if (opts.open) openBrowser(url);
|
|
2292
|
+
}
|
|
2293
|
+
async function syncClone(local, link2, opts) {
|
|
2294
|
+
const since = link2.lastSyncedAt ?? link2.clonedAt;
|
|
2295
|
+
console.log(
|
|
2296
|
+
`Syncing ${link2.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
|
|
2297
|
+
);
|
|
2298
|
+
if (opts.dryRun) {
|
|
2299
|
+
console.log(
|
|
2300
|
+
` would push prompt (${kb(local.prompt)}) to ${link2.agentId}. Dry run \u2014 nothing sent.`
|
|
2301
|
+
);
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
let agent2;
|
|
2305
|
+
try {
|
|
2306
|
+
({ agent: agent2 } = await api(
|
|
2307
|
+
"GET",
|
|
2308
|
+
`/agents/${encodeURIComponent(link2.agentId)}`
|
|
2309
|
+
));
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
2312
|
+
throw new Error(
|
|
2313
|
+
`linked agent ${link2.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
throw error;
|
|
2263
2317
|
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
if (unreadableCount > 0) gaps.push({ kind: "unreadable", count: unreadableCount });
|
|
2269
|
-
const cwd = meta.cwd ?? (session2.harness === "claude_code" ? session2.workspace : "");
|
|
2270
|
-
return { body, transcriptLines: lines.length, outputBytes, redactionCount, gaps, attachedFiles: files2.attached, leftOutFiles: files2.leftOut, setup: files2.setup, cwd, resourceLines: files2.lines };
|
|
2271
|
-
}
|
|
2272
|
-
async function importSession(opts) {
|
|
2273
|
-
const base = (opts.agentUseUrl ?? agentUseBase()).replace(/\/$/, "");
|
|
2274
|
-
const destination2 = new URL(base);
|
|
2275
|
-
if (destination2.username || destination2.password || destination2.protocol !== "https:" && !(destination2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(destination2.hostname))) {
|
|
2276
|
-
throw new Error("Import requires HTTPS, or a loopback development server.");
|
|
2318
|
+
if (agent2.agentType !== local.harness) {
|
|
2319
|
+
throw new Error(
|
|
2320
|
+
`linked agent ${link2.agentId} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
|
|
2321
|
+
);
|
|
2277
2322
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2323
|
+
const result = await api(
|
|
2324
|
+
"PUT",
|
|
2325
|
+
`/agents/${encodeURIComponent(link2.agentId)}/prompt`,
|
|
2326
|
+
{ prompt: local.prompt }
|
|
2327
|
+
);
|
|
2328
|
+
console.log(
|
|
2329
|
+
result.unchanged ? " prompt unchanged" : ` prompt updated (v${result.version}${result.applied === "live" ? ", hot-reloaded" : ", applies on next boot"})`
|
|
2330
|
+
);
|
|
2331
|
+
const drift = [];
|
|
2332
|
+
const localModel = local.model ?? null;
|
|
2333
|
+
const linkedModel = link2.llm ?? agent2.llm;
|
|
2334
|
+
if (localModel && linkedModel && localModel !== linkedModel) {
|
|
2335
|
+
drift.push(`model changed ${linkedModel} \u2192 ${localModel}`);
|
|
2288
2336
|
}
|
|
2289
|
-
|
|
2290
|
-
const
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
});
|
|
2297
|
-
|
|
2298
|
-
const result2 = await response.json().catch(() => null);
|
|
2299
|
-
throw new Error(typeof result2?.error === "string" ? result2.error : result2?.error?.message ?? `Upload failed (${response.status}).`);
|
|
2337
|
+
const before = new Set(link2.mcpServers ?? []);
|
|
2338
|
+
const nowKept = Object.keys(local.keptServers);
|
|
2339
|
+
const newServers = nowKept.filter((n) => !before.has(n));
|
|
2340
|
+
const goneServers = [...before].filter((n) => !nowKept.includes(n));
|
|
2341
|
+
if (newServers.length) drift.push(`${newServers.length} new MCP server${newServers.length === 1 ? "" : "s"} (${newServers.join(", ")})`);
|
|
2342
|
+
if (goneServers.length) drift.push(`${goneServers.length} removed MCP server${goneServers.length === 1 ? "" : "s"} (${goneServers.join(", ")})`);
|
|
2343
|
+
if (drift.length) {
|
|
2344
|
+
console.log(` skipped (set at launch): ${drift.join("; ")}`);
|
|
2345
|
+
console.log(" \u2192 a running agent can't change these; run `sky clone --new` to launch a fresh agent.");
|
|
2300
2346
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
traceId: finalTraceId,
|
|
2306
|
-
url: `${base}/playground/new?import=${encodeURIComponent(finalTraceId)}`,
|
|
2307
|
-
harness: opts.session.harness,
|
|
2308
|
-
sessionId: opts.session.sessionId,
|
|
2309
|
-
transcriptFiles: opts.session.files.length,
|
|
2310
|
-
bytes: outputBytes,
|
|
2311
|
-
redactionCount,
|
|
2312
|
-
gaps,
|
|
2313
|
-
attachedFiles,
|
|
2314
|
-
leftOutFiles,
|
|
2315
|
-
setup,
|
|
2316
|
-
environment
|
|
2317
|
-
};
|
|
2318
|
-
}
|
|
2319
|
-
async function grantCall(base, key, path, body) {
|
|
2320
|
-
const response = await fetch(`${base}${path}`, {
|
|
2321
|
-
method: "POST",
|
|
2322
|
-
redirect: "error",
|
|
2323
|
-
signal: AbortSignal.timeout(3e5),
|
|
2324
|
-
headers: { authorization: `Bearer ${key}`, "content-type": "application/json", "user-agent": USER_AGENT },
|
|
2325
|
-
body: JSON.stringify(body)
|
|
2326
|
-
});
|
|
2327
|
-
const answer = await response.json().catch(() => null);
|
|
2328
|
-
if (!response.ok) throw new Error(typeof answer?.error === "string" ? answer.error : answer?.error?.message ?? `The environment upload failed (${response.status}).`);
|
|
2329
|
-
return answer;
|
|
2330
|
-
}
|
|
2331
|
-
async function uploadEnvironment(base, key, traceId, cwd, resourceLines, onProgress) {
|
|
2332
|
-
onProgress?.("Packing the working folder (secrets redacted locally)\u2026");
|
|
2333
|
-
const packed = await packEnvironment(cwd, resourcesOf(resourceLines));
|
|
2334
|
-
try {
|
|
2335
|
-
const path = `/api/traces/${encodeURIComponent(traceId)}/environment`;
|
|
2336
|
-
const { upload } = await grantCall(base, key, path, { name: packed.folder, size: packed.bytes, sha256: packed.sha256, files: packed.files });
|
|
2337
|
-
let shown = -1;
|
|
2338
|
-
await transfer(upload, packed.path, (sent, total) => {
|
|
2339
|
-
const percent = Math.floor(sent / total * 100);
|
|
2340
|
-
if (percent >= shown + 10 || sent === total) {
|
|
2341
|
-
shown = percent;
|
|
2342
|
-
onProgress?.(`Uploading the working folder\u2026 ${percent}% of ${(total / 1024 / 1024).toFixed(1)} MiB`);
|
|
2343
|
-
}
|
|
2344
|
-
});
|
|
2345
|
-
await grantCall(base, key, `${path}/complete`, {});
|
|
2346
|
-
return { folder: packed.folder, files: packed.files, bytes: packed.bytes, redacted: packed.redacted, leftOut: packed.leftOut.length };
|
|
2347
|
-
} finally {
|
|
2348
|
-
packed.dispose();
|
|
2347
|
+
if (local.droppedServers.length) {
|
|
2348
|
+
console.log(
|
|
2349
|
+
` left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
|
|
2350
|
+
);
|
|
2349
2351
|
}
|
|
2352
|
+
writeLink(local.configRoot, { ...link2, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2353
|
+
console.log(`Synced ${link2.agentId}.`);
|
|
2354
|
+
await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
|
|
2355
|
+
if (opts.open) openBrowser(`${apiBase()}/agents/${link2.agentId}`);
|
|
2356
|
+
}
|
|
2357
|
+
async function clone(opts) {
|
|
2358
|
+
const harness2 = detectHarness(opts.harness);
|
|
2359
|
+
const local = readLocal(harness2);
|
|
2360
|
+
const link2 = opts.new ? void 0 : readLink(local.configRoot);
|
|
2361
|
+
if (link2) await syncClone(local, link2, opts);
|
|
2362
|
+
else await createClone(local, opts);
|
|
2350
2363
|
}
|
|
2351
2364
|
|
|
2352
2365
|
// src/channels.ts
|
|
@@ -2631,6 +2644,35 @@ it, and name its path in the brief.
|
|
|
2631
2644
|
6. Tell the person what was done, which agent did which part, and what it cost
|
|
2632
2645
|
(\`get_task\` reports the task's cost).
|
|
2633
2646
|
|
|
2647
|
+
## Share an existing Library document
|
|
2648
|
+
|
|
2649
|
+
Use \`read_library_file_links\` with the document's exact \`ref\` to read its
|
|
2650
|
+
current revision, supported access and real available members. The reference
|
|
2651
|
+
can name a Drive file (including an uploaded meeting note), task file, memory
|
|
2652
|
+
note, skill file, agent user instructions (\`{kind:"agent_prompt",agentId}\`), saved artifact or platform artifact. Only the user-authored AGENTS.md is shared; system instructions and credentials are never included. \`workspace\` names the
|
|
2653
|
+
audience workspace, never a storage move or the connection's default.
|
|
2654
|
+
\`share_library_file\` changes only the requested workspace/member grants;
|
|
2655
|
+
\`publish_library_file\` enables or disables a public view-only link while
|
|
2656
|
+
preserving other grants. Both read first and merge against the current
|
|
2657
|
+
revision. \`revoke_library_file_links\` needs the explicit revision you reviewed.
|
|
2658
|
+
A conflict is an error: read the current state before intentionally retrying.
|
|
2659
|
+
These tools send no messages and start no sessions. CLI equivalents are
|
|
2660
|
+
\`sky library links show\`, \`share\`, \`publish\` and \`revoke\`; their help
|
|
2661
|
+
shows every reference shape. Existing artifact links and version pins keep
|
|
2662
|
+
using their existing controls and \`share_artifact\`.
|
|
2663
|
+
|
|
2664
|
+
When you receive a Library shared/public URL, \`read_shared_library_file\`
|
|
2665
|
+
accepts the 43-character token at its end; \`public: true\` chooses the public
|
|
2666
|
+
view-only reader. A recipient with a live edit grant can use
|
|
2667
|
+
\`save_shared_library_file\` with the opaque source revision from that read
|
|
2668
|
+
and exactly one native text/HTML/Office/restore field. Public links have no
|
|
2669
|
+
writer. The CLI equivalents are \`sky library links read --token \u2026\`
|
|
2670
|
+
(\`--output\` streams original bytes) and \`sky library links save --token \u2026
|
|
2671
|
+
--revision \u2026 --file \u2026\`; \`--data\` accepts the other native save fields.
|
|
2672
|
+
A 403 is not a saved result; a 409 requires reading current state before an
|
|
2673
|
+
intentional retry. This source revision is distinct from the integer revision
|
|
2674
|
+
of the sharing settings.
|
|
2675
|
+
|
|
2634
2676
|
## Build an agent that the person's own application runs
|
|
2635
2677
|
|
|
2636
2678
|
When the person wants an agent their product starts for its customers, built
|
|
@@ -5092,41 +5134,43 @@ async function listBrowserProfiles() {
|
|
|
5092
5134
|
const { data } = await api("GET", "/browser_profiles");
|
|
5093
5135
|
return data;
|
|
5094
5136
|
}
|
|
5095
|
-
async function getBrowserProfile(scope) {
|
|
5096
|
-
const profiles = await listBrowserProfiles();
|
|
5097
|
-
const profile = profiles.find((entry2) => entry2.scope === scope);
|
|
5098
|
-
if (!profile) throw new ApiError(404, "not_found", "browser profile not found");
|
|
5099
|
-
return profile;
|
|
5100
|
-
}
|
|
5101
5137
|
async function importBrowserProfileState(scope, state) {
|
|
5102
5138
|
return api("POST", `/browser_profiles/${encodeURIComponent(scope)}/imports`, state);
|
|
5103
5139
|
}
|
|
5104
|
-
async function listBrowserProfileSites(
|
|
5105
|
-
const
|
|
5106
|
-
return
|
|
5140
|
+
async function listBrowserProfileSites() {
|
|
5141
|
+
const profiles = await listBrowserProfiles();
|
|
5142
|
+
return (profiles.find((entry2) => entry2.scope === "mine") ?? profiles.find((entry2) => entry2.scope === "workspace"))?.sites ?? [];
|
|
5107
5143
|
}
|
|
5108
|
-
async function
|
|
5109
|
-
const sites = await listBrowserProfileSites(
|
|
5144
|
+
async function removeBrowserProfileSite(site) {
|
|
5145
|
+
const sites = await listBrowserProfileSites();
|
|
5110
5146
|
const row = sites.find((entry2) => entry2.site === site) ?? sites.find((entry2) => entry2.site_id === site);
|
|
5111
|
-
if (!row) throw new ApiError(404, "not_found", "site not found in
|
|
5112
|
-
|
|
5147
|
+
if (!row) throw new ApiError(404, "not_found", "site not found in your browser");
|
|
5148
|
+
await api("DELETE", `/browser_profiles/mine/sites/${row.site_id ? `${encodeURIComponent(row.site_id)}?by=id` : encodeURIComponent(row.site)}`);
|
|
5113
5149
|
}
|
|
5114
|
-
async function
|
|
5115
|
-
await api("
|
|
5150
|
+
async function shareBrowserProfile(member, sites) {
|
|
5151
|
+
const { share } = await api("POST", "/browser_profiles/mine/shares", {
|
|
5152
|
+
grantee: member ? { user_id: member } : "workspace",
|
|
5153
|
+
sites: sites.length ? sites : null
|
|
5154
|
+
});
|
|
5155
|
+
return share;
|
|
5116
5156
|
}
|
|
5117
|
-
async function
|
|
5118
|
-
|
|
5119
|
-
const [identifier, query] = reference.split("?");
|
|
5120
|
-
return api("POST", `/browser_profiles/${encodeURIComponent(scope)}/sites/${identifier}/move${query ? `?${query}` : ""}`, { to });
|
|
5157
|
+
async function unshareBrowserProfile(shareId) {
|
|
5158
|
+
await api("DELETE", `/browser_profiles/mine/shares/${encodeURIComponent(shareId)}`);
|
|
5121
5159
|
}
|
|
5122
5160
|
function renderBrowserProfileSite(site) {
|
|
5123
5161
|
const who = site.signed_in_by?.email ?? site.signed_in_by?.name ?? "unknown";
|
|
5124
5162
|
const used = site.last_used_at ? `used ${site.last_used_at.slice(0, 10)}` : "never used";
|
|
5125
5163
|
return `${site.site.padEnd(40)} ${site.status.padEnd(14)} ${who} ${used}${site.status === "removing" && site.site_id ? ` retry with site ID ${site.site_id}` : ""}`;
|
|
5126
5164
|
}
|
|
5165
|
+
function renderBrowserProfileShare(share) {
|
|
5166
|
+
const who = share.grantee.type === "workspace" ? "everyone in this workspace" : share.grantee.user.email ?? share.grantee.user.name ?? share.grantee.user.id;
|
|
5167
|
+
return `shared with ${who}: ${share.sites ? share.sites.join(", ") : "every site"} (share ${share.id})`;
|
|
5168
|
+
}
|
|
5127
5169
|
function renderBrowserProfile(profile) {
|
|
5128
|
-
const
|
|
5170
|
+
const title = profile.scope === "mine" ? "your browser" : profile.scope === "shared" ? `shared with you by ${profile.owner?.email ?? profile.owner?.name ?? profile.owner?.id ?? "someone"}` : "shared with this workspace";
|
|
5171
|
+
const lines = [`${title} revision: ${profile.revision} sites: ${profile.sites.length}`];
|
|
5129
5172
|
for (const site of profile.sites) lines.push(` ${renderBrowserProfileSite(site)}`);
|
|
5173
|
+
for (const share of profile.shares ?? []) lines.push(` ${renderBrowserProfileShare(share)}`);
|
|
5130
5174
|
return lines.join("\n");
|
|
5131
5175
|
}
|
|
5132
5176
|
|
|
@@ -5248,42 +5292,10 @@ function renderThreadSummary(t) {
|
|
|
5248
5292
|
}
|
|
5249
5293
|
|
|
5250
5294
|
// src/library.ts
|
|
5251
|
-
|
|
5252
|
-
async function listLibrary(query = {}) {
|
|
5253
|
-
const params = new URLSearchParams(Object.entries(query).filter((entry2) => Boolean(entry2[1])));
|
|
5254
|
-
const qs = params.size ? `?${params}` : "";
|
|
5255
|
-
return api("GET", `/library/artifacts${qs}`);
|
|
5256
|
-
}
|
|
5257
|
-
async function uploadToLibrary(file, requestId, workspace2) {
|
|
5258
|
-
const form = new FormData();
|
|
5259
|
-
form.append("file", new Blob([new Uint8Array(file.bytes)], { type: file.mediaType }), file.name);
|
|
5260
|
-
form.append("requestId", requestId);
|
|
5261
|
-
if (workspace2) form.append("workspace", workspace2);
|
|
5262
|
-
return apiForm("POST", "/library/artifacts", form);
|
|
5263
|
-
}
|
|
5264
|
-
async function getLibraryArtifact(id) {
|
|
5265
|
-
return api("GET", artifactPath(id));
|
|
5266
|
-
}
|
|
5267
|
-
async function deleteLibraryArtifact(id, revision) {
|
|
5268
|
-
return api("DELETE", artifactPath(id), { revision });
|
|
5269
|
-
}
|
|
5270
|
-
async function shareLibraryArtifact(id, enabled) {
|
|
5271
|
-
const { currentVersion } = await getLibraryArtifact(id);
|
|
5272
|
-
return api("PATCH", `${artifactPath(id)}/share`, { enabled, revision: currentVersion });
|
|
5273
|
-
}
|
|
5274
|
-
async function placeLibraryArtifact(id, workspace2) {
|
|
5275
|
-
return api("PATCH", `${artifactPath(id)}/workspace`, { workspace: workspace2 });
|
|
5276
|
-
}
|
|
5277
|
-
async function downloadLibraryArtifact(id, version) {
|
|
5278
|
-
return apiBytes("GET", `${artifactPath(id)}/download${version ? `?version=${version}` : ""}`);
|
|
5279
|
-
}
|
|
5280
|
-
function renderLibraryArtifact(artifact2) {
|
|
5281
|
-
const version = artifact2.currentVersion ? `v${artifact2.currentVersion}` : "";
|
|
5282
|
-
return `${artifact2.id} ${artifact2.name} ${version}${artifact2.shared ? " shared" : ""}`;
|
|
5283
|
-
}
|
|
5295
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
5284
5296
|
|
|
5285
5297
|
// src/api-command.ts
|
|
5286
|
-
import { readFileSync as readFileSync16
|
|
5298
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
5287
5299
|
var API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
5288
5300
|
function readData(data, stdin = () => readFileSync16(0, "utf8")) {
|
|
5289
5301
|
if (data === void 0) return void 0;
|
|
@@ -5304,9 +5316,9 @@ function requestPath(path, query = []) {
|
|
|
5304
5316
|
if (at <= 0) throw new Error(`--query needs key=value, got "${pair}"`);
|
|
5305
5317
|
params.append(pair.slice(0, at), pair.slice(at + 1));
|
|
5306
5318
|
}
|
|
5307
|
-
const
|
|
5319
|
+
const sep4 = clean.includes("?") ? "&" : "?";
|
|
5308
5320
|
const encoded = params.toString();
|
|
5309
|
-
return encoded ? `${clean}${
|
|
5321
|
+
return encoded ? `${clean}${sep4}${encoded}` : clean;
|
|
5310
5322
|
}
|
|
5311
5323
|
function matchOperations(doc, words) {
|
|
5312
5324
|
const needles = words.map((w) => w.toLowerCase());
|
|
@@ -5338,7 +5350,7 @@ async function callApi(method, path, opts) {
|
|
|
5338
5350
|
const res = await apiRaw(verb, requestPath(path, opts.query), { body, idempotencyKey: opts.idempotencyKey, universe: opts.workspace });
|
|
5339
5351
|
const type = res.headers.get("content-type") ?? "";
|
|
5340
5352
|
if (opts.output) {
|
|
5341
|
-
|
|
5353
|
+
await writeResponseFile(res, opts.output);
|
|
5342
5354
|
console.error(`${res.status} \u2192 ${opts.output}`);
|
|
5343
5355
|
} else if (/json/.test(type)) {
|
|
5344
5356
|
const text = await res.text();
|
|
@@ -5356,6 +5368,239 @@ async function callApi(method, path, opts) {
|
|
|
5356
5368
|
if (!res.ok && res.status !== 304) process.exitCode = 1;
|
|
5357
5369
|
}
|
|
5358
5370
|
|
|
5371
|
+
// src/library.ts
|
|
5372
|
+
var artifactPath = (id) => `/library/artifacts/${encodeURIComponent(id)}`;
|
|
5373
|
+
async function listLibrary(query = {}) {
|
|
5374
|
+
const params = new URLSearchParams(Object.entries(query).filter((entry2) => Boolean(entry2[1])));
|
|
5375
|
+
const qs = params.size ? `?${params}` : "";
|
|
5376
|
+
return api("GET", `/library/artifacts${qs}`);
|
|
5377
|
+
}
|
|
5378
|
+
async function uploadToLibrary(file, requestId, workspace2) {
|
|
5379
|
+
const form = new FormData();
|
|
5380
|
+
form.append("file", new Blob([new Uint8Array(file.bytes)], { type: file.mediaType }), file.name);
|
|
5381
|
+
form.append("requestId", requestId);
|
|
5382
|
+
if (workspace2) form.append("workspace", workspace2);
|
|
5383
|
+
return apiForm("POST", "/library/artifacts", form);
|
|
5384
|
+
}
|
|
5385
|
+
async function getLibraryArtifact(id) {
|
|
5386
|
+
return api("GET", artifactPath(id));
|
|
5387
|
+
}
|
|
5388
|
+
async function deleteLibraryArtifact(id, revision) {
|
|
5389
|
+
return api("DELETE", artifactPath(id), { revision });
|
|
5390
|
+
}
|
|
5391
|
+
async function shareLibraryArtifact(id, enabled) {
|
|
5392
|
+
const { currentVersion } = await getLibraryArtifact(id);
|
|
5393
|
+
return api("PATCH", `${artifactPath(id)}/share`, { enabled, revision: currentVersion });
|
|
5394
|
+
}
|
|
5395
|
+
async function placeLibraryArtifact(id, workspace2) {
|
|
5396
|
+
return api("PATCH", `${artifactPath(id)}/workspace`, { workspace: workspace2 });
|
|
5397
|
+
}
|
|
5398
|
+
async function downloadLibraryArtifact(id, version) {
|
|
5399
|
+
return apiBytes("GET", `${artifactPath(id)}/download${version ? `?version=${version}` : ""}`);
|
|
5400
|
+
}
|
|
5401
|
+
function renderLibraryArtifact(artifact2) {
|
|
5402
|
+
const version = artifact2.currentVersion ? `v${artifact2.currentVersion}` : "";
|
|
5403
|
+
return `${artifact2.id} ${artifact2.name} ${version}${artifact2.shared ? " shared" : ""}`;
|
|
5404
|
+
}
|
|
5405
|
+
var libraryFileRefSchema = external_exports.discriminatedUnion("kind", [
|
|
5406
|
+
external_exports.object({ kind: external_exports.literal("agent_prompt"), agentId: external_exports.string().min(1).max(200).regex(/^[a-zA-Z0-9_-]+$/) }).strict(),
|
|
5407
|
+
external_exports.object({ kind: external_exports.literal("file"), fileId: external_exports.string().min(1), sessionId: external_exports.string().min(1).optional() }).strict(),
|
|
5408
|
+
external_exports.object({ kind: external_exports.literal("task_file"), taskId: external_exports.string().min(1), path: external_exports.string().min(1) }).strict(),
|
|
5409
|
+
external_exports.object({ kind: external_exports.literal("memory_note"), storeId: external_exports.string().min(1), memoryId: external_exports.string().min(1) }).strict(),
|
|
5410
|
+
external_exports.object({ kind: external_exports.literal("skill_file"), skillId: external_exports.string().min(1), path: external_exports.string().min(1) }).strict(),
|
|
5411
|
+
external_exports.object({ kind: external_exports.literal("artifact"), artifactId: external_exports.string().min(1) }).strict(),
|
|
5412
|
+
external_exports.object({ kind: external_exports.literal("platform_artifact"), artifactId: external_exports.string().min(1), path: external_exports.string().min(1).optional() }).strict()
|
|
5413
|
+
]);
|
|
5414
|
+
var linkAccessSchema = external_exports.enum(["none", "view", "edit"]);
|
|
5415
|
+
var linkMemberSchema = external_exports.object({ userId: external_exports.string().min(1), access: external_exports.enum(["view", "edit"]) });
|
|
5416
|
+
var linkRevisionSchema = external_exports.number().int().nonnegative();
|
|
5417
|
+
var libraryLinksSchema = external_exports.object({
|
|
5418
|
+
link: external_exports.object({
|
|
5419
|
+
id: external_exports.string().nullable(),
|
|
5420
|
+
ref: libraryFileRefSchema,
|
|
5421
|
+
workspace: external_exports.string().nullable(),
|
|
5422
|
+
revision: linkRevisionSchema,
|
|
5423
|
+
editable: external_exports.boolean(),
|
|
5424
|
+
workspaceAccess: linkAccessSchema,
|
|
5425
|
+
members: external_exports.array(linkMemberSchema),
|
|
5426
|
+
publicEnabled: external_exports.boolean(),
|
|
5427
|
+
sharedUrl: external_exports.string().nullable(),
|
|
5428
|
+
publicUrl: external_exports.string().nullable()
|
|
5429
|
+
}),
|
|
5430
|
+
availableMembers: external_exports.array(external_exports.object({ userId: external_exports.string(), name: external_exports.string().nullable(), email: external_exports.string().nullable() }))
|
|
5431
|
+
});
|
|
5432
|
+
function parseLibraryFileRef(value) {
|
|
5433
|
+
return libraryFileRefSchema.parse(JSON.parse(value));
|
|
5434
|
+
}
|
|
5435
|
+
async function getLibraryLinks(ref, workspace2) {
|
|
5436
|
+
const params = new URLSearchParams({ ref: JSON.stringify(libraryFileRefSchema.parse(ref)), ...workspace2 ? { workspace: workspace2 } : {} });
|
|
5437
|
+
const result = libraryLinksSchema.parse(await api("GET", `/library/links?${params}`));
|
|
5438
|
+
if (JSON.stringify(result.link.ref) !== JSON.stringify(libraryFileRefSchema.parse(ref)) || workspace2 !== void 0 && result.link.workspace !== workspace2) throw new Error("The API returned sharing for a different source. No access was changed.");
|
|
5439
|
+
return result;
|
|
5440
|
+
}
|
|
5441
|
+
async function updateLibraryLinks(ref, change) {
|
|
5442
|
+
const current = await getLibraryLinks(ref, change.workspace);
|
|
5443
|
+
if (change.revision !== void 0 && linkRevisionSchema.parse(change.revision) !== current.link.revision) throw new Error("Sharing changed since that revision. Read the current links and retry.");
|
|
5444
|
+
const members = new Map(current.link.members.map((member) => [member.userId, member]));
|
|
5445
|
+
for (const member of change.memberChanges ?? []) {
|
|
5446
|
+
const userId = external_exports.string().min(1).parse(member.userId);
|
|
5447
|
+
const access = linkAccessSchema.parse(member.access);
|
|
5448
|
+
if (access === "none") members.delete(userId);
|
|
5449
|
+
else members.set(userId, { userId, access });
|
|
5450
|
+
}
|
|
5451
|
+
return libraryLinksSchema.parse(await api("PUT", "/library/links", {
|
|
5452
|
+
ref,
|
|
5453
|
+
workspace: current.link.workspace,
|
|
5454
|
+
revision: current.link.revision,
|
|
5455
|
+
workspaceAccess: change.workspaceAccess === void 0 ? current.link.workspaceAccess : linkAccessSchema.parse(change.workspaceAccess),
|
|
5456
|
+
members: [...members.values()],
|
|
5457
|
+
publicEnabled: change.publicEnabled === void 0 ? current.link.publicEnabled : external_exports.boolean().parse(change.publicEnabled)
|
|
5458
|
+
}));
|
|
5459
|
+
}
|
|
5460
|
+
async function revokeLibraryLinks(ref, revision, workspace2) {
|
|
5461
|
+
return libraryLinksSchema.parse(await api("DELETE", "/library/links", {
|
|
5462
|
+
ref: libraryFileRefSchema.parse(ref),
|
|
5463
|
+
revision: linkRevisionSchema.parse(revision),
|
|
5464
|
+
...workspace2 ? { workspace: workspace2 } : {}
|
|
5465
|
+
}));
|
|
5466
|
+
}
|
|
5467
|
+
var sharedLibraryToken = external_exports.string().length(43).regex(/^[A-Za-z0-9_-]{43}$/, "use the 43-character token from the Library shared or public URL");
|
|
5468
|
+
var sharedLibrarySource = external_exports.object({
|
|
5469
|
+
name: external_exports.string(),
|
|
5470
|
+
format: external_exports.enum(["file", "site"]),
|
|
5471
|
+
mediaType: external_exports.string(),
|
|
5472
|
+
revision: external_exports.string(),
|
|
5473
|
+
editable: external_exports.boolean(),
|
|
5474
|
+
url: external_exports.string(),
|
|
5475
|
+
maxBytes: external_exports.number(),
|
|
5476
|
+
history: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown()))
|
|
5477
|
+
}).passthrough();
|
|
5478
|
+
var sharedLibrarySaved = external_exports.object({ url: external_exports.string(), revision: external_exports.string().min(1), publishedAt: external_exports.string().datetime({ offset: true }), text: external_exports.string().nullable().optional(), historyRevision: external_exports.string().optional() });
|
|
5479
|
+
var sharedLibrarySaveSchema = external_exports.object({
|
|
5480
|
+
revision: external_exports.string().min(1),
|
|
5481
|
+
text: external_exports.string().optional(),
|
|
5482
|
+
html: external_exports.string().optional(),
|
|
5483
|
+
office: external_exports.string().optional(),
|
|
5484
|
+
docx: external_exports.string().optional(),
|
|
5485
|
+
restoreRevision: external_exports.string().min(1).optional(),
|
|
5486
|
+
requestId: external_exports.string().uuid().optional()
|
|
5487
|
+
}).strict().refine((body) => [body.text, body.html, body.office, body.docx, body.restoreRevision].filter((field) => field !== void 0).length === 1, "provide exactly one of text, html, office, docx or restoreRevision");
|
|
5488
|
+
async function readSharedLibraryFile(token, options = {}) {
|
|
5489
|
+
const path = `/library/${options.public ? "public" : "shared"}/${sharedLibraryToken.parse(token)}`;
|
|
5490
|
+
if (!options.output) return sharedLibrarySource.parse(await api("GET", path, void 0, { auth: options.public !== true }));
|
|
5491
|
+
const response = await apiRaw("GET", `${path}?raw=1`, { auth: options.public !== true });
|
|
5492
|
+
if (!response.ok) {
|
|
5493
|
+
const body = await response.json().catch(() => null);
|
|
5494
|
+
throw new ApiError(response.status, body?.error?.code ?? "http_error", body?.error?.message ?? `Download failed (${response.status}).`);
|
|
5495
|
+
}
|
|
5496
|
+
return { output: options.output, bytes: await writeResponseFile(response, options.output) };
|
|
5497
|
+
}
|
|
5498
|
+
async function saveSharedLibraryFile(token, body) {
|
|
5499
|
+
return sharedLibrarySaved.parse(await api("PUT", `/library/shared/${sharedLibraryToken.parse(token)}`, sharedLibrarySaveSchema.parse(body)));
|
|
5500
|
+
}
|
|
5501
|
+
function registerLibraryLinkCommands(library2) {
|
|
5502
|
+
const libraryLinks = library2.command("links").description("share or publish any Library file; --workspace selects the sharing audience without moving the file");
|
|
5503
|
+
libraryLinks.command("read").description("read a shared native document snapshot; --public reads a public view-only link").requiredOption("--token <token>", "43-character token from a Library shared or public URL").option("--public", "use the public reader without sending your API key").option("--output <file>", "stream the original bytes to this file instead of printing the JSON snapshot").action(async (opts) => {
|
|
5504
|
+
console.log(JSON.stringify(await readSharedLibraryFile(opts.token, opts), null, 2));
|
|
5505
|
+
});
|
|
5506
|
+
libraryLinks.command("save").description("save through a live recipient edit grant; --revision is the opaque source revision from links read").requiredOption("--token <token>", "43-character token from the authenticated shared URL (public URLs have no writer)").requiredOption("--revision <revision>", "native source revision from links read; stale revisions fail without retry").option("--file <path>", "UTF-8 text file to save").option("--data <json>", "instead of --file, JSON/ @file / - containing exactly one text/html/office/docx/restoreRevision and optional requestId").action(async (opts) => {
|
|
5507
|
+
if (opts.file !== void 0 === (opts.data !== void 0)) throw new Error("Choose exactly one of --file or --data.");
|
|
5508
|
+
const body = opts.file !== void 0 ? { text: new TextDecoder("utf-8", { fatal: true }).decode(readFileSync17(opts.file)) } : JSON.parse(readData(opts.data));
|
|
5509
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("--data must be a JSON object with one native save field.");
|
|
5510
|
+
if ("revision" in body && body.revision !== opts.revision) throw new Error("--data revision must match --revision.");
|
|
5511
|
+
console.log(JSON.stringify(await saveSharedLibraryFile(opts.token, sharedLibrarySaveSchema.parse({ ...body, revision: opts.revision })), null, 2));
|
|
5512
|
+
});
|
|
5513
|
+
const linkRefOption = (command) => command.requiredOption("--ref <json>", 'file reference JSON, e.g. {"kind":"file","fileId":"file_\u2026"}').option("--workspace <slug>", "the team audience for this link (omit for private/public-only sharing)");
|
|
5514
|
+
const linkRevision = (value) => value === void 0 ? void 0 : linkRevisionSchema.parse(Number(external_exports.string().regex(/^\d+$/, "revision must be a nonnegative integer").parse(value)));
|
|
5515
|
+
const printLibraryLinks = (value) => console.log(JSON.stringify(value, null, 2));
|
|
5516
|
+
linkRefOption(libraryLinks.command("show").description("read sharing, real workspace members and the current revision without creating a link")).action(async (opts) => printLibraryLinks(await getLibraryLinks(parseLibraryFileRef(opts.ref), opts.workspace)));
|
|
5517
|
+
linkRefOption(libraryLinks.command("share").description("change workspace or specific members' access while preserving every unmentioned grant")).option("--workspace-access <access>", "none, view or edit (edit requires an editable source)").option("--member <user:access>", "grant a member view or edit; repeat to update several members", (value, previous) => [...previous, value], []).option("--remove-member <user>", "clear a member override; workspace access still applies", (value, previous) => [...previous, value], []).option("--revision <n>", "refuse if sharing changed since this reviewed revision").action(async (opts) => {
|
|
5518
|
+
if (opts.workspaceAccess === void 0 && !opts.member?.length && !opts.removeMember?.length) throw new Error("Choose --workspace-access, --member or --remove-member; links show reads without changing access.");
|
|
5519
|
+
const memberChanges = (opts.member ?? []).map((value) => {
|
|
5520
|
+
const colon = value.lastIndexOf(":");
|
|
5521
|
+
if (colon < 1) throw new Error("--member must be a user id followed by :view or :edit");
|
|
5522
|
+
return { userId: value.slice(0, colon), access: external_exports.enum(["view", "edit"]).parse(value.slice(colon + 1)) };
|
|
5523
|
+
});
|
|
5524
|
+
memberChanges.push(...(opts.removeMember ?? []).map((userId) => ({ userId, access: "none" })));
|
|
5525
|
+
printLibraryLinks(await updateLibraryLinks(parseLibraryFileRef(opts.ref), {
|
|
5526
|
+
workspace: opts.workspace,
|
|
5527
|
+
revision: linkRevision(opts.revision),
|
|
5528
|
+
memberChanges,
|
|
5529
|
+
workspaceAccess: opts.workspaceAccess === void 0 ? void 0 : external_exports.enum(["none", "view", "edit"]).parse(opts.workspaceAccess)
|
|
5530
|
+
}));
|
|
5531
|
+
});
|
|
5532
|
+
linkRefOption(libraryLinks.command("publish").description("create a public view-only link; --disable revokes only public access and keeps other grants")).option("--disable", "turn off public viewing").option("--revision <n>", "refuse if sharing changed since this reviewed revision").action(async (opts) => printLibraryLinks(await updateLibraryLinks(parseLibraryFileRef(opts.ref), { workspace: opts.workspace, revision: linkRevision(opts.revision), publicEnabled: !opts.disable })));
|
|
5533
|
+
linkRefOption(libraryLinks.command("revoke").description("stop all sharing at an explicitly reviewed revision")).requiredOption("--revision <n>", "current revision from links show; stale revisions fail without retry").action(async (opts) => printLibraryLinks(await revokeLibraryLinks(parseLibraryFileRef(opts.ref), linkRevision(opts.revision), opts.workspace)));
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5536
|
+
// src/design-systems.ts
|
|
5537
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync18, statSync as statSync9 } from "fs";
|
|
5538
|
+
import { join as join18, relative as relative5, sep as sep3 } from "path";
|
|
5539
|
+
var TEXT2 = /\.(mdx?|markdown|txt|json|m?js|cjs|jsx|tsx?|css|html?|svg|xml|csv|ya?ml|toml)$/i;
|
|
5540
|
+
var MEDIA = {
|
|
5541
|
+
woff2: "font/woff2",
|
|
5542
|
+
woff: "font/woff",
|
|
5543
|
+
ttf: "font/ttf",
|
|
5544
|
+
otf: "font/otf",
|
|
5545
|
+
png: "image/png",
|
|
5546
|
+
jpg: "image/jpeg",
|
|
5547
|
+
jpeg: "image/jpeg",
|
|
5548
|
+
gif: "image/gif",
|
|
5549
|
+
webp: "image/webp",
|
|
5550
|
+
pdf: "application/pdf",
|
|
5551
|
+
mp4: "video/mp4",
|
|
5552
|
+
webm: "video/webm"
|
|
5553
|
+
};
|
|
5554
|
+
async function listDesignSystems() {
|
|
5555
|
+
return (await api("GET", "/artifacts?kind=design_system")).artifacts;
|
|
5556
|
+
}
|
|
5557
|
+
async function getDesignSystem(id) {
|
|
5558
|
+
return (await api("GET", `/artifacts/${encodeURIComponent(id)}/design-system`)).design_system;
|
|
5559
|
+
}
|
|
5560
|
+
async function importDesignSystem(task2, files2, title) {
|
|
5561
|
+
return (await api("POST", "/artifacts/design-systems", { task_id: task2, files: files2, ...title ? { title } : {} })).artifact;
|
|
5562
|
+
}
|
|
5563
|
+
async function saveDesignTokens(id, baseVersion, tokens, note) {
|
|
5564
|
+
return (await api("PUT", `/artifacts/${encodeURIComponent(id)}/design-system/tokens`, {
|
|
5565
|
+
base_version: baseVersion,
|
|
5566
|
+
tokens,
|
|
5567
|
+
...note ? { note } : {}
|
|
5568
|
+
})).artifact;
|
|
5569
|
+
}
|
|
5570
|
+
function fileTableFrom(dir) {
|
|
5571
|
+
const files2 = {};
|
|
5572
|
+
const walk = (current) => {
|
|
5573
|
+
for (const name of readdirSync8(current)) {
|
|
5574
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
5575
|
+
const path = join18(current, name);
|
|
5576
|
+
if (statSync9(path).isDirectory()) {
|
|
5577
|
+
walk(path);
|
|
5578
|
+
continue;
|
|
5579
|
+
}
|
|
5580
|
+
const key = relative5(dir, path).split(sep3).join("/");
|
|
5581
|
+
const bytes = readFileSync18(path);
|
|
5582
|
+
files2[key] = TEXT2.test(name) ? bytes.toString("utf8") : `data:${MEDIA[name.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream"};base64,${bytes.toString("base64")}`;
|
|
5583
|
+
}
|
|
5584
|
+
};
|
|
5585
|
+
walk(dir);
|
|
5586
|
+
return files2;
|
|
5587
|
+
}
|
|
5588
|
+
function renderDesignSystem(system) {
|
|
5589
|
+
const tokens = system.tokens;
|
|
5590
|
+
const lines = [
|
|
5591
|
+
`${system.artifact_id} ${system.title} v${system.version}${system.editable ? "" : " read-only"}`,
|
|
5592
|
+
tokens ? `themes ${tokens.themes.map((theme) => theme.id).join(", ")} \xB7 ${tokens.colors.length} colors \xB7 ${tokens.typeGroups.reduce((sum, group) => sum + group.styles.length, 0)} type styles \xB7 ${tokens.spacing.length} spacing \xB7 ${tokens.radius.length} radius` : "no tokens.json",
|
|
5593
|
+
`components: ${system.components.map((component) => component.name).join(", ") || "none"}`,
|
|
5594
|
+
`assets: ${system.assets.map((group) => `${group.name} (${group.files.length})`).join(", ") || "none"}`
|
|
5595
|
+
];
|
|
5596
|
+
if (system.last_change) lines.push(`last change: ${system.last_change.by}${system.last_change.via ? ` via ${system.last_change.via}` : ""} at ${system.last_change.at}`);
|
|
5597
|
+
for (const token of tokens?.dropped ?? []) lines.push(`not read: ${token.family} ${token.name || "(unnamed)"}: ${token.reason}`);
|
|
5598
|
+
return lines.join("\n");
|
|
5599
|
+
}
|
|
5600
|
+
async function designComments(id, input) {
|
|
5601
|
+
return api(input ? "POST" : "GET", `/artifacts/${encodeURIComponent(id)}/design-system/comments`, input);
|
|
5602
|
+
}
|
|
5603
|
+
|
|
5359
5604
|
// src/billing-plans.ts
|
|
5360
5605
|
var readCodingPlan = () => api("GET", "/billing/coding-plan");
|
|
5361
5606
|
function joinCodingPlan(dollars2, requestId) {
|
|
@@ -5442,11 +5687,11 @@ function workspaceFlagIsCommands(argv, commandPath) {
|
|
|
5442
5687
|
}
|
|
5443
5688
|
|
|
5444
5689
|
// src/harness.ts
|
|
5445
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as
|
|
5446
|
-
import { dirname as dirname9, join as
|
|
5690
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync19, writeFileSync as writeFileSync9 } from "fs";
|
|
5691
|
+
import { dirname as dirname9, join as join19, resolve as resolve8 } from "path";
|
|
5447
5692
|
var HARNESS_FILE = "harness.json";
|
|
5448
5693
|
function harnessFilePath(dir) {
|
|
5449
|
-
return
|
|
5694
|
+
return join19(resolve8(dir), HARNESS_FILE);
|
|
5450
5695
|
}
|
|
5451
5696
|
function readHarnessFile(dir) {
|
|
5452
5697
|
const path = harnessFilePath(dir);
|
|
@@ -5454,7 +5699,7 @@ function readHarnessFile(dir) {
|
|
|
5454
5699
|
throw new Error(`no ${HARNESS_FILE} found in ${resolve8(dir)} \u2014 run \`sky harness init\` first`);
|
|
5455
5700
|
}
|
|
5456
5701
|
try {
|
|
5457
|
-
return JSON.parse(
|
|
5702
|
+
return JSON.parse(readFileSync19(path, "utf8"));
|
|
5458
5703
|
} catch (err) {
|
|
5459
5704
|
throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5460
5705
|
}
|
|
@@ -5485,7 +5730,7 @@ function harnessInit(dir) {
|
|
|
5485
5730
|
throw new Error(`${path} already exists \u2014 delete it to re-init`);
|
|
5486
5731
|
}
|
|
5487
5732
|
mkdirSync6(dirname9(path), { recursive: true });
|
|
5488
|
-
|
|
5733
|
+
writeFileSync9(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
|
|
5489
5734
|
`);
|
|
5490
5735
|
console.log(`Created ${path}`);
|
|
5491
5736
|
console.log(INIT_COMMENT);
|
|
@@ -6224,7 +6469,7 @@ sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1
|
|
|
6224
6469
|
run(async (ref, path, opts) => {
|
|
6225
6470
|
const bytes = await readFile(await resolveSessionRef(ref), path);
|
|
6226
6471
|
if (opts.out) {
|
|
6227
|
-
|
|
6472
|
+
writeFileSync10(opts.out, bytes);
|
|
6228
6473
|
console.error(`saved ${bytes.length} bytes to ${opts.out}`);
|
|
6229
6474
|
} else process.stdout.write(bytes);
|
|
6230
6475
|
})
|
|
@@ -6241,7 +6486,7 @@ sessionWorkspace.command("patch <ref> [repo]").description("the work in a reposi
|
|
|
6241
6486
|
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)})`);
|
|
6242
6487
|
if (!patch.length) console.error("nothing changed");
|
|
6243
6488
|
else if (opts.out) {
|
|
6244
|
-
|
|
6489
|
+
writeFileSync10(opts.out, patch);
|
|
6245
6490
|
console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
|
|
6246
6491
|
} else process.stdout.write(patch);
|
|
6247
6492
|
})
|
|
@@ -6746,14 +6991,14 @@ task.command("get <task> <path>").description("read one task file; prints it, or
|
|
|
6746
6991
|
run(async (taskId, path, opts) => {
|
|
6747
6992
|
const bytes = await readTaskFile(taskId, path);
|
|
6748
6993
|
if (opts.out) {
|
|
6749
|
-
|
|
6994
|
+
writeFileSync10(opts.out, bytes);
|
|
6750
6995
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
6751
6996
|
} else process.stdout.write(bytes);
|
|
6752
6997
|
})
|
|
6753
6998
|
);
|
|
6754
6999
|
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(
|
|
6755
7000
|
run(async (taskId, path, local) => {
|
|
6756
|
-
const { file, conflict } = await writeTaskFile(taskId, path,
|
|
7001
|
+
const { file, conflict } = await writeTaskFile(taskId, path, readFileSync20(local), mediaTypeFor(local));
|
|
6757
7002
|
console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
|
|
6758
7003
|
})
|
|
6759
7004
|
);
|
|
@@ -6826,6 +7071,34 @@ task.command("verdict <task> <did-the-job>").description("record whether the tas
|
|
|
6826
7071
|
console.log(`Recorded verdict on task ${updated.id}: did_the_job=${value === "yes"}`);
|
|
6827
7072
|
})
|
|
6828
7073
|
);
|
|
7074
|
+
var plugins = program.command("plugins").description("Claude Code plugins in the workspace: install from GitHub, list, remove");
|
|
7075
|
+
plugins.command("list").description("the workspace's installed plugins and the skills each one installed").option("--json", "print the list as JSON").action(
|
|
7076
|
+
run(async (opts) => {
|
|
7077
|
+
const { plugins: list } = await api("GET", "/plugins");
|
|
7078
|
+
if (opts.json) return void console.log(JSON.stringify(list));
|
|
7079
|
+
if (list.length === 0) return void console.log("No plugins. `sky plugins install github:owner/repo[/path][@ref]`");
|
|
7080
|
+
for (const p of list) console.log(`${p.name} ${p.skills.length} skill${p.skills.length === 1 ? "" : "s"}: ${p.skills.map((s) => s.displayName.split("/").pop()).join(", ")}`);
|
|
7081
|
+
})
|
|
7082
|
+
);
|
|
7083
|
+
plugins.command("install <source>").description("install or update a plugin: github:owner/repo[/path][@ref] or a github.com URL").option("--json", "print the result as JSON").action(
|
|
7084
|
+
run(async (source, opts) => {
|
|
7085
|
+
const result = await api("POST", "/plugins", { source });
|
|
7086
|
+
if (opts.json) return void console.log(JSON.stringify(result));
|
|
7087
|
+
console.log(`Installed ${result.plugin.name}${result.plugin.version ? ` ${result.plugin.version}` : ""} from ${result.plugin.source}: ${result.skills.length} skill(s), on for every new session.`);
|
|
7088
|
+
for (const c of result.connectors) {
|
|
7089
|
+
const state = c.app === null ? "no AgentSky connector for it yet" : c.connected ? `uses your ${c.app} connection` : `connect ${c.app}: ${c.connect_url}`;
|
|
7090
|
+
console.log(` connector ${c.server}: ${state}`);
|
|
7091
|
+
}
|
|
7092
|
+
for (const s of result.skipped) console.log(` skipped ${s.part}: ${s.reason}`);
|
|
7093
|
+
for (const r of result.removed) console.log(` ${r.part}: ${r.result.replace("_", " ")} (no longer in the plugin)`);
|
|
7094
|
+
})
|
|
7095
|
+
);
|
|
7096
|
+
plugins.command("rm <name>").description("remove a plugin's skills (a skill a session still holds is switched off)").action(
|
|
7097
|
+
run(async (name) => {
|
|
7098
|
+
const result = await api("DELETE", `/plugins/${encodeURIComponent(name)}`);
|
|
7099
|
+
console.log(`Removed ${result.name}: ${result.parts.map((p) => `${p.part} ${p.result.replace("_", " ")}`).join(", ")}`);
|
|
7100
|
+
})
|
|
7101
|
+
);
|
|
6829
7102
|
var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent (sky skills attach, or [[skills]] in agent.toml)");
|
|
6830
7103
|
skills.command("upload <dir>").description("upload a skill directory (SKILL.md at its root) as a new skill").option("--name <name>", "display name (default: the directory's name)").option("--json", "print the stored skill as JSON").action(
|
|
6831
7104
|
run(async (dir, opts) => {
|
|
@@ -6917,7 +7190,7 @@ library.command("list").description("your Library, newest first").option("--sear
|
|
|
6917
7190
|
);
|
|
6918
7191
|
library.command("upload <path>").description("save a file from this computer to your Library").option("--workspace <key>", "place it in this workspace").action(
|
|
6919
7192
|
run(async (path, opts) => {
|
|
6920
|
-
const bytes =
|
|
7193
|
+
const bytes = readFileSync20(path);
|
|
6921
7194
|
const { artifact: artifact2 } = await uploadToLibrary({ bytes, name: basename9(path), mediaType: "application/octet-stream" }, `sky-upload-${crypto.randomUUID()}`, opts.workspace);
|
|
6922
7195
|
console.log(renderLibraryArtifact(artifact2));
|
|
6923
7196
|
})
|
|
@@ -6948,10 +7221,11 @@ library.command("place <id> <workspace>").description('place your artifact in a
|
|
|
6948
7221
|
library.command("download <id> <out>").description("save one version's bytes to a file").option("--version <n>", "a saved version (the current one when omitted)").action(
|
|
6949
7222
|
run(async (id, out, opts) => {
|
|
6950
7223
|
const { bytes } = await downloadLibraryArtifact(id, opts.version ? Number(opts.version) : void 0);
|
|
6951
|
-
|
|
7224
|
+
writeFileSync10(out, bytes);
|
|
6952
7225
|
console.log(`wrote ${bytes.length} bytes to ${out}`);
|
|
6953
7226
|
})
|
|
6954
7227
|
);
|
|
7228
|
+
registerLibraryLinkCommands(library);
|
|
6955
7229
|
var artifact = program.command("artifact").description("artifacts: publish a task's files as a named, shareable result");
|
|
6956
7230
|
artifact.command("list").description("the workspace's artifacts, or one task's with --task").option("--task <task>", "only this task's artifacts").action(
|
|
6957
7231
|
run(async (opts) => {
|
|
@@ -6969,7 +7243,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
|
|
|
6969
7243
|
run(async (id, path, opts) => {
|
|
6970
7244
|
const bytes = await readArtifactFile(id, path);
|
|
6971
7245
|
if (opts.out) {
|
|
6972
|
-
|
|
7246
|
+
writeFileSync10(opts.out, bytes);
|
|
6973
7247
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
6974
7248
|
} else process.stdout.write(bytes);
|
|
6975
7249
|
})
|
|
@@ -6986,6 +7260,35 @@ artifact.command("rm <id>").description("delete an artifact").action(
|
|
|
6986
7260
|
console.log(`Deleted ${id}`);
|
|
6987
7261
|
})
|
|
6988
7262
|
);
|
|
7263
|
+
var designSystem = program.command("design-system").description("design systems: brand books, tokens and components agents design with");
|
|
7264
|
+
designSystem.command("list").description("the workspace's design systems").action(
|
|
7265
|
+
run(async () => {
|
|
7266
|
+
const list = await listDesignSystems();
|
|
7267
|
+
if (!list.length) console.log("no design systems");
|
|
7268
|
+
for (const a of list) console.log(renderArtifact(a));
|
|
7269
|
+
})
|
|
7270
|
+
);
|
|
7271
|
+
designSystem.command("show <id>").description("a design system's tokens, components and assets; --readme prints its brand book instead").option("--readme", "print project/README.md").action(
|
|
7272
|
+
run(async (id, opts) => {
|
|
7273
|
+
const system = await getDesignSystem(id);
|
|
7274
|
+
console.log(opts.readme ? system.readme ?? "this design system has no README.md" : renderDesignSystem(system));
|
|
7275
|
+
})
|
|
7276
|
+
);
|
|
7277
|
+
designSystem.command("import <task> <folder>").description("import a local design-system folder (one holding project/, or project/ itself) into a task and publish it").option("--title <title>", "the system's name; defaults to the title in design-system.json").action(
|
|
7278
|
+
run(async (task2, folder, opts) => {
|
|
7279
|
+
console.log(renderArtifact(await importDesignSystem(task2, fileTableFrom(resolve9(folder)), opts.title)));
|
|
7280
|
+
})
|
|
7281
|
+
);
|
|
7282
|
+
designSystem.command("tokens <id> <file>").description("save a tokens.json file as the system's next version; --base is the version you edited").requiredOption("--base <version>", "the version the file was edited from", (value) => Number(value)).option("--note <note>", "one line saying what changed").action(
|
|
7283
|
+
run(async (id, file, opts) => {
|
|
7284
|
+
const tokens = JSON.parse(readFileSync20(resolve9(file), "utf8"));
|
|
7285
|
+
console.log(renderArtifact(await saveDesignTokens(id, opts.base, tokens, opts.note)));
|
|
7286
|
+
})
|
|
7287
|
+
);
|
|
7288
|
+
designSystem.command("comments <id>").description("read design-system comments, or add one with --text and --base").option("--text <text>", "comment to add").option("--target <target>", "overview, color:<name> or component:<name>", "overview").option("--base <version>", "the version you viewed", Number).action(run(async (id, opts) => {
|
|
7289
|
+
if (opts.text !== void 0 && (!Number.isInteger(opts.base) || opts.base < 1)) throw Error("Adding a comment requires --base <version>.");
|
|
7290
|
+
console.log(JSON.stringify(await designComments(id, opts.text === void 0 ? void 0 : { text: opts.text, target: opts.target, base_version: opts.base }), null, 2));
|
|
7291
|
+
}));
|
|
6989
7292
|
function routineLine(r) {
|
|
6990
7293
|
const target = r.target.type === "session" ? `\u2192 ${r.target.session_id}` : `+ ${r.target.agent}`;
|
|
6991
7294
|
const cron = r.schedule ? `${r.schedule.expression} ${r.schedule.timezone}` : "manual-only";
|
|
@@ -7291,7 +7594,7 @@ usageOptions(
|
|
|
7291
7594
|
run(async (opts) => {
|
|
7292
7595
|
const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
|
|
7293
7596
|
if (opts.out) {
|
|
7294
|
-
|
|
7597
|
+
writeFileSync10(opts.out, bytes);
|
|
7295
7598
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
7296
7599
|
} else {
|
|
7297
7600
|
process.stdout.write(bytes);
|
|
@@ -7369,10 +7672,10 @@ memory.command("save <text...>").description("add an entry").action(run(async (t
|
|
|
7369
7672
|
memory.command("edit <index> <text...>").description("replace entry <index>").action(run(async (index, text) => writeMemory({ op: "update", index: memoryIndex(index), text: text.join(" ") })));
|
|
7370
7673
|
memory.command("rm <index>").description("remove entry <index>").action(run(async (index) => writeMemory({ op: "remove", index: memoryIndex(index) })));
|
|
7371
7674
|
memory.command("clear").description("remove every entry").action(run(async () => writeMemory({ op: "clear" })));
|
|
7372
|
-
var context = program.command("context").description("search
|
|
7373
|
-
context.command("search <words...>").description("
|
|
7675
|
+
var context = program.command("context").description("search everything you can open in the workspace, and read a hit");
|
|
7676
|
+
context.command("search <words...>").description('plain words, "exact phrase" or /regex/ (every matching line, like grep); follow --cursor for more').option("--scope <folder>", "memory/, chats, traces, outputs/, files/, meetings/, skills/<name>/, agents/ or docs/ (default everything)").option("--limit <n>", "hits per page (default 20)").option("--cursor <cursor>", "continue a previous search").option("--since <iso>", "inclusive lower time bound").option("--until <iso>", "exclusive upper time bound").option("--json", "print the API's answer as JSON").action(
|
|
7374
7677
|
run(async (words, opts) => {
|
|
7375
|
-
const page = await
|
|
7678
|
+
const page = await searchContext(words.join(" "), opts);
|
|
7376
7679
|
console.log(opts.json ? JSON.stringify(page, null, 2) : renderContextPage(page));
|
|
7377
7680
|
})
|
|
7378
7681
|
);
|
|
@@ -7380,7 +7683,7 @@ context.command("read <ref>").description("read a search hit in full: <ref> is t
|
|
|
7380
7683
|
run(async (ref, opts) => {
|
|
7381
7684
|
const params = new URLSearchParams({ ref });
|
|
7382
7685
|
if (opts.offset) params.set("offset", opts.offset);
|
|
7383
|
-
const read = await
|
|
7686
|
+
const read = await contextRequest(`/context/read?${params}`);
|
|
7384
7687
|
console.log(read.text);
|
|
7385
7688
|
if (read.nextOffset !== null) console.error(`more: --offset ${read.nextOffset}`);
|
|
7386
7689
|
})
|
|
@@ -7690,7 +7993,7 @@ mac.command("take-back <mount>").description("end a folder, browser window, scre
|
|
|
7690
7993
|
})
|
|
7691
7994
|
);
|
|
7692
7995
|
var browserProfile = program.command("browser-profile").description("saved-login profiles: the sites your agents stay signed in to");
|
|
7693
|
-
browserProfile.command("list").description("your
|
|
7996
|
+
browserProfile.command("list").description("your browser, with whom you shared it, and the browsers shared with you in this workspace").option("--json", "print as JSON").action(
|
|
7694
7997
|
run(async (opts) => {
|
|
7695
7998
|
const profiles = await listBrowserProfiles();
|
|
7696
7999
|
if (opts.json) {
|
|
@@ -7701,9 +8004,9 @@ browserProfile.command("list").description("your workspace's browser profiles: t
|
|
|
7701
8004
|
for (const p of profiles) console.log(renderBrowserProfile(p));
|
|
7702
8005
|
})
|
|
7703
8006
|
);
|
|
7704
|
-
browserProfile.command("sites
|
|
7705
|
-
run(async (
|
|
7706
|
-
const sites = await listBrowserProfileSites(
|
|
8007
|
+
browserProfile.command("sites").description("list the sites your browser is signed in to").option("--json", "print as JSON").action(
|
|
8008
|
+
run(async (opts) => {
|
|
8009
|
+
const sites = await listBrowserProfileSites();
|
|
7707
8010
|
if (opts.json) {
|
|
7708
8011
|
console.log(JSON.stringify(sites, null, 2));
|
|
7709
8012
|
return;
|
|
@@ -7713,8 +8016,8 @@ browserProfile.command("sites <scope>").description("list the sites in a profile
|
|
|
7713
8016
|
for (const s of sites) console.log(renderBrowserProfileSite(s));
|
|
7714
8017
|
})
|
|
7715
8018
|
);
|
|
7716
|
-
browserProfile.command("import
|
|
7717
|
-
run(async (
|
|
8019
|
+
browserProfile.command("import").description("import saved browser state into your browser; reads Playwright storage state JSON from stdin or --file").option("--file <path>", "path to a Playwright storage state JSON file (reads stdin if omitted)").action(
|
|
8020
|
+
run(async (opts) => {
|
|
7718
8021
|
let raw;
|
|
7719
8022
|
if (opts.file) {
|
|
7720
8023
|
const { readFileSync: rf } = await import("fs");
|
|
@@ -7727,21 +8030,26 @@ browserProfile.command("import <scope>").description("import saved browser state
|
|
|
7727
8030
|
});
|
|
7728
8031
|
}
|
|
7729
8032
|
const state = JSON.parse(raw);
|
|
7730
|
-
const result = await importBrowserProfileState(
|
|
8033
|
+
const result = await importBrowserProfileState("mine", { state });
|
|
7731
8034
|
console.log(`Imported ${result.sites.length} site${result.sites.length === 1 ? "" : "s"}: ${result.sites.join(", ")}`);
|
|
7732
8035
|
})
|
|
7733
8036
|
);
|
|
7734
|
-
browserProfile.command("remove-site <
|
|
7735
|
-
run(async (
|
|
7736
|
-
await removeBrowserProfileSite(
|
|
7737
|
-
console.log(`
|
|
8037
|
+
browserProfile.command("remove-site <site>").description("sign your browser out of a site everywhere (the agent will need to sign in again)").action(
|
|
8038
|
+
run(async (site) => {
|
|
8039
|
+
await removeBrowserProfileSite(site);
|
|
8040
|
+
console.log(`Signed out of ${site}`);
|
|
8041
|
+
})
|
|
8042
|
+
);
|
|
8043
|
+
browserProfile.command("share").description("share your browser in this workspace: with one member (--member <user id>) or everyone; every site unless --sites").option("--member <userId>", "the member to share with (default: everyone in the workspace)").option("--sites <sites>", "comma-separated sites to share (default: every site)").action(
|
|
8044
|
+
run(async (opts) => {
|
|
8045
|
+
const share = await shareBrowserProfile(opts.member, (opts.sites ?? "").split(",").map((s) => s.trim()).filter(Boolean));
|
|
8046
|
+
console.log(renderBrowserProfileShare(share));
|
|
7738
8047
|
})
|
|
7739
8048
|
);
|
|
7740
|
-
browserProfile.command("
|
|
7741
|
-
run(async (
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
console.log(`Moved ${site} from ${scope} to ${result.to}`);
|
|
8049
|
+
browserProfile.command("unshare <shareId>").description("stop a share of your browser; it ends from the grantee's next turn").action(
|
|
8050
|
+
run(async (shareId) => {
|
|
8051
|
+
await unshareBrowserProfile(shareId);
|
|
8052
|
+
console.log(`Stopped share ${shareId}`);
|
|
7745
8053
|
})
|
|
7746
8054
|
);
|
|
7747
8055
|
var notifications = program.command("notifications").description("email and browser push notification preferences for tasks");
|
|
@@ -7801,21 +8109,21 @@ async function helpFetch(path) {
|
|
|
7801
8109
|
return await res.json();
|
|
7802
8110
|
}
|
|
7803
8111
|
var help = program.command("help").description("search the AgentSky help center");
|
|
7804
|
-
help.command("search <query...>").description(
|
|
8112
|
+
help.command("search <query...>").description('search help articles: plain words rank them, "exact" or /regex/ list every matching line').option("--top <n>", "how many results (1\u201320, default 5)", "5").option("--json", "print as JSON").action(
|
|
7805
8113
|
run(async (words, opts) => {
|
|
7806
8114
|
const q = words.join(" ");
|
|
7807
8115
|
const topK = Math.min(Math.max(parseInt(opts.top, 10) || 5, 1), 20);
|
|
7808
|
-
const {
|
|
8116
|
+
const { hits } = await helpFetch(`/api/context/search?${new URLSearchParams({ q, scope: "docs/", limit: String(topK) })}`);
|
|
7809
8117
|
if (opts.json) {
|
|
7810
|
-
console.log(JSON.stringify(
|
|
8118
|
+
console.log(JSON.stringify(hits, null, 2));
|
|
7811
8119
|
return;
|
|
7812
8120
|
}
|
|
7813
|
-
if (!
|
|
8121
|
+
if (!hits.length) {
|
|
7814
8122
|
console.log("no results");
|
|
7815
8123
|
return;
|
|
7816
8124
|
}
|
|
7817
|
-
for (const
|
|
7818
|
-
${
|
|
8125
|
+
for (const hit of hits) console.log(`${String(hit.ref.slug).padEnd(40)} ${hit.title}
|
|
8126
|
+
${hit.snippet.slice(0, 100)}`);
|
|
7819
8127
|
})
|
|
7820
8128
|
);
|
|
7821
8129
|
help.command("get <slug>").description("read a help article by its slug").option("--json", "print as JSON").action(
|
|
@@ -7898,7 +8206,7 @@ notes.command("read <storeId> <noteId>").description("print one note").action(
|
|
|
7898
8206
|
notes.command("save <storeId> <file>").description("write a note from a local file: --path creates one, --note saves a new version of one").option("--path <path>", "path of a new note, e.g. /projects/launch.md").option("--note <noteId>", "the note to update").action(
|
|
7899
8207
|
run(async (storeId, file, opts) => {
|
|
7900
8208
|
if (!opts.path === !opts.note) throw new Error("pass exactly one of --path (new note) or --note (update)");
|
|
7901
|
-
const content =
|
|
8209
|
+
const content = readFileSync20(file, "utf8");
|
|
7902
8210
|
const target = `/memory_stores/${encodeURIComponent(storeId)}/memories${opts.note ? `/${encodeURIComponent(opts.note)}` : ""}`;
|
|
7903
8211
|
const { memory: memory2 } = await api("POST", target, { content, ...opts.path ? { path: opts.path } : {} });
|
|
7904
8212
|
console.log(`Saved ${memory2.path} (${memory2.id})`);
|
|
@@ -7998,7 +8306,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
|
|
|
7998
8306
|
}));
|
|
7999
8307
|
var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
|
|
8000
8308
|
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) => {
|
|
8001
|
-
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-
|
|
8309
|
+
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-HV4E2BPX.js");
|
|
8002
8310
|
if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
|
|
8003
8311
|
const scopes = parseContentScopes(opts.capture);
|
|
8004
8312
|
const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
|
|
@@ -8033,7 +8341,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
|
|
|
8033
8341
|
}));
|
|
8034
8342
|
traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
|
|
8035
8343
|
try {
|
|
8036
|
-
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-
|
|
8344
|
+
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-HV4E2BPX.js");
|
|
8037
8345
|
const chunks = [];
|
|
8038
8346
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
8039
8347
|
const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
@@ -8046,14 +8354,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
|
|
|
8046
8354
|
}
|
|
8047
8355
|
});
|
|
8048
8356
|
traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
|
|
8049
|
-
const { readState, flush, grantSender } = await import("./trace-
|
|
8357
|
+
const { readState, flush, grantSender } = await import("./trace-HV4E2BPX.js");
|
|
8050
8358
|
const state = readState();
|
|
8051
8359
|
if (!state) throw new Error("no trace is running here");
|
|
8052
8360
|
const outcome = await flush(grantSender(state), { attempts: 4 });
|
|
8053
8361
|
console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
|
|
8054
8362
|
}));
|
|
8055
8363
|
traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
|
|
8056
|
-
const { readState, spoolStatus } = await import("./trace-
|
|
8364
|
+
const { readState, spoolStatus } = await import("./trace-HV4E2BPX.js");
|
|
8057
8365
|
const state = readState();
|
|
8058
8366
|
if (!state) {
|
|
8059
8367
|
console.log("No trace is running here.");
|
|
@@ -8063,7 +8371,7 @@ traceCmd.command("status").description("the running trace and its spool").action
|
|
|
8063
8371
|
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`);
|
|
8064
8372
|
}));
|
|
8065
8373
|
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) => {
|
|
8066
|
-
const { readState } = await import("./trace-
|
|
8374
|
+
const { readState } = await import("./trace-HV4E2BPX.js");
|
|
8067
8375
|
const ref = id ?? readState()?.trace_id;
|
|
8068
8376
|
if (!ref) throw new Error("name a trace, or start one here");
|
|
8069
8377
|
const bundle = await api("GET", `/traces/${ref}`);
|
|
@@ -8090,7 +8398,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
|
|
|
8090
8398
|
for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
|
|
8091
8399
|
}));
|
|
8092
8400
|
traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
|
|
8093
|
-
const { readState, record, flush, grantSender, clearState } = await import("./trace-
|
|
8401
|
+
const { readState, record, flush, grantSender, clearState } = await import("./trace-HV4E2BPX.js");
|
|
8094
8402
|
const state = readState();
|
|
8095
8403
|
if (!state) throw new Error("no trace is running here");
|
|
8096
8404
|
if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
|
|
@@ -8100,7 +8408,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
|
|
|
8100
8408
|
console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
|
|
8101
8409
|
}));
|
|
8102
8410
|
traceCmd.command("sync [payload]").description("upload this machine's agent sessions (Claude Code, Codex, Cursor, Gemini CLI and every other agent `--agents` lists) to the matching workspace's trace library, redacted here first").option("--enable", "turn sync on for this machine").option("--disable", "turn sync off; nothing more is uploaded").option("--route <match=workspace>", "send sessions whose folder path contains <match> to <workspace> (repeatable; replaces the saved rules)", collect).option("--default <workspace>", "where a session no rule matches goes").option("--keep-local", "keep sessions no rule matches on this machine (the default)").option("--session <ref>", "only this session: its id or transcript path (repeatable)", collect).option("--import <export>", "add a downloaded cloud export (claude.ai, ChatGPT, Gemini, Grok, DeepSeek \u2026: its .zip, folder or file) so sync uploads it (repeatable)", collect).option("--agents", "list the agents sync reads and which are on this machine").option("--full", "re-read every session, not only files that changed").option("--limit <n>", "at most this many changed sessions this run", (v) => Number.parseInt(v, 10)).option("--dry-run", "show what would be uploaded without uploading").option("--hook", "run from a Claude Code SessionEnd hook or Codex notify: upload that session in the background and return at once").option("--install-schedule", "also sweep every 15 minutes (macOS launchd)").option("--status", "what sync has done on this machine").option("--quiet", "print only failures").action(async (payload, opts) => {
|
|
8103
|
-
const lib = await import("./trace-library-
|
|
8411
|
+
const lib = await import("./trace-library-HJWI7KM7.js");
|
|
8104
8412
|
if (opts.hook) {
|
|
8105
8413
|
try {
|
|
8106
8414
|
let raw = payload ?? "";
|
|
@@ -8159,10 +8467,10 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
|
|
|
8159
8467
|
console.log(`Added ${imported.sessions} ${imported.agent} session${imported.sessions === 1 ? "" : "s"} from ${input}.`);
|
|
8160
8468
|
}
|
|
8161
8469
|
if (opts.installSchedule) {
|
|
8162
|
-
const { join:
|
|
8470
|
+
const { join: join20 } = await import("path");
|
|
8163
8471
|
const { homedir: homedir10 } = await import("os");
|
|
8164
8472
|
const { execFileSync } = await import("child_process");
|
|
8165
|
-
const log =
|
|
8473
|
+
const log = join20(homedir10(), ".config", "sky", "trace-sync.log");
|
|
8166
8474
|
const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
|
|
8167
8475
|
try {
|
|
8168
8476
|
execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
|
|
@@ -8172,11 +8480,11 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
|
|
|
8172
8480
|
console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
|
|
8173
8481
|
}
|
|
8174
8482
|
if (opts.status) {
|
|
8175
|
-
const { readFileSync:
|
|
8176
|
-
const { join:
|
|
8483
|
+
const { readFileSync: readFileSync21 } = await import("fs");
|
|
8484
|
+
const { join: join20 } = await import("path");
|
|
8177
8485
|
let state = { sessions: {} };
|
|
8178
8486
|
try {
|
|
8179
|
-
state = JSON.parse(
|
|
8487
|
+
state = JSON.parse(readFileSync21(join20(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
|
|
8180
8488
|
} catch {
|
|
8181
8489
|
}
|
|
8182
8490
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -8208,25 +8516,14 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
|
|
|
8208
8516
|
if (report.failed) process.exitCode = 1;
|
|
8209
8517
|
})();
|
|
8210
8518
|
});
|
|
8211
|
-
traceCmd.command("search <query...>").description("search every session in a workspace's trace library
|
|
8212
|
-
const { libraryRequest } = await import("./trace-library-2VHTJCOY.js");
|
|
8519
|
+
traceCmd.command("search <query...>").description("search every session in a workspace's trace library: the one search with --scope traces").option("--workspace <slug>", "the workspace to search (default: the current one)").option("--limit <n>", "hits per page", "20").option("--cursor <cursor>", "the next page of an earlier search").option("--json", "print the page as JSON").action(run(async (words, opts) => {
|
|
8213
8520
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
8214
8521
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
8215
|
-
const
|
|
8216
|
-
|
|
8217
|
-
if (opts.json) {
|
|
8218
|
-
console.log(JSON.stringify(page, null, 2));
|
|
8219
|
-
return;
|
|
8220
|
-
}
|
|
8221
|
-
if (!page.hits.length) console.log(page.partial ? `No hits, but the search was cut short (${page.coverage}).` : "No hits.");
|
|
8222
|
-
for (const { hit } of page.hits) {
|
|
8223
|
-
console.log(`${hit.occurredAt?.slice(0, 16).replace("T", " ") ?? " "} ${hit.title}`);
|
|
8224
|
-
console.log(` ${hit.snippet.replace(/\s+/g, " ")}`);
|
|
8225
|
-
}
|
|
8226
|
-
if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
|
|
8522
|
+
const page = await searchContext(words.join(" "), { ...opts, scope: "traces" }, workspace2);
|
|
8523
|
+
console.log(opts.json ? JSON.stringify(page, null, 2) : renderContextPage(page));
|
|
8227
8524
|
}));
|
|
8228
8525
|
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) => {
|
|
8229
|
-
const { libraryRequest } = await import("./trace-library-
|
|
8526
|
+
const { libraryRequest } = await import("./trace-library-HJWI7KM7.js");
|
|
8230
8527
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
8231
8528
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
8232
8529
|
const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
|