@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/index.js CHANGED
@@ -9,8 +9,9 @@ import {
9
9
  postEvents,
10
10
  resolveSessionRef,
11
11
  streamEvents,
12
- uploadFile
13
- } from "./chunk-UHMKEYJN.js";
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-RAFHOB5S.js";
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-277AZSSG.js";
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 readFileSync18, writeFileSync as writeFileSync11 } from "fs";
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/context.ts
471
- function memoryBody(operation, version) {
472
- return { ...operation, expectedVersion: version };
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
- function memoryIndex(value) {
475
- if (!/^\d+$/.test(value.trim())) throw new Error(`the entry number must be a whole number, got ${JSON.stringify(value)}`);
476
- return Number(value.trim());
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 renderMemory(document) {
479
- if (!document.entries.length) return "no saved memory \u2014 `sky memory save <text>` adds an entry";
480
- return document.entries.map((entry2) => `${String(entry2.index).padStart(3)} ${entry2.text}`).join("\n");
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 searchQuery(query, opts) {
483
- const params = new URLSearchParams({ q: query });
484
- params.set("scope", opts.scope ?? "sessions");
485
- if (opts.limit) params.set("limit", opts.limit);
486
- if (opts.cursor) params.set("cursor", opts.cursor);
487
- if (opts.since) params.set("since", opts.since);
488
- if (opts.until) params.set("until", opts.until);
489
- return `?${params.toString()}`;
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 refArgument(ref) {
492
- return `'${JSON.stringify(ref).replace(/'/g, "'\\''")}'`;
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 renderContextPage(page) {
495
- const lines = [];
496
- for (const { hit } of page.hits) {
497
- lines.push(`${hit.source.padEnd(8)} ${hit.occurredAt?.slice(0, 10) ?? " "} ${hit.title}`);
498
- lines.push(` ${hit.snippet.replace(/\s+/g, " ").slice(0, 160)}`);
499
- lines.push(` sky context read ${refArgument(hit.ref)}`);
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
- if (!page.hits.length) lines.push("no matches on this page");
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
- // src/agent-file.ts
508
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
509
- import { dirname as dirname2, resolve as resolve2 } from "path";
510
- import { parse as parseToml } from "smol-toml";
511
- var fileInstructionSchema = external_exports.object({
512
- name: external_exports.string(),
513
- content: external_exports.string().optional(),
514
- file: external_exports.string().optional()
515
- });
516
- var agentFileSchema = external_exports.object({
517
- name: external_exports.string().min(1),
518
- id: external_exports.string().optional(),
519
- // stamped by launch/deploy; identifies the live agent
520
- slug: external_exports.string().optional(),
521
- // pre-RFC-0079 stamp; ignored (agents are id-addressed)
522
- description: external_exports.string().optional(),
523
- type: external_exports.enum(AGENT_TYPES).optional(),
524
- llm: external_exports.string().optional(),
525
- reasoning_effort: external_exports.enum(REASONING_EFFORT_VALUES).optional(),
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 file = parsed.data;
549
- if (file.reasoning_effort && file.type && !isModelEffortSupported(file.type, file.llm ?? defaultLlm(file.type), file.reasoning_effort)) {
550
- throw new Error(
551
- `${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
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
- const base = dirname2(resolve2(path));
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 stampId(path, id) {
584
- writeFileSync2(path, stampIdText(readFileSync3(path, "utf8"), id));
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 stampIdText(text, id) {
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
- let topLevel = true;
589
- let openString = null;
590
- let stamped = false;
591
- let nameAt = -1;
592
- for (const line of text.split("\n")) {
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
- if (/^\s*\[/.test(line)) topLevel = false;
599
- if (topLevel && /^\s*(id|slug)\s*=/.test(line)) {
600
- if (/^\s*id\s*=/.test(line) && !stamped) {
601
- out.push(`id = ${JSON.stringify(id)}`);
602
- stamped = true;
603
- }
604
- continue;
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
- if (!stamped) out.splice(nameAt + 1, 0, `id = ${JSON.stringify(id)}`);
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 scaffoldAgentFile(path, name) {
617
- if (existsSync2(path)) throw new Error(`${path} already exists`);
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 deployPatch(spec) {
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
- displayName: spec.displayName,
641
- ...spec.agentType ? { agentType: spec.agentType } : {},
642
- ...spec.llm ? { llm: spec.llm } : {},
643
- ...spec.capabilities ? { capabilities: spec.capabilities } : {},
644
- ...spec.reasoningEffort ? { reasoningEffort: spec.reasoningEffort } : {},
645
- ...spec.skills ? { skills: spec.skills } : {},
646
- ...spec.hiringPolicy ? { hiringPolicy: spec.hiringPolicy } : {},
647
- ...spec.environmentId ? { environmentId: spec.environmentId } : {}
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 deployReport(id, spec, before, after, promptChanged) {
651
- if (spec.agentType && after.agentType !== spec.agentType) {
652
- throw new Error(`${id} is still ${after.agentType}: the API did not apply type = "${spec.agentType}"`);
653
- }
654
- if (spec.llm && after.llm !== spec.llm) {
655
- throw new Error(`${id} still runs ${after.llm}: the API did not apply llm = "${spec.llm}"`);
656
- }
657
- const changed = [];
658
- if (promptChanged) changed.push("prompt");
659
- if (after.version !== before.version) changed.push("settings");
660
- if (!changed.length) return [`${id} is up to date \u2014 nothing to deploy (version ${after.version})`];
661
- const lines = [`Deployed ${id}: ${changed.join(" and ")} updated (version ${before.version} \u2192 ${after.version})`];
662
- if (after.agentType !== before.agentType || after.llm !== before.llm) {
663
- lines.push(` now ${after.agentType} \xB7 ${after.llm} for new sessions (was ${before.agentType} \xB7 ${before.llm})`);
664
- }
665
- if (spec.skills && after.version !== before.version) {
666
- lines.push(` Skills apply to new sessions; running sessions keep the skills they started with \u2014 start one with \`sky session create ${id}\`.`);
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 lines;
730
+ return name;
669
731
  }
670
732
 
671
- // src/catalog.ts
672
- var bare = (handle) => handle.replace(/^@/, "");
673
- var optionalAuth = () => ({ auth: Boolean(apiToken()) });
674
- function catalogQuery(filter) {
675
- const params = new URLSearchParams();
676
- if (filter.text?.trim()) params.set("q", filter.text.trim());
677
- if (filter.category) params.set("category", filter.category);
678
- const query = params.toString();
679
- return query ? `?${query}` : "";
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
- function listingBody(agentId, options) {
682
- return {
683
- agent_id: agentId,
684
- handle: bare(options.handle),
685
- tagline: options.tagline,
686
- ...options.description ? { description: options.description } : {},
687
- ...options.category?.length ? { categories: options.category } : {},
688
- ...options.does?.length ? { does: options.does } : {},
689
- ...options.doesNot?.length ? { does_not: options.doesNot } : {},
690
- ...options.example?.length ? { examples: options.example } : {}
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
- async function listCatalog(filter = {}) {
694
- return (await api("GET", `/catalog/agents${catalogQuery(filter)}`, void 0, optionalAuth())).agents;
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
- async function showCatalogAgent(handle) {
697
- return (await api("GET", `/catalog/agents/${encodeURIComponent(bare(handle))}`, void 0, optionalAuth())).agent;
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
- async function listCategories() {
700
- return (await api("GET", "/catalog/categories", void 0, { auth: false })).categories;
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
- async function publishListing(agentId, options) {
703
- return (await api("POST", "/catalog/listings", listingBody(agentId, options))).agent;
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
- async function removeListing(handle) {
706
- await api("DELETE", `/catalog/listings/${encodeURIComponent(bare(handle))}`);
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
- async function setEngineStatus(engine, status, reason) {
709
- await api("PATCH", `/catalog/engines/${encodeURIComponent(engine)}`, { status, reason });
710
- const state = await api("GET", "/catalog/engines", void 0, { auth: false });
711
- if (state.delisted.includes(engine) !== (status === "delisted")) {
712
- throw new Error("The catalog does not show the requested state; check whether another operator changed it.");
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
- var uses = (agent2) => `${agent2.stats.uses} ${agent2.stats.uses === 1 ? "use" : "uses"}`;
716
- var mine = (agent2) => agent2.visibility === "workspace" ? " (yours)" : "";
717
- function renderCatalogAgent(agent2) {
718
- const section = (title, lines) => lines.length ? [`${title}:`, ...lines.map((line) => ` - ${line}`)] : [];
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
- `@${agent2.handle} ${agent2.name}${mine(agent2)}`,
721
- agent2.tagline,
722
- ...agent2.description && agent2.description !== agent2.tagline ? ["", agent2.description] : [],
1034
+ "# Imported working copy",
723
1035
  "",
724
- `Categories: ${agent2.categories.join(", ") || "none"}`,
725
- `Used: ${uses(agent2)}`,
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
- `Hire it: sky session create @${agent2.handle} --message "\u2026"`
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
- // src/clone.ts
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
- return readFileSync4(path, "utf8");
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 void 0;
1069
+ return null;
750
1070
  }
751
1071
  }
752
- function readJson(path) {
753
- const raw = readIfExists(path);
754
- if (raw === void 0) return void 0;
755
- try {
756
- return JSON.parse(raw);
757
- } catch {
758
- return void 0;
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
- function claudeModel(configRoot) {
762
- const candidates = [
763
- join3(process.cwd(), ".claude", "settings.local.json"),
764
- join3(process.cwd(), ".claude", "settings.json"),
765
- join3(configRoot, "settings.json")
766
- ];
767
- for (const path of candidates) {
768
- const model = readJson(path)?.model;
769
- if (typeof model === "string" && model.trim()) return model.trim();
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 claudeMcpServers() {
774
- const merged = {};
775
- const userConfig = readJson(join3(homedir(), ".claude.json"));
776
- Object.assign(merged, userConfig?.mcpServers ?? {});
777
- Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
778
- const projectConfig = readJson(
779
- join3(process.cwd(), ".mcp.json")
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
- ${p.content.trim()}`).join("\n\n").trim();
788
- if (joined.length <= PROMPT_CAP) return { prompt: joined, truncated: false };
789
- const notice = "\n\n[truncated by sky clone: local instructions exceeded the 100k prompt cap]";
790
- return { prompt: joined.slice(0, PROMPT_CAP - notice.length) + notice, truncated: true };
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 readClaudeCode() {
793
- const configRoot = claudeConfigRoot();
794
- const parts = [];
795
- const sources = [];
796
- const global = readIfExists(join3(configRoot, "CLAUDE.md"));
797
- if (global?.trim()) {
798
- parts.push({ label: "Global CLAUDE.md", content: global });
799
- sources.push("global CLAUDE.md");
800
- }
801
- for (const name of ["CLAUDE.md", "AGENTS.md"]) {
802
- const project2 = readIfExists(join3(process.cwd(), name));
803
- if (project2?.trim()) {
804
- parts.push({ label: `Project ${name}`, content: project2 });
805
- sources.push(`project ${name}`);
806
- break;
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
- const { prompt, truncated } = assemblePrompt(parts);
810
- const allServers = claudeMcpServers();
811
- const { kept, dropped } = splitMcpServers(allServers);
812
- const secretCandidates = [];
813
- for (const d of dropped) {
814
- const def = allServers[d.name];
815
- if (!def?.env) continue;
816
- for (const key of d.envKeys) {
817
- const value = def.env[key];
818
- if (value !== void 0 && value.trim()) {
819
- secretCandidates.push({ serverName: d.name, envKey: key, value, url: d.url });
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 detectHarness(explicit) {
836
- if (explicit) {
837
- if (!HARNESSES.includes(explicit)) {
838
- throw new Error(`unknown harness ${explicit} \u2014 expected ${HARNESSES.join(" | ")}`);
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
- return explicit;
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
- function readLocal(harness2) {
850
- if (harness2 === "claude_code") return readClaudeCode();
851
- throw new Error(
852
- `${harness2} cloning lands next \u2014 claude_code is the first supported harness`
853
- );
1294
+
1295
+ // src/import-local.ts
1296
+ function redactLine2(raw) {
1297
+ return redactLine(raw).line;
854
1298
  }
855
- function linkPath(configRoot) {
856
- return join3(configRoot, "agentsky-link.json");
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 readLink(configRoot) {
859
- const link2 = readJson(linkPath(configRoot));
860
- return link2?.agentId ? link2 : void 0;
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 writeLink(configRoot, link2) {
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 { vault: vault2 } = await api("POST", "/vaults", {
879
- display_name: DEFAULT_VAULT_NAME
880
- });
881
- return vault2.id;
882
- } catch (error) {
883
- if (error instanceof ApiError && error.status === 409) {
884
- const raced = await fetchDefaultVaultId();
885
- if (raced) return raced;
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
- throw error;
1361
+ return null;
1362
+ } catch {
1363
+ return null;
888
1364
  }
889
1365
  }
890
- async function confirmAndVaultSecrets(candidates, yes) {
891
- if (!candidates.length) return;
892
- if (yes) {
893
- console.log(
894
- ` ${candidates.length} secret${candidates.length === 1 ? "" : "s"} from dropped MCP servers not saved (--yes skips vault confirmation).`
895
- );
896
- return;
897
- }
898
- console.log("\nSecret-bearing MCP servers were left out of the clone payload.");
899
- console.log("You can save their keys to your workspace vault now (values are sent encrypted, never displayed).\n");
900
- let vaultId;
901
- let saved = 0;
902
- let skipped = 0;
903
- for (const c of candidates) {
904
- const chosen = await confirm(` Add ${c.envKey} (from ${c.serverName}) to your default vault?`);
905
- if (!chosen) {
906
- console.log(` \u2014 skipped ${c.envKey}`);
907
- skipped += 1;
908
- continue;
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
- vaultId ??= await ensureDefaultVaultId();
912
- await api("POST", `/vaults/${encodeURIComponent(vaultId)}/secrets`, {
913
- display_name: `${c.envKey} (${c.serverName})`,
914
- auth: {
915
- type: "environment_variable",
916
- secret_name: c.envKey,
917
- value: c.value,
918
- allowed_hosts: c.url ? [c.url] : []
919
- }
920
- });
921
- console.log(` \u2713 saved ${c.envKey}`);
922
- saved += 1;
923
- } catch (error) {
924
- if (error instanceof ApiError && error.status === 409) {
925
- console.log(` \u2014 ${c.envKey} already in vault`);
926
- } else {
927
- const msg = error instanceof Error ? error.message : String(error);
928
- console.log(` \u2717 ${c.envKey} failed: ${msg}`);
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
- if (saved || skipped) {
933
- console.log(
934
- `
935
- Vault: ${saved} saved, ${skipped} skipped. Attach the "default" vault to a session to use these keys.`
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 confirm(question) {
940
- return new Promise((done) => {
941
- const rl = createInterface({ input: process.stdin, output: process.stdout });
942
- rl.question(`${question} [Y/n] `, (answer) => {
943
- rl.close();
944
- done(/^(y(es)?)?$/i.test(answer.trim()));
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 openBrowser(url) {
949
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1515
+ function loadSummaryFile(summaryPath) {
1516
+ let raw;
950
1517
  try {
951
- spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
1518
+ raw = readFileSync6(summaryPath, "utf8");
952
1519
  } catch {
1520
+ throw new Error(`Cannot read summary file: ${summaryPath}`);
953
1521
  }
954
- }
955
- function kb(text) {
956
- return `${Math.max(1, Math.round(text.length / 1024))} KB`;
957
- }
958
- async function createClone(local, opts) {
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
- return {
1311
- manifest: {
1312
- format: "agentsky.import-workspace",
1313
- format_version: 1,
1314
- harness: harness2,
1315
- title: title?.trim().slice(0, 120) || null,
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
- function skillFiles(skill) {
1328
- const out = [];
1329
- const walk = (dir, depth) => {
1330
- if (depth > 8 || out.length > 400) return;
1331
- let entries;
1332
- try {
1333
- entries = readdirSync2(dir, { withFileTypes: true });
1334
- } catch {
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 codexThreadName(sessionId, home = homedir2()) {
1348
- let name = null;
1349
- try {
1350
- for (const line of readFileSync5(join4(process.env.CODEX_HOME ?? join4(home, ".codex"), "session_index.jsonl"), "utf8").split("\n")) {
1351
- if (!line.includes(sessionId)) continue;
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
- const entry2 = JSON.parse(line);
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
- return name;
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
- // src/import-files.ts
1364
- var MAX_ATTACHED_FILE_BYTES = 10 * 1024 * 1024;
1365
- 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;
1366
- var SKIP_DIR = /(^|\/)(node_modules|\.git|\.next|dist|build|__pycache__|\.venv)(\/|$)/;
1367
- var INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", "HERMES.md", "GEMINI.md", ".cursorrules"];
1368
- var DOCUMENTS = {
1369
- ".pdf": "application/pdf",
1370
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1371
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1372
- ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1373
- ".doc": "application/msword",
1374
- ".rtf": "application/rtf",
1375
- ".odt": "application/vnd.oasis.opendocument.text"
1376
- };
1377
- var IMAGES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
1378
- var TEXT = {
1379
- ".md": "text/markdown",
1380
- ".mdx": "text/markdown",
1381
- ".txt": "text/plain",
1382
- ".csv": "text/csv",
1383
- ".tsv": "text/tab-separated-values",
1384
- ".html": "text/html",
1385
- ".htm": "text/html",
1386
- ".css": "text/css",
1387
- ".scss": "text/css",
1388
- ".json": "application/json",
1389
- ".jsonl": "application/x-ndjson",
1390
- ".ndjson": "application/x-ndjson",
1391
- ".yaml": "application/yaml",
1392
- ".yml": "application/yaml",
1393
- ".toml": "application/toml",
1394
- ".xml": "application/xml",
1395
- ".sql": "application/sql",
1396
- ".svg": "image/svg+xml",
1397
- ".js": "application/javascript",
1398
- ".mjs": "application/javascript",
1399
- ".cjs": "application/javascript",
1400
- ".py": "text/x-python",
1401
- ".sh": "text/x-shellscript",
1402
- ".bash": "text/x-shellscript",
1403
- ".zsh": "text/x-shellscript"
1404
- };
1405
- 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"]);
1406
- var PLAIN_NAMES = /* @__PURE__ */ new Set(["Dockerfile", "Makefile", "Procfile", "Gemfile", "Rakefile", "LICENSE", "README", ".cursorrules", ".gitignore", ".dockerignore", ".editorconfig", ".prettierrc", ".eslintrc"]);
1407
- function classify(path) {
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
- 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;
1416
- var PATH_KEYS = ["file_path", "path", "notebook_path", "filePath", "target_file"];
1417
- var obj = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1418
- var str = (value) => typeof value === "string" ? value : "";
1419
- function shellText(input) {
1420
- if (typeof input === "string") return input;
1421
- const i = obj(input);
1422
- if (!i) return "";
1423
- const command = Array.isArray(i.command) ? i.command.filter((part) => typeof part === "string").join(" ") : str(i.command);
1424
- return [command, str(i.cmd), str(i.script)].filter(Boolean).join("\n");
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 touchedPaths(records, cwd, home = homedir3()) {
1427
- const out = /* @__PURE__ */ new Set();
1428
- const add = (raw, base) => {
1429
- if (!raw || raw.length > 1e3) return;
1430
- const path = raw.replace(/^~(?=\/)/, home);
1431
- if (isAbsolute(path)) out.add(resolve4(path));
1432
- else if (base) out.add(resolve4(base, path));
1433
- };
1434
- const call = (name, input) => {
1435
- const i = obj(input);
1436
- const base = str(i?.workdir) || str(i?.cwd) || cwd;
1437
- if (i) for (const key of PATH_KEYS) add(str(i[key]), base);
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
- call(str(payload.name), input);
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
- function gitRoot(dir) {
1469
- for (let d = dir; ; d = dirname4(d)) {
1470
- if (existsSync5(join5(d, ".git"))) return d;
1471
- if (d === dirname4(d)) return null;
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 instructionPaths(cwd, harness2, home = homedir3()) {
1475
- const out = [];
1476
- if (cwd && existsSync5(cwd)) {
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 resourceName(path, cwd, home = homedir3()) {
1495
- const inside = cwd ? relative2(resolve4(cwd), path) : "";
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
- var ORDER = { text: 0, document: 1, image: 2 };
1502
- function resourceLine(name, mediaType, data) {
1503
- return redactLine(JSON.stringify({ type: "import.resource", name, media_type: mediaType, data: data.toString("base64") }), { requireStructured: true }).line + "\n";
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 collectSessionFiles(records, opts) {
1506
- const home = opts.home ?? homedir3();
1507
- const exclude = new Set((opts.exclude ?? []).map((path) => resolve4(path)));
1508
- const leftOut = [];
1509
- const candidates = [];
1510
- const seen = /* @__PURE__ */ new Set();
1511
- const touched = touchedPaths(records, opts.cwd, home);
1512
- const skills2 = loadedSkills(records, touched, opts.cwd, home);
1513
- const consider = (path, name) => {
1514
- if (seen.has(path) || exclude.has(path)) return;
1515
- seen.add(path);
1516
- if (SECRET_FILE.test(path) || SKIP_DIR.test(path)) {
1517
- leftOut.push({ path, reason: "credential or build file" });
1518
- return;
1519
- }
1520
- let size;
1521
- try {
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 attached = [];
1545
- let bytes = 0;
1546
- const state = workspaceState(opts.cwd, opts.harness, skills2, home, opts.title ?? null);
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
- for (const file of candidates) {
1570
- let data;
1571
- try {
1572
- data = readFileSync6(file.path);
1573
- } catch {
1574
- leftOut.push({ path: file.path, reason: "missing" });
1575
- continue;
1576
- }
1577
- if (file.kind === "text" && data.includes(0)) {
1578
- leftOut.push({ path: file.path, reason: "not valid UTF-8 text" });
1579
- continue;
1580
- }
1581
- let line;
1582
- try {
1583
- line = resourceLine(file.name, file.mediaType, data);
1584
- } catch {
1585
- leftOut.push({ path: file.path, reason: file.kind === "text" && !isUtf8(data) ? "not valid UTF-8 text" : "could not be redacted safely" });
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
- const size = Buffer.byteLength(line);
1589
- if (bytes + size > opts.budget) {
1590
- leftOut.push({ path: file.path, reason: "over the upload budget" });
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
- bytes += size;
1594
- lines.push(line);
1595
- attached.push({ name: file.name, kind: file.kind, bytes: data.length });
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
- const setup = {
1598
- skills: skills2.found.map((skill) => skill.name),
1599
- missingSkills: skills2.missing,
1600
- mcpServers: state.manifest.mcp_servers.map((server) => server.name),
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
- // src/import-environment.ts
1607
- import { spawnSync as spawnSync2 } from "child_process";
1608
- import { createHash as createHash2 } from "crypto";
1609
- import { closeSync as closeSync2, createWriteStream, lstatSync, mkdtempSync, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync7, readSync as readSync2, rmSync as rmSync2 } from "fs";
1610
- import { tmpdir } from "os";
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 envName(server, field) {
1623
- return `${server}_${field}`.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
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 ownServer(server, platform) {
1626
- try {
1627
- return server.transport !== "stdio" && platform.ownHost.test(new URL(server.url ?? "").hostname);
1628
- } catch {
1629
- return false;
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
- function mcpJson(servers, platform) {
1633
- const entries = servers.filter((server) => !ownServer(server, platform)).map((server) => {
1634
- if (server.transport === "stdio") {
1635
- return [server.name, {
1636
- command: server.command,
1637
- ...server.args?.length ? { args: server.args } : {},
1638
- ...server.env?.length ? { env: Object.fromEntries(server.env.map((name) => [name, `\${${name}:-}`])) } : {}
1639
- }];
1640
- }
1641
- const query = server.query?.length ? `?${server.query.map((name) => `${encodeURIComponent(name)}=\${${envName(server.name, name)}:-}`).join("&")}` : "";
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 bullet(items, none) {
1651
- return items.length ? items.map((item) => `- ${item}`).join("\n") : `- ${none}`;
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 readme(folder, workspace2, counts, skills2, mcpWritten, platform) {
1654
- const git2 = workspace2?.git;
1655
- const servers = workspace2?.mcp_servers ?? [];
1656
- const missingSkills = (workspace2?.skills ?? []).filter((skill) => !skill.found).map((skill) => skill.name);
1657
- const secrets2 = servers.filter((server) => !ownServer(server, platform)).flatMap((server) => [
1658
- ...server.env ?? [],
1659
- ...(server.headers ?? []).map((name) => envName(server.name, name)),
1660
- ...(server.query ?? []).map((name) => envName(server.name, name))
1661
- ]);
1662
- const where = workspace2?.cwd ? ` (${workspace2.cwd})` : "";
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
- "# Imported working copy",
1665
- "",
1666
- `This folder (/workspace/${folder}) holds what the imported conversation worked with on the person's computer.`,
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
- "## Skills",
1684
- bullet(skills2.map((name) => `${name}: .claude/skills/${name}/SKILL.md (also .agents/skills/${name}/)`), "No skills were loaded."),
1685
- ...missingSkills.length ? ["", `Loaded but no longer on that computer, so not carried: ${missingSkills.join(", ")}.`] : [],
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
- "## Tool servers (MCP)",
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
- function parseWorkspace(file) {
1694
- if (!file) return null;
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
- const value = JSON.parse(file.data.toString("utf8"));
1697
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
2011
+ return readFileSync8(path, "utf8");
1698
2012
  } catch {
1699
- return null;
2013
+ return void 0;
1700
2014
  }
1701
2015
  }
1702
- function importLayout(files2, sourceCwd, platform, wholeFolder) {
1703
- if (!files2.length && !wholeFolder) return null;
1704
- const workspace2 = parseWorkspace(files2.find((file) => file.name === "agentsky/workspace.json"));
1705
- const folder = folderName(workspace2, sourceCwd);
1706
- const entries = /* @__PURE__ */ new Map();
1707
- const skills2 = /* @__PURE__ */ new Set();
1708
- const counts = { project: wholeFolder?.files ?? 0, outside: 0, attachments: 0, wholeFolder: Boolean(wholeFolder) };
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 paxRecord(key, value) {
1757
- const body = ` ${key}=${value}
1758
- `;
1759
- let length = Buffer.byteLength(body);
1760
- for (; ; ) {
1761
- const total = String(length).length + length;
1762
- if (String(total).length + length === total) return Buffer.from(`${total}${body}`);
1763
- length += 1;
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 tarPadding(size) {
1767
- const rest = size % TAR_BLOCK;
1768
- return Buffer.alloc(rest ? TAR_BLOCK - rest : 0);
1769
- }
1770
- function tarHeader(path, size) {
1771
- if (Buffer.byteLength(path) <= 100) return header(path, size, "0");
1772
- const pax = paxRecord("path", path);
1773
- return Buffer.concat([header("PaxHeader", pax.length, "x"), pax, tarPadding(pax.length), header(path, size, "0")]);
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
- // src/import-environment.ts
1777
- var IMPORT_PLATFORM = { name: "AgentSky", ownHost: /(^|\.)(agentsky\.dev|asteroidbelt\.ai)$/i, notesDir: ".agentsky-import" };
1778
- var MAX_ENVIRONMENT_FILES = 2e5;
1779
- var MAX_ENVIRONMENT_BYTES = 2 * 1024 ** 3;
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
- async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1812
- const listed = listFiles(cwd);
1813
- if (listed.length > MAX_ENVIRONMENT_FILES) throw new Error(`The working folder has more than ${MAX_ENVIRONMENT_FILES} files; import it with --no-environment.`);
1814
- const scratch = mkdtempSync(join6(tmpdir(), "sky-environment-"));
1815
- const path = join6(scratch, "environment.tar.gz");
1816
- const leftOut = [];
1817
- const written2 = /* @__PURE__ */ new Set();
1818
- const hash = createHash2("sha256");
1819
- let bytes = 0;
1820
- let redacted2 = 0;
1821
- const gzip = createGzip({ level: 6 });
1822
- const sink = createWriteStream(path);
1823
- gzip.on("data", (chunk) => {
1824
- bytes += chunk.length;
1825
- hash.update(chunk);
1826
- });
1827
- gzip.pipe(sink);
1828
- const done = new Promise((resolve10, reject) => {
1829
- sink.on("finish", () => resolve10());
1830
- sink.on("error", reject);
1831
- gzip.on("error", reject);
1832
- });
1833
- const write = (chunk) => new Promise((resolve10, reject) => {
1834
- if (bytes > MAX_ENVIRONMENT_BYTES) {
1835
- reject(new Error(`The working folder packs to more than ${MAX_ENVIRONMENT_BYTES / 1024 ** 3} GiB; import it with --no-environment.`));
1836
- return;
1837
- }
1838
- if (gzip.write(chunk)) resolve10();
1839
- else gzip.once("drain", resolve10);
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 resourcesOf(lines) {
1914
- return lines.flatMap((line) => {
1915
- try {
1916
- const record = JSON.parse(line);
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
- // src/import-local.ts
1926
- function redactLine2(raw) {
1927
- return redactLine(raw).line;
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 decodeWorkspace(encoded) {
1930
- try {
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 readPrefix(filePath) {
1937
- const fd = openSync3(filePath, "r");
1938
- try {
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 sniffTitle(filePath) {
1946
- try {
1947
- const data = readPrefix(filePath);
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
- function listClaudeCodeSessions(limit) {
1997
- const root = process.env.CLAUDE_CONFIG_DIR ?? join7(homedir4(), ".claude");
1998
- const projectsDir = join7(root, "projects");
1999
- if (!existsSync6(projectsDir)) return [];
2000
- const sessions = [];
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
- for (const encoded of readdirSync4(projectsDir)) {
2003
- const projectDir = join7(projectsDir, encoded);
2004
- let entries;
2005
- try {
2006
- entries = readdirSync4(projectDir);
2007
- } catch {
2008
- continue;
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
- } catch {
2151
+ throw error;
2047
2152
  }
2048
- return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
2049
2153
  }
2050
- function listCodexSessions(limit) {
2051
- const sessionsDir = join7(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join7(homedir4(), ".codex"), "sessions");
2052
- const records = [];
2053
- function walk(dir) {
2054
- let entries;
2055
- try {
2056
- entries = readdirSync4(dir, { withFileTypes: true });
2057
- } catch {
2058
- return;
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
- for (const entry2 of entries) {
2061
- const full = join7(dir, entry2.name);
2062
- if (entry2.isDirectory()) {
2063
- walk(full);
2064
- continue;
2065
- }
2066
- if (!entry2.isFile() || !entry2.name.startsWith("rollout-") || !entry2.name.endsWith(".jsonl")) continue;
2067
- try {
2068
- const first = readPrefix(full).split("\n", 1)[0];
2069
- const meta = JSON.parse(first);
2070
- if (meta.type !== "session_meta" || typeof meta.payload?.id !== "string") continue;
2071
- const data = meta.payload;
2072
- const parent = data.forked_from_id ?? data.source?.spawn?.parent_thread_id ?? data.source?.subagent?.spawn?.parent_thread_id;
2073
- records.push({
2074
- session: {
2075
- harness: "codex",
2076
- sessionId: data.id,
2077
- workspace: typeof data.cwd === "string" ? data.cwd : dir,
2078
- title: sniffTitle(full),
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
- walk(sessionsDir);
2090
- function descendants(id, visited) {
2091
- if (visited.has(id)) return [];
2092
- visited.add(id);
2093
- return records.filter((item) => item.parent === id).flatMap((item) => [
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
- async function mintTrace(base, harness2) {
2124
- const token = apiToken();
2125
- if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
2126
- const res = await fetch(`${base}/api/traces`, {
2127
- method: "POST",
2128
- redirect: "error",
2129
- signal: AbortSignal.timeout(15e3),
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 loadSummaryFile(summaryPath) {
2146
- let raw;
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
- pkg = JSON.parse(raw);
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
- var MAX_IMPORT_BYTES = 30 * 1024 * 1024;
2166
- var SUMMARY_RESERVE_BYTES = 128 * 1024;
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 prepareImport(session2, summaryPath, options = {}) {
2175
- if (session2.discoveryError) throw new Error(session2.discoveryError);
2176
- const maximum = 16 * 1024 * 1024;
2177
- let inputBytes = 0;
2178
- let outputBytes = 0;
2179
- const lines = [];
2180
- const digest = createHash3("sha256");
2181
- let nativeId;
2182
- let redactionCount = 0;
2183
- let encryptedReasoningCount = 0;
2184
- let compactionCount = 0;
2185
- let unreadableCount = 0;
2186
- const source = new SessionSourceCollector();
2187
- const records = [];
2188
- for (const path of session2.files) {
2189
- const stat = statSync5(path);
2190
- if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
2191
- const text = readFileSync8(path, "utf8");
2192
- for (const raw of text.split("\n")) {
2193
- if (!raw.trim()) continue;
2194
- let record;
2195
- try {
2196
- record = JSON.parse(raw);
2197
- } catch {
2198
- unreadableCount++;
2199
- continue;
2200
- }
2201
- if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("Transcript records must be JSON objects.");
2202
- const rec = record;
2203
- if (String(rec.type).startsWith("import.")) throw new Error("Native transcripts cannot supply import protocol records.");
2204
- if (path === session2.files[0]) source.push(rec);
2205
- records.push(rec);
2206
- if (rec.type === "response_item") {
2207
- const payload = rec.payload;
2208
- if (payload?.type === "reasoning" && typeof payload?.encrypted_content === "string" && payload.encrypted_content) {
2209
- encryptedReasoningCount++;
2210
- }
2211
- }
2212
- if (rec.type === "compacted") {
2213
- compactionCount++;
2214
- }
2215
- const id = rec.type === "session_meta" ? rec.payload?.id : rec.sessionId;
2216
- if (!nativeId && typeof id === "string") nativeId = id;
2217
- const result = redactLine(raw.replace(/\r$/, ""), { requireStructured: true });
2218
- if (Object.keys(result.hits).length > 0) redactionCount++;
2219
- const line = result.line + "\n";
2220
- outputBytes += Buffer.byteLength(line);
2221
- if (outputBytes > maximum) throw new Error("Redacted transcript exceeds the 16 MiB limit.");
2222
- digest.update(line);
2223
- lines.push(line);
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
- if (!lines.length) throw new Error("The selected transcript is empty.");
2227
- const sourceId = nativeId ?? session2.sessionId;
2228
- 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.");
2229
- const meta = source.result();
2230
- const files2 = options.files === false ? { lines: [], bytes: 0, attached: [], leftOut: [], setup: { skills: [], missingSkills: [], mcpServers: [], git: false } } : collectSessionFiles(records, {
2231
- harness: session2.harness,
2232
- cwd: meta.cwd ?? (session2.harness === "claude_code" ? session2.workspace : ""),
2233
- budget: MAX_IMPORT_BYTES - outputBytes - SUMMARY_RESERVE_BYTES,
2234
- exclude: session2.files,
2235
- title: meta.title ?? (session2.harness === "codex" ? codexThreadName(session2.sessionId) : null)
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 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).` : "";
2238
- let summaryJson;
2239
- if (summaryPath) {
2240
- summaryJson = loadSummaryFile(summaryPath);
2241
- } else {
2242
- const summary = {
2243
- format: "asteroids.session-summary",
2244
- format_version: 1,
2245
- source_harness: session2.harness,
2246
- source_harness_version: meta.harness_version?.slice(0, 100) ?? null,
2247
- source_session_id: sourceId,
2248
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
2249
- 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.`,
2250
- context: {
2251
- objective: "Review the imported conversation and continue its latest unfinished authorized objective.",
2252
- constraints: [],
2253
- decisions: [],
2254
- completed: [],
2255
- pending: ["Read the complete imported transcript, reconcile the latest human instructions and completed work, then continue."],
2256
- verification: [],
2257
- 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(", ")}.`] : []],
2258
- 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).` })),
2259
- provenance: { native_checkpoint_sha256: digest.digest("hex"), selected_native_session_id: sourceId, context_bytes: outputBytes }
2260
- }
2261
- };
2262
- summaryJson = JSON.stringify(summary);
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
- 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";
2265
- const gaps = [];
2266
- if (encryptedReasoningCount > 0) gaps.push({ kind: "encrypted_reasoning", count: encryptedReasoningCount });
2267
- if (compactionCount > 0) gaps.push({ kind: "compaction", count: compactionCount });
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
- opts.onProgress?.("Reading transcript and redacting secrets locally\u2026");
2279
- const { body, transcriptLines, outputBytes, redactionCount, gaps, attachedFiles, leftOutFiles, setup, cwd, resourceLines } = prepareImport(opts.session, opts.summaryPath, { files: opts.files });
2280
- let traceId;
2281
- let key;
2282
- if (opts.importKey && opts.grantTraceId) {
2283
- traceId = opts.grantTraceId;
2284
- key = opts.importKey;
2285
- } else {
2286
- opts.onProgress?.("Minting import key\u2026");
2287
- ({ traceId, key } = await mintTrace(base, opts.session.harness));
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
- opts.onProgress?.("Uploading the complete transcript\u2026");
2290
- const response = await fetch(`${base}/api/traces/ingest`, {
2291
- method: "POST",
2292
- redirect: "error",
2293
- signal: AbortSignal.timeout(12e4),
2294
- headers: { authorization: `Bearer ${key}`, "content-type": "application/x-ndjson", "x-source-harness": opts.session.harness, "user-agent": USER_AGENT },
2295
- body
2296
- });
2297
- if (!response.ok) {
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
- const result = await response.json();
2302
- const finalTraceId = result.traceId ?? traceId;
2303
- const environment = opts.files === false || opts.environment === false || !cwd || !existsSync6(cwd) ? null : await uploadEnvironment(base, key, finalTraceId, cwd, resourceLines, opts.onProgress).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
2304
- return {
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(scope) {
5105
- const profile = await getBrowserProfile(scope);
5106
- return profile.sites;
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 browserSiteReference(scope, site) {
5109
- const sites = await listBrowserProfileSites(scope);
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 this browser profile");
5112
- return row.site_id ? `${encodeURIComponent(row.site_id)}?by=id` : encodeURIComponent(row.site);
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 removeBrowserProfileSite(scope, site) {
5115
- await api("DELETE", `/browser_profiles/${encodeURIComponent(scope)}/sites/${await browserSiteReference(scope, site)}`);
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 moveBrowserProfileSite(scope, site, to) {
5118
- const reference = await browserSiteReference(scope, site);
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 lines = [`scope: ${profile.scope} revision: ${profile.revision} sites: ${profile.sites.length}`];
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
- var artifactPath = (id) => `/library/artifacts/${encodeURIComponent(id)}`;
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, writeFileSync as writeFileSync9 } from "fs";
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 sep3 = clean.includes("?") ? "&" : "?";
5319
+ const sep4 = clean.includes("?") ? "&" : "?";
5308
5320
  const encoded = params.toString();
5309
- return encoded ? `${clean}${sep3}${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
- writeFileSync9(opts.output, new Uint8Array(await res.arrayBuffer()));
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 readFileSync17, writeFileSync as writeFileSync10 } from "fs";
5446
- import { dirname as dirname9, join as join18, resolve as resolve8 } from "path";
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 join18(resolve8(dir), HARNESS_FILE);
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(readFileSync17(path, "utf8"));
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
- writeFileSync10(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
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
- writeFileSync11(opts.out, bytes);
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
- writeFileSync11(opts.out, patch);
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
- writeFileSync11(opts.out, bytes);
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, readFileSync18(local), mediaTypeFor(local));
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 = readFileSync18(path);
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
- writeFileSync11(out, bytes);
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
- writeFileSync11(opts.out, bytes);
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
- writeFileSync11(opts.out, bytes);
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 your conversations, memory and the workspace trace library");
7373
- context.command("search <words...>").description("literal matches, in source order; follow --cursor for more").option("--scope <scope>", "sessions, memory or traces (default sessions)").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(
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 api("GET", `/context/search${searchQuery(words.join(" "), opts)}`);
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 api("GET", `/context/read?${params}`);
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 workspace's browser profiles: the sites your agents stay signed in to").option("--json", "print as JSON").action(
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 <scope>").description("list the sites in a profile: workspace or private").option("--json", "print as JSON").action(
7705
- run(async (scope, opts) => {
7706
- const sites = await listBrowserProfileSites(scope);
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 <scope>").description("import saved browser state into a profile (workspace or private); 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(
7717
- run(async (scope, opts) => {
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(scope, { state });
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 <scope> <site>").description("remove a site from a profile (the agent will need to sign in again)").action(
7735
- run(async (scope, site) => {
7736
- await removeBrowserProfileSite(scope, site);
7737
- console.log(`Removed ${site} from ${scope}`);
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("move-site <scope> <site> <to>").description("move a site between profiles: <scope> is the current one, <to> is workspace or private").action(
7741
- run(async (scope, site, to) => {
7742
- if (to !== "workspace" && to !== "private") throw new ApiError(400, "invalid_request", "to must be workspace or private");
7743
- const result = await moveBrowserProfileSite(scope, site, to);
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("search help articles by keyword").option("--top <n>", "how many results (1\u201320, default 5)", "5").option("--json", "print as JSON").action(
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 { results } = await helpFetch(`/api/help/search?q=${encodeURIComponent(q)}&top_k=${topK}`);
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(results, null, 2));
8118
+ console.log(JSON.stringify(hits, null, 2));
7811
8119
  return;
7812
8120
  }
7813
- if (!results.length) {
8121
+ if (!hits.length) {
7814
8122
  console.log("no results");
7815
8123
  return;
7816
8124
  }
7817
- for (const r of results) console.log(`${r.slug.padEnd(40)} [${r.category}] ${r.title}
7818
- ${r.snippet.slice(0, 100)}`);
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 = readFileSync18(file, "utf8");
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-CFHCOGKB.js");
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-CFHCOGKB.js");
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-CFHCOGKB.js");
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-CFHCOGKB.js");
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-CFHCOGKB.js");
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-CFHCOGKB.js");
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-2VHTJCOY.js");
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: join19 } = await import("path");
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 = join19(homedir10(), ".config", "sky", "trace-sync.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: readFileSync19 } = await import("fs");
8176
- const { join: join19 } = await import("path");
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(readFileSync19(join19(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
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 (literal substring)").option("--workspace <slug>", "the workspace to search (default: the current one)").option("--limit <n>", "hits per page", (v) => Number.parseInt(v, 10), 20).option("--cursor <cursor>", "the next page of an earlier search").option("--json", "print the page as JSON").action(run(async (words, opts) => {
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 params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
8216
- const page = await libraryRequest("GET", `/context/search?${params}`, workspace2);
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-2VHTJCOY.js");
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);