@agentsky/sky 2.24.0 → 2.24.1

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
@@ -10,7 +10,7 @@ import {
10
10
  resolveSessionRef,
11
11
  streamEvents,
12
12
  uploadFile
13
- } from "./chunk-UHMKEYJN.js";
13
+ } from "./chunk-KWLEADJQ.js";
14
14
  import {
15
15
  CLI_VERSION,
16
16
  USER_AGENT,
@@ -21,7 +21,7 @@ import {
21
21
  currentUniverse,
22
22
  loadConfig,
23
23
  saveConfig
24
- } from "./chunk-RAFHOB5S.js";
24
+ } from "./chunk-S4YG3TXA.js";
25
25
  import {
26
26
  AGENT_TYPES,
27
27
  MODEL_SUBSCRIPTION_PLAN_PHRASE,
@@ -43,11 +43,11 @@ import {
43
43
  skillRefSchema,
44
44
  splitMcpServers,
45
45
  supersededBy
46
- } from "./chunk-277AZSSG.js";
46
+ } from "./chunk-D3WG4BMK.js";
47
47
  import "./chunk-MLKGABMK.js";
48
48
 
49
49
  // src/index.ts
50
- import { existsSync as existsSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
50
+ import { existsSync as existsSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "fs";
51
51
  import { basename as basename9, resolve as resolve9 } from "path";
52
52
  import { createInterface as createInterface6 } from "readline";
53
53
  import { Command } from "commander";
@@ -467,1886 +467,1898 @@ async function unsubscribeMarketing() {
467
467
  return { marketing: notification_preferences.email.marketing };
468
468
  }
469
469
 
470
- // src/context.ts
471
- function memoryBody(operation, version) {
472
- return { ...operation, expectedVersion: version };
470
+ // src/import-local.ts
471
+ import { closeSync as closeSync3, existsSync as existsSync4, openSync as openSync3, readdirSync as readdirSync4, readFileSync as readFileSync6, readSync as readSync3, statSync as statSync5 } from "fs";
472
+ import { homedir as homedir3 } from "os";
473
+ import { join as join6 } from "path";
474
+ import { createHash as createHash3 } from "crypto";
475
+
476
+ // src/import-files.ts
477
+ import { isUtf8 } from "buffer";
478
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
479
+ import { homedir as homedir2 } from "os";
480
+ 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";
481
+
482
+ // src/import-workspace.ts
483
+ import { spawnSync } from "child_process";
484
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, statSync as statSync3 } from "fs";
485
+ import { homedir } from "os";
486
+ import { basename as basename3, dirname as dirname2, join as join3, relative, sep } from "path";
487
+ import { parse as parseToml } from "smol-toml";
488
+ var MAX_TREE = 5e3;
489
+ var SKILL_LINE = /Base directory for this skill: (\/[^\n\r"'\\<>]+)/g;
490
+ function git(cwd, ...args) {
491
+ const run2 = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 1e4, maxBuffer: 32 * 1024 * 1024 });
492
+ return run2.status === 0 ? run2.stdout : null;
473
493
  }
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());
494
+ var isFile = (path) => {
495
+ try {
496
+ return statSync3(path).isFile();
497
+ } catch {
498
+ return false;
499
+ }
500
+ };
501
+ var isDir = (path) => {
502
+ try {
503
+ return statSync3(path).isDirectory();
504
+ } catch {
505
+ return false;
506
+ }
507
+ };
508
+ function sharedInstructionPaths(home = homedir()) {
509
+ const rules = join3(home, ".agents", "rules");
510
+ let ruleFiles = [];
511
+ try {
512
+ ruleFiles = readdirSync2(rules).filter((name) => name.endsWith(".md")).sort().map((name) => join3(rules, name));
513
+ } catch {
514
+ }
515
+ return [join3(home, "AGENTS.md"), join3(home, "MEMORY.md"), ...ruleFiles].filter(isFile);
477
516
  }
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");
517
+ function strings(value, out, depth = 0) {
518
+ if (depth > 12) return;
519
+ if (typeof value === "string") out.push(value);
520
+ else if (Array.isArray(value)) for (const item of value) strings(item, out, depth + 1);
521
+ else if (value && typeof value === "object") for (const item of Object.values(value)) strings(item, out, depth + 1);
481
522
  }
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()}`;
523
+ function skillRoots(cwd, home) {
524
+ const claude = process.env.CLAUDE_CONFIG_DIR ?? join3(home, ".claude");
525
+ const codex = process.env.CODEX_HOME ?? join3(home, ".codex");
526
+ return [
527
+ ...cwd ? [join3(cwd, ".claude", "skills"), join3(cwd, ".agents", "skills")] : [],
528
+ join3(home, ".agents", "skills"),
529
+ join3(claude, "skills"),
530
+ join3(codex, "skills"),
531
+ join3(home, ".hermes", "skills")
532
+ ];
490
533
  }
491
- function refArgument(ref) {
492
- return `'${JSON.stringify(ref).replace(/'/g, "'\\''")}'`;
534
+ function pluginSkill(plugin, skill, home) {
535
+ const walk = (dir, depth, underPlugin) => {
536
+ if (depth > 7) return null;
537
+ let entries;
538
+ try {
539
+ entries = readdirSync2(dir, { withFileTypes: true });
540
+ } catch {
541
+ return null;
542
+ }
543
+ for (const entry2 of entries) {
544
+ if (!entry2.isDirectory() || entry2.name === "node_modules" || entry2.name === ".git") continue;
545
+ const path = join3(dir, entry2.name);
546
+ if (entry2.name === skill && basename3(dir) === "skills" && (underPlugin || path.includes(`${sep}${plugin}${sep}`)) && isFile(join3(path, "SKILL.md"))) return realpathSync(path);
547
+ const found = walk(path, depth + 1, underPlugin);
548
+ if (found) return found;
549
+ }
550
+ return null;
551
+ };
552
+ 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
553
  }
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)}`);
554
+ function findSkill(name, cwd, home) {
555
+ const [scope, skill] = name.includes(":") ? name.split(":", 2) : [null, name];
556
+ if (scope && skill) return pluginSkill(scope, skill, home);
557
+ for (const root of skillRoots(cwd, home)) {
558
+ const direct = join3(root, name);
559
+ if (isFile(join3(direct, "SKILL.md"))) return realpathSync(direct);
500
560
  }
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");
561
+ return null;
505
562
  }
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"}`);
563
+ function loadedSkills(records, touched, cwd, home = homedir()) {
564
+ const dirs = /* @__PURE__ */ new Map();
565
+ const missing = /* @__PURE__ */ new Set();
566
+ const add = (dir, name = basename3(dir)) => {
567
+ if (!isFile(join3(dir, "SKILL.md"))) return false;
568
+ const real = realpathSync(dir);
569
+ if (![...dirs.values()].includes(real)) dirs.set(name, real);
570
+ return true;
571
+ };
572
+ const called = [];
573
+ for (const record of records) {
574
+ const texts = [];
575
+ strings(record, texts);
576
+ for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].trim().replace(/[.,;:)]+$/, ""));
577
+ const content = record?.message?.content;
578
+ if (!Array.isArray(content)) continue;
579
+ for (const block of content) {
580
+ const b = block;
581
+ if (b?.type === "tool_use" && b.name === "Skill" && typeof b.input?.skill === "string") called.push(b.input.skill.replace(/^\//, ""));
582
+ }
547
583
  }
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
- );
584
+ for (const path of touched) if (basename3(path) === "SKILL.md") add(dirname2(path));
585
+ for (const name of called) {
586
+ if (!name || dirs.has(name) || [...dirs.keys()].some((key) => key.endsWith(`:${name}`) || name.endsWith(`:${key}`))) continue;
587
+ const dir = findSkill(name, cwd, home);
588
+ if (!dir || !add(dir, name)) missing.add(name);
553
589
  }
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 };
590
+ return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
582
591
  }
583
- function stampId(path, id) {
584
- writeFileSync2(path, stampIdText(readFileSync3(path, "utf8"), id));
592
+ function readJson(path) {
593
+ try {
594
+ const value = JSON.parse(readFileSync3(path, "utf8"));
595
+ return value && typeof value === "object" ? value : null;
596
+ } catch {
597
+ return null;
598
+ }
585
599
  }
586
- function stampIdText(text, id) {
600
+ function names(value) {
601
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
602
+ const keys2 = Object.keys(value).sort();
603
+ return keys2.length ? keys2 : void 0;
604
+ }
605
+ function safeUrl(raw) {
606
+ try {
607
+ const url = new URL(raw);
608
+ if (!/^https?:$/.test(url.protocol)) return null;
609
+ const query = [.../* @__PURE__ */ new Set([...url.searchParams.keys()])].sort();
610
+ return { url: `${url.protocol}//${url.host}${url.pathname}`, ...query.length ? { query } : {} };
611
+ } catch {
612
+ return null;
613
+ }
614
+ }
615
+ function serverEntry(name, def, scope) {
616
+ if (!def || typeof def !== "object") return null;
617
+ const env = names(def.env);
618
+ const headers = names(def.headers ?? def.http_headers);
619
+ if (typeof def.url === "string") {
620
+ const url = safeUrl(def.url);
621
+ if (!url) return null;
622
+ const type = def.type === "sse" ? "sse" : "http";
623
+ return { name, scope, transport: type, ...url, ...headers ? { headers } : {}, ...env ? { env } : {} };
624
+ }
625
+ if (typeof def.command !== "string" || !def.command) return null;
626
+ const args = Array.isArray(def.args) ? def.args.filter((arg) => typeof arg === "string") : [];
627
+ return { name, scope, transport: "stdio", command: def.command, ...args.length ? { args } : {}, ...env ? { env } : {} };
628
+ }
629
+ function mcpServers(harness2, cwd, home = homedir()) {
587
630
  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;
631
+ const push = (servers, scope) => {
632
+ if (!servers || typeof servers !== "object") return;
633
+ for (const [name, def] of Object.entries(servers)) {
634
+ const entry2 = serverEntry(name, def, scope);
635
+ if (entry2 && !out.some((existing) => existing.name === name)) out.push(entry2);
597
636
  }
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;
637
+ };
638
+ if (harness2 === "claude_code") {
639
+ const config = readJson(join3(process.env.CLAUDE_CONFIG_DIR ? dirname2(process.env.CLAUDE_CONFIG_DIR) : home, ".claude.json"));
640
+ const projects = config?.projects && typeof config.projects === "object" ? config.projects : {};
641
+ const root = cwd ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || cwd : "";
642
+ if (cwd) push(projects[cwd]?.mcpServers ?? projects[root]?.mcpServers, "local");
643
+ if (root) push(readJson(join3(root, ".mcp.json"))?.mcpServers, "project");
644
+ push(config?.mcpServers, "user");
645
+ }
646
+ if (harness2 === "codex") {
647
+ try {
648
+ const config = parseToml(readFileSync3(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "config.toml"), "utf8"));
649
+ push(config.mcp_servers, "user");
650
+ } catch {
605
651
  }
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
652
  }
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;
653
+ return out;
615
654
  }
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
- );
655
+ function remoteWithoutCredentials(remote) {
656
+ return remote.trim().replace(/\/\/[^@/]+@/, "//").replace(/^[^@/]+@([^:]+):/, "$1:");
637
657
  }
638
- function deployPatch(spec) {
658
+ function workspaceState(cwd, harness2, skills2, home = homedir(), title = null) {
659
+ const present = !!cwd && isDir(cwd);
660
+ const root = present ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || null : null;
661
+ let gitState = null;
662
+ let tree = [];
663
+ let patch = null;
664
+ if (root) {
665
+ const dirty = (git(root, "status", "--porcelain=v1", "--untracked-files=normal") ?? "").split("\n").filter(Boolean).map((line) => line.slice(3));
666
+ const ahead = git(root, "rev-list", "--count", "@{upstream}..HEAD");
667
+ const remote = git(root, "remote", "get-url", "origin");
668
+ const branch = git(root, "rev-parse", "--abbrev-ref", "HEAD")?.trim() || null;
669
+ gitState = {
670
+ remote: remote ? remoteWithoutCredentials(remote) : null,
671
+ branch: branch === "HEAD" ? null : branch,
672
+ commit: git(root, "rev-parse", "HEAD")?.trim() || null,
673
+ dirty: dirty.slice(0, 500),
674
+ ahead: ahead === null ? null : Number(ahead.trim()) || 0
675
+ };
676
+ tree = (git(cwd, "-c", "core.quotePath=false", "ls-files") ?? "").split("\n").filter(Boolean);
677
+ if (dirty.length) patch = git(cwd, "diff", "HEAD", "--no-color", "--no-ext-diff", "--relative", "--", ".") || null;
678
+ }
639
679
  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 } : {}
680
+ manifest: {
681
+ format: "agentsky.import-workspace",
682
+ format_version: 1,
683
+ harness: harness2,
684
+ title: title?.trim().slice(0, 120) || null,
685
+ cwd: cwd ? cwd.replace(home, "~") : null,
686
+ project: cwd ? basename3(root ?? cwd) : null,
687
+ git: gitState,
688
+ tree: tree.slice(0, MAX_TREE),
689
+ tree_truncated: tree.length > MAX_TREE,
690
+ skills: [...skills2.found.map((skill) => ({ name: skill.name, found: true })), ...skills2.missing.map((name) => ({ name, found: false }))],
691
+ mcp_servers: mcpServers(harness2, cwd, home)
692
+ },
693
+ patch
648
694
  };
649
695
  }
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}\`.`);
696
+ function skillFiles(skill) {
697
+ const out = [];
698
+ const walk = (dir, depth) => {
699
+ if (depth > 8 || out.length > 400) return;
700
+ let entries;
701
+ try {
702
+ entries = readdirSync2(dir, { withFileTypes: true });
703
+ } catch {
704
+ return;
705
+ }
706
+ for (const entry2 of entries) {
707
+ if (entry2.name === "node_modules" || entry2.name === ".git" || entry2.name === "__pycache__" || entry2.name === ".venv") continue;
708
+ const path = join3(dir, entry2.name);
709
+ if (entry2.isDirectory()) walk(path, depth + 1);
710
+ else if (entry2.isFile()) out.push({ path, name: `skills/${skill.name}/${relative(skill.dir, path).split(sep).join("/")}` });
711
+ }
712
+ };
713
+ if (existsSync2(skill.dir)) walk(skill.dir, 0);
714
+ return out;
715
+ }
716
+ function codexThreadName(sessionId, home = homedir()) {
717
+ let name = null;
718
+ try {
719
+ for (const line of readFileSync3(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "session_index.jsonl"), "utf8").split("\n")) {
720
+ if (!line.includes(sessionId)) continue;
721
+ try {
722
+ const entry2 = JSON.parse(line);
723
+ if (entry2.id === sessionId && typeof entry2.thread_name === "string" && entry2.thread_name.trim()) name = entry2.thread_name.trim();
724
+ } catch {
725
+ }
726
+ }
727
+ } catch {
667
728
  }
668
- return lines;
729
+ return name;
669
730
  }
670
731
 
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}` : "";
732
+ // src/import-files.ts
733
+ var MAX_ATTACHED_FILE_BYTES = 10 * 1024 * 1024;
734
+ 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;
735
+ var SKIP_DIR = /(^|\/)(node_modules|\.git|\.next|dist|build|__pycache__|\.venv)(\/|$)/;
736
+ var INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", "HERMES.md", "GEMINI.md", ".cursorrules"];
737
+ var DOCUMENTS = {
738
+ ".pdf": "application/pdf",
739
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
740
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
741
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
742
+ ".doc": "application/msword",
743
+ ".rtf": "application/rtf",
744
+ ".odt": "application/vnd.oasis.opendocument.text"
745
+ };
746
+ var IMAGES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
747
+ var TEXT = {
748
+ ".md": "text/markdown",
749
+ ".mdx": "text/markdown",
750
+ ".txt": "text/plain",
751
+ ".csv": "text/csv",
752
+ ".tsv": "text/tab-separated-values",
753
+ ".html": "text/html",
754
+ ".htm": "text/html",
755
+ ".css": "text/css",
756
+ ".scss": "text/css",
757
+ ".json": "application/json",
758
+ ".jsonl": "application/x-ndjson",
759
+ ".ndjson": "application/x-ndjson",
760
+ ".yaml": "application/yaml",
761
+ ".yml": "application/yaml",
762
+ ".toml": "application/toml",
763
+ ".xml": "application/xml",
764
+ ".sql": "application/sql",
765
+ ".svg": "image/svg+xml",
766
+ ".js": "application/javascript",
767
+ ".mjs": "application/javascript",
768
+ ".cjs": "application/javascript",
769
+ ".py": "text/x-python",
770
+ ".sh": "text/x-shellscript",
771
+ ".bash": "text/x-shellscript",
772
+ ".zsh": "text/x-shellscript"
773
+ };
774
+ 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"]);
775
+ var PLAIN_NAMES = /* @__PURE__ */ new Set(["Dockerfile", "Makefile", "Procfile", "Gemfile", "Rakefile", "LICENSE", "README", ".cursorrules", ".gitignore", ".dockerignore", ".editorconfig", ".prettierrc", ".eslintrc"]);
776
+ function classify(path) {
777
+ const ext = extname2(path).toLowerCase();
778
+ if (DOCUMENTS[ext]) return { kind: "document", mediaType: DOCUMENTS[ext] };
779
+ if (IMAGES[ext]) return { kind: "image", mediaType: IMAGES[ext] };
780
+ if (TEXT[ext]) return { kind: "text", mediaType: TEXT[ext] };
781
+ if (PLAIN_TEXT.has(ext) || PLAIN_NAMES.has(basename4(path))) return { kind: "text", mediaType: "text/plain" };
782
+ return null;
680
783
  }
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 } : {}
784
+ 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;
785
+ var PATH_KEYS = ["file_path", "path", "notebook_path", "filePath", "target_file"];
786
+ var obj = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
787
+ var str = (value) => typeof value === "string" ? value : "";
788
+ function shellText(input) {
789
+ if (typeof input === "string") return input;
790
+ const i = obj(input);
791
+ if (!i) return "";
792
+ const command = Array.isArray(i.command) ? i.command.filter((part) => typeof part === "string").join(" ") : str(i.command);
793
+ return [command, str(i.cmd), str(i.script)].filter(Boolean).join("\n");
794
+ }
795
+ function touchedPaths(records, cwd, home = homedir2()) {
796
+ const out = /* @__PURE__ */ new Set();
797
+ const add = (raw, base) => {
798
+ if (!raw || raw.length > 1e3) return;
799
+ const path = raw.replace(/^~(?=\/)/, home);
800
+ if (isAbsolute(path)) out.add(resolve2(path));
801
+ else if (base) out.add(resolve2(base, path));
802
+ };
803
+ const call = (name, input) => {
804
+ const i = obj(input);
805
+ const base = str(i?.workdir) || str(i?.cwd) || cwd;
806
+ if (i) for (const key of PATH_KEYS) add(str(i[key]), base);
807
+ const patch = typeof input === "string" ? input : str(i?.input) || str(i?.patch);
808
+ for (const match of patch.matchAll(/^\*\*\* (?:Add|Update) File: (.+)$/gm)) add(match[1].trim(), base);
809
+ if (!/bash|shell|exec|terminal|command|run/i.test(name)) return;
810
+ for (const match of shellText(input).replace(/\\n/g, "\n").matchAll(SHELL_READ)) add(match[1], base);
691
811
  };
812
+ for (const raw of records) {
813
+ const record = obj(raw);
814
+ if (!record) continue;
815
+ const content = obj(record.message)?.content;
816
+ if (record.type === "assistant" && Array.isArray(content)) {
817
+ for (const block of content) {
818
+ const b = obj(block);
819
+ if (b?.type === "tool_use") call(str(b.name), b.input);
820
+ }
821
+ }
822
+ const payload = obj(record.payload);
823
+ if (record.type === "response_item" && (payload?.type === "function_call" || payload?.type === "custom_tool_call")) {
824
+ let input = payload.arguments ?? payload.input ?? "";
825
+ if (typeof input === "string") {
826
+ try {
827
+ input = JSON.parse(input);
828
+ } catch {
829
+ input = payload.arguments ?? payload.input;
830
+ }
831
+ }
832
+ call(str(payload.name), input);
833
+ }
834
+ }
835
+ return [...out];
692
836
  }
693
- async function listCatalog(filter = {}) {
694
- return (await api("GET", `/catalog/agents${catalogQuery(filter)}`, void 0, optionalAuth())).agents;
837
+ function gitRoot(dir) {
838
+ for (let d = dir; ; d = dirname3(d)) {
839
+ if (existsSync3(join4(d, ".git"))) return d;
840
+ if (d === dirname3(d)) return null;
841
+ }
695
842
  }
696
- async function showCatalogAgent(handle) {
697
- return (await api("GET", `/catalog/agents/${encodeURIComponent(bare(handle))}`, void 0, optionalAuth())).agent;
843
+ function instructionPaths(cwd, harness2, home = homedir2()) {
844
+ const out = [];
845
+ if (cwd && existsSync3(cwd)) {
846
+ const root = gitRoot(cwd) ?? cwd;
847
+ for (let d = resolve2(cwd); ; d = dirname3(d)) {
848
+ for (const name of INSTRUCTION_FILES) out.push(join4(d, name));
849
+ if (d === root || d === dirname3(d)) break;
850
+ }
851
+ }
852
+ if (harness2 === "claude_code") out.push(join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "CLAUDE.md"));
853
+ if (harness2 === "codex") out.push(join4(process.env.CODEX_HOME ?? join4(home, ".codex"), "AGENTS.md"));
854
+ out.push(...sharedInstructionPaths(home));
855
+ return out.filter((path) => {
856
+ try {
857
+ return statSync4(path).isFile();
858
+ } catch {
859
+ return false;
860
+ }
861
+ });
698
862
  }
699
- async function listCategories() {
700
- return (await api("GET", "/catalog/categories", void 0, { auth: false })).categories;
863
+ function resourceName(path, cwd, home = homedir2()) {
864
+ const inside = cwd ? relative2(resolve2(cwd), path) : "";
865
+ if (cwd && inside && !inside.startsWith("..") && !isAbsolute(inside)) return `workspace/${inside.split(sep2).join("/")}`;
866
+ const underHome = relative2(home, path);
867
+ const shown = !underHome.startsWith("..") && !isAbsolute(underHome) ? `~/${underHome}` : path.replace(/^\/+/, "");
868
+ return `files/${shown.split(sep2).join("/")}`;
701
869
  }
702
- async function publishListing(agentId, options) {
703
- return (await api("POST", "/catalog/listings", listingBody(agentId, options))).agent;
870
+ var ORDER = { text: 0, document: 1, image: 2 };
871
+ function resourceLine(name, mediaType, data) {
872
+ return redactLine(JSON.stringify({ type: "import.resource", name, media_type: mediaType, data: data.toString("base64") }), { requireStructured: true }).line + "\n";
704
873
  }
705
- async function removeListing(handle) {
706
- await api("DELETE", `/catalog/listings/${encodeURIComponent(bare(handle))}`);
874
+ function collectSessionFiles(records, opts) {
875
+ const home = opts.home ?? homedir2();
876
+ const exclude = new Set((opts.exclude ?? []).map((path) => resolve2(path)));
877
+ const leftOut = [];
878
+ const candidates = [];
879
+ const seen = /* @__PURE__ */ new Set();
880
+ const touched = touchedPaths(records, opts.cwd, home);
881
+ const skills2 = loadedSkills(records, touched, opts.cwd, home);
882
+ const consider = (path, name) => {
883
+ if (seen.has(path) || exclude.has(path)) return;
884
+ seen.add(path);
885
+ if (SECRET_FILE.test(path) || SKIP_DIR.test(path)) {
886
+ leftOut.push({ path, reason: "credential or build file" });
887
+ return;
888
+ }
889
+ let size;
890
+ try {
891
+ const stat = statSync4(path);
892
+ if (!stat.isFile()) return;
893
+ size = stat.size;
894
+ } catch {
895
+ leftOut.push({ path, reason: "missing" });
896
+ return;
897
+ }
898
+ const type = classify(path);
899
+ if (!type) {
900
+ leftOut.push({ path, reason: "unsupported type" });
901
+ return;
902
+ }
903
+ if (size > MAX_ATTACHED_FILE_BYTES) {
904
+ leftOut.push({ path, reason: "too large" });
905
+ return;
906
+ }
907
+ candidates.push({ path, name, ...type, size });
908
+ };
909
+ for (const path of [...instructionPaths(opts.cwd, opts.harness, home), ...touched]) consider(path, resourceName(path, opts.cwd, home));
910
+ for (const skill of skills2.found) for (const file of skillFiles(skill)) consider(file.path, file.name);
911
+ candidates.sort((a, b) => ORDER[a.kind] - ORDER[b.kind] || a.size - b.size);
912
+ const lines = [];
913
+ const attached = [];
914
+ let bytes = 0;
915
+ const state = workspaceState(opts.cwd, opts.harness, skills2, home, opts.title ?? null);
916
+ const described = !!opts.cwd || !!state.manifest.title || state.manifest.skills.length > 0 || state.manifest.mcp_servers.length > 0;
917
+ const generated = [
918
+ ...described ? [{ name: "agentsky/workspace.json", mediaType: "application/json", data: Buffer.from(JSON.stringify(state.manifest, null, 2)) }] : [],
919
+ ...state.patch ? [{ name: "agentsky/uncommitted.patch", mediaType: "text/x-diff", data: Buffer.from(state.patch) }] : []
920
+ ];
921
+ for (const file of generated) {
922
+ let line;
923
+ try {
924
+ line = resourceLine(file.name, file.mediaType, file.data);
925
+ } catch {
926
+ leftOut.push({ path: file.name, reason: "could not be redacted safely" });
927
+ continue;
928
+ }
929
+ const size = Buffer.byteLength(line);
930
+ if (bytes + size > opts.budget) {
931
+ leftOut.push({ path: file.name, reason: "over the upload budget" });
932
+ continue;
933
+ }
934
+ bytes += size;
935
+ lines.push(line);
936
+ attached.push({ name: file.name, kind: "text", bytes: file.data.length });
937
+ }
938
+ for (const file of candidates) {
939
+ let data;
940
+ try {
941
+ data = readFileSync4(file.path);
942
+ } catch {
943
+ leftOut.push({ path: file.path, reason: "missing" });
944
+ continue;
945
+ }
946
+ if (file.kind === "text" && data.includes(0)) {
947
+ leftOut.push({ path: file.path, reason: "not valid UTF-8 text" });
948
+ continue;
949
+ }
950
+ let line;
951
+ try {
952
+ line = resourceLine(file.name, file.mediaType, data);
953
+ } catch {
954
+ leftOut.push({ path: file.path, reason: file.kind === "text" && !isUtf8(data) ? "not valid UTF-8 text" : "could not be redacted safely" });
955
+ continue;
956
+ }
957
+ const size = Buffer.byteLength(line);
958
+ if (bytes + size > opts.budget) {
959
+ leftOut.push({ path: file.path, reason: "over the upload budget" });
960
+ continue;
961
+ }
962
+ bytes += size;
963
+ lines.push(line);
964
+ attached.push({ name: file.name, kind: file.kind, bytes: data.length });
965
+ }
966
+ const setup = {
967
+ skills: skills2.found.map((skill) => skill.name),
968
+ missingSkills: skills2.missing,
969
+ mcpServers: state.manifest.mcp_servers.map((server) => server.name),
970
+ git: state.manifest.git !== null
971
+ };
972
+ return { lines, bytes, attached, leftOut, setup };
707
973
  }
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.");
974
+
975
+ // src/import-environment.ts
976
+ import { spawnSync as spawnSync2 } from "child_process";
977
+ import { createHash as createHash2 } from "crypto";
978
+ import { closeSync as closeSync2, createWriteStream, lstatSync, mkdtempSync, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, readSync as readSync2, rmSync as rmSync2 } from "fs";
979
+ import { tmpdir } from "os";
980
+ import { join as join5 } from "path";
981
+ import { isUtf8 as isUtf82 } from "buffer";
982
+ import { createGzip } from "zlib";
983
+
984
+ // ../../packages/spec/src/import-layout.ts
985
+ import { gzipSync } from "zlib";
986
+ function folderName(workspace2, sourceCwd) {
987
+ const raw = workspace2?.project || (sourceCwd ? sourceCwd.split("/").filter(Boolean).pop() : "") || "";
988
+ const cleaned = raw.replace(/[^A-Za-z0-9._ -]/g, "-").replace(/^[^A-Za-z0-9_]+/, "").replace(/\.\.+/g, ".").slice(0, 80).trim();
989
+ return cleaned || "imported-session";
990
+ }
991
+ function envName(server, field) {
992
+ return `${server}_${field}`.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
993
+ }
994
+ function ownServer(server, platform) {
995
+ try {
996
+ return server.transport !== "stdio" && platform.ownHost.test(new URL(server.url ?? "").hostname);
997
+ } catch {
998
+ return false;
713
999
  }
714
1000
  }
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}`)] : [];
1001
+ function mcpJson(servers, platform) {
1002
+ const entries = servers.filter((server) => !ownServer(server, platform)).map((server) => {
1003
+ if (server.transport === "stdio") {
1004
+ return [server.name, {
1005
+ command: server.command,
1006
+ ...server.args?.length ? { args: server.args } : {},
1007
+ ...server.env?.length ? { env: Object.fromEntries(server.env.map((name) => [name, `\${${name}:-}`])) } : {}
1008
+ }];
1009
+ }
1010
+ const query = server.query?.length ? `?${server.query.map((name) => `${encodeURIComponent(name)}=\${${envName(server.name, name)}:-}`).join("&")}` : "";
1011
+ return [server.name, {
1012
+ type: server.transport,
1013
+ url: `${server.url}${query}`,
1014
+ ...server.headers?.length ? { headers: Object.fromEntries(server.headers.map((name) => [name, `\${${envName(server.name, name)}:-}`])) } : {}
1015
+ }];
1016
+ });
1017
+ return JSON.stringify({ mcpServers: Object.fromEntries(entries) }, null, 2) + "\n";
1018
+ }
1019
+ function bullet(items, none) {
1020
+ return items.length ? items.map((item) => `- ${item}`).join("\n") : `- ${none}`;
1021
+ }
1022
+ function readme(folder, workspace2, counts, skills2, mcpWritten, platform) {
1023
+ const git2 = workspace2?.git;
1024
+ const servers = workspace2?.mcp_servers ?? [];
1025
+ const missingSkills = (workspace2?.skills ?? []).filter((skill) => !skill.found).map((skill) => skill.name);
1026
+ const secrets2 = servers.filter((server) => !ownServer(server, platform)).flatMap((server) => [
1027
+ ...server.env ?? [],
1028
+ ...(server.headers ?? []).map((name) => envName(server.name, name)),
1029
+ ...(server.query ?? []).map((name) => envName(server.name, name))
1030
+ ]);
1031
+ const where = workspace2?.cwd ? ` (${workspace2.cwd})` : "";
719
1032
  return [
720
- `@${agent2.handle} ${agent2.name}${mine(agent2)}`,
721
- agent2.tagline,
722
- ...agent2.description && agent2.description !== agent2.tagline ? ["", agent2.description] : [],
1033
+ "# Imported working copy",
723
1034
  "",
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),
1035
+ `This folder (/workspace/${folder}) holds what the imported conversation worked with on the person's computer.`,
1036
+ "It is historical context: verify a path before acting on it, and follow the person's current message.",
729
1037
  "",
730
- `Hire it: sky session create @${agent2.handle} --message "\u2026"`
1038
+ "## Files",
1039
+ 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\`)` : ""}.`,
1040
+ ...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/.`] : [],
1041
+ ...counts.attachments ? [`- ${counts.attachments} attachments of the conversation under ${platform.notesDir}/attachments/.`] : [],
1042
+ "- 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.",
1043
+ "",
1044
+ "## Repository",
1045
+ ...git2 ? [
1046
+ `- Remote: ${git2.remote ?? "none"}`,
1047
+ `- Branch: ${git2.branch ?? "detached"} at commit ${git2.commit ?? "unknown"}${git2.ahead ? ` (${git2.ahead} commits not pushed)` : ""}`,
1048
+ ...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."],
1049
+ 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.`
1050
+ ] : ["- The working directory was not a git repository."],
1051
+ "",
1052
+ "## Skills",
1053
+ bullet(skills2.map((name) => `${name}: .claude/skills/${name}/SKILL.md (also .agents/skills/${name}/)`), "No skills were loaded."),
1054
+ ...missingSkills.length ? ["", `Loaded but no longer on that computer, so not carried: ${missingSkills.join(", ")}.`] : [],
1055
+ "",
1056
+ "## Tool servers (MCP)",
1057
+ 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."),
1058
+ ...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.`] : [],
1059
+ ""
731
1060
  ].join("\n");
732
1061
  }
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) {
1062
+ function parseWorkspace(file) {
1063
+ if (!file) return null;
746
1064
  try {
747
- return readFileSync4(path, "utf8");
1065
+ const value = JSON.parse(file.data.toString("utf8"));
1066
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
748
1067
  } catch {
749
- return void 0;
1068
+ return null;
750
1069
  }
751
1070
  }
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;
1071
+ function importLayout(files2, sourceCwd, platform, wholeFolder) {
1072
+ if (!files2.length && !wholeFolder) return null;
1073
+ const workspace2 = parseWorkspace(files2.find((file) => file.name === "agentsky/workspace.json"));
1074
+ const folder = folderName(workspace2, sourceCwd);
1075
+ const entries = /* @__PURE__ */ new Map();
1076
+ const skills2 = /* @__PURE__ */ new Set();
1077
+ const counts = { project: wholeFolder?.files ?? 0, outside: 0, attachments: 0, wholeFolder: Boolean(wholeFolder) };
1078
+ for (const file of files2) {
1079
+ const [kind, ...rest] = file.name.split("/");
1080
+ const path = rest.join("/");
1081
+ if (kind === "workspace") {
1082
+ if (wholeFolder) continue;
1083
+ entries.set(path, file.data);
1084
+ counts.project += 1;
1085
+ } else if (kind === "files") {
1086
+ entries.set(`${platform.notesDir}/files/${path.replace(/^~\//, "home/")}`, file.data);
1087
+ counts.outside += 1;
1088
+ } else if (kind === "imported-attachments") {
1089
+ entries.set(`${platform.notesDir}/attachments/${path}`, file.data);
1090
+ counts.attachments += 1;
1091
+ } else if (kind === "agentsky") entries.set(`${platform.notesDir}/${path}`, file.data);
1092
+ else if (kind === "skills" && rest.length > 1) {
1093
+ const skill = rest[0].split(":").pop();
1094
+ const inside = rest.slice(1).join("/");
1095
+ skills2.add(skill);
1096
+ entries.set(`.claude/skills/${skill}/${inside}`, file.data);
1097
+ entries.set(`.agents/skills/${skill}/${inside}`, file.data);
1098
+ }
759
1099
  }
1100
+ const servers = (workspace2?.mcp_servers ?? []).filter((server) => server && typeof server.name === "string" && (server.transport === "stdio" ? typeof server.command === "string" : typeof server.url === "string"));
1101
+ const mcpWritten = servers.length > 0 && !entries.has(".mcp.json") && !wholeFolder?.paths.has(".mcp.json");
1102
+ if (mcpWritten) entries.set(".mcp.json", Buffer.from(mcpJson(servers, platform)));
1103
+ entries.set(`${platform.notesDir}/README.md`, Buffer.from(readme(folder, workspace2 ? { ...workspace2, mcp_servers: servers } : null, counts, [...skills2], mcpWritten, platform)));
1104
+ if (wholeFolder) for (const path of wholeFolder.paths) entries.delete(path);
1105
+ return { folder, entries, skills: [...skills2], mcpServers: servers.map((server) => server.name) };
760
1106
  }
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();
1107
+ var TAR_BLOCK = 512;
1108
+ var TAR_END = Buffer.alloc(TAR_BLOCK * 2);
1109
+ function header(name, size, type) {
1110
+ const block = Buffer.alloc(TAR_BLOCK);
1111
+ block.write(name, 0, 100, "utf8");
1112
+ block.write("0000644\0", 100);
1113
+ block.write("0000000\0", 108);
1114
+ block.write("0000000\0", 116);
1115
+ block.write(size.toString(8).padStart(11, "0") + "\0", 124);
1116
+ block.write("00000000000\0", 136);
1117
+ block.write(" ", 148);
1118
+ block.write(type, 156);
1119
+ block.write("ustar\x0000", 257);
1120
+ let sum = 0;
1121
+ for (const byte of block) sum += byte;
1122
+ block.write(sum.toString(8).padStart(6, "0") + "\0 ", 148);
1123
+ return block;
1124
+ }
1125
+ function paxRecord(key, value) {
1126
+ const body = ` ${key}=${value}
1127
+ `;
1128
+ let length = Buffer.byteLength(body);
1129
+ for (; ; ) {
1130
+ const total = String(length).length + length;
1131
+ if (String(total).length + length === total) return Buffer.from(`${total}${body}`);
1132
+ length += 1;
770
1133
  }
771
- return void 0;
772
1134
  }
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;
1135
+ function tarPadding(size) {
1136
+ const rest = size % TAR_BLOCK;
1137
+ return Buffer.alloc(rest ? TAR_BLOCK - rest : 0);
1138
+ }
1139
+ function tarHeader(path, size) {
1140
+ if (Buffer.byteLength(path) <= 100) return header(path, size, "0");
1141
+ const pax = paxRecord("path", path);
1142
+ return Buffer.concat([header("PaxHeader", pax.length, "x"), pax, tarPadding(pax.length), header(path, size, "0")]);
783
1143
  }
784
- function assemblePrompt(parts) {
785
- const joined = parts.map((p) => `# \u2500\u2500 ${p.label} \u2500\u2500
786
1144
 
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 };
1145
+ // src/import-environment.ts
1146
+ var IMPORT_PLATFORM = { name: "AgentSky", ownHost: /(^|\.)(agentsky\.dev|asteroidbelt\.ai)$/i, notesDir: ".agentsky-import" };
1147
+ var MAX_ENVIRONMENT_FILES = 2e5;
1148
+ var MAX_ENVIRONMENT_BYTES = 2 * 1024 ** 3;
1149
+ var MAX_REDACTED_TEXT_BYTES = 10 * 1024 * 1024;
1150
+ var SKIPPED = /* @__PURE__ */ new Set([".git", "node_modules", ".venv", "venv", "__pycache__", ".next", "dist", "build", "target", ".DS_Store", ".cache", ".turbo"]);
1151
+ 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;
1152
+ function listFiles(cwd) {
1153
+ const git2 = spawnSync2("git", ["-C", cwd, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 512 * 1024 * 1024 });
1154
+ if (git2.status === 0) return git2.stdout.split("\0").filter(Boolean).filter((path) => !path.split("/").some((part) => part === ".git"));
1155
+ const out = [];
1156
+ const walk = (dir, prefix) => {
1157
+ let entries;
1158
+ try {
1159
+ entries = readdirSync3(dir, { withFileTypes: true });
1160
+ } catch {
1161
+ return;
1162
+ }
1163
+ for (const entry2 of entries) {
1164
+ if (SKIPPED.has(entry2.name)) continue;
1165
+ const rel = prefix ? `${prefix}/${entry2.name}` : entry2.name;
1166
+ if (entry2.isDirectory()) walk(join5(dir, entry2.name), rel);
1167
+ else out.push(rel);
1168
+ if (out.length > MAX_ENVIRONMENT_FILES) return;
1169
+ }
1170
+ };
1171
+ walk(cwd, "");
1172
+ return out;
791
1173
  }
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;
1174
+ function textOf(path, size) {
1175
+ if (size > MAX_REDACTED_TEXT_BYTES) return null;
1176
+ const data = readFileSync5(path);
1177
+ if (data.subarray(0, 8e3).includes(0) || !isUtf82(data)) return null;
1178
+ return data;
1179
+ }
1180
+ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1181
+ const listed = listFiles(cwd);
1182
+ if (listed.length > MAX_ENVIRONMENT_FILES) throw new Error(`The working folder has more than ${MAX_ENVIRONMENT_FILES} files; import it with --no-environment.`);
1183
+ const scratch = mkdtempSync(join5(tmpdir(), "sky-environment-"));
1184
+ const path = join5(scratch, "environment.tar.gz");
1185
+ const leftOut = [];
1186
+ const written2 = /* @__PURE__ */ new Set();
1187
+ const hash = createHash2("sha256");
1188
+ let bytes = 0;
1189
+ let redacted2 = 0;
1190
+ const gzip = createGzip({ level: 6 });
1191
+ const sink = createWriteStream(path);
1192
+ gzip.on("data", (chunk) => {
1193
+ bytes += chunk.length;
1194
+ hash.update(chunk);
1195
+ });
1196
+ gzip.pipe(sink);
1197
+ const done = new Promise((resolve10, reject) => {
1198
+ sink.on("finish", () => resolve10());
1199
+ sink.on("error", reject);
1200
+ gzip.on("error", reject);
1201
+ });
1202
+ const write = (chunk) => new Promise((resolve10, reject) => {
1203
+ if (bytes > MAX_ENVIRONMENT_BYTES) {
1204
+ reject(new Error(`The working folder packs to more than ${MAX_ENVIRONMENT_BYTES / 1024 ** 3} GiB; import it with --no-environment.`));
1205
+ return;
807
1206
  }
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 });
1207
+ if (gzip.write(chunk)) resolve10();
1208
+ else gzip.once("drain", resolve10);
1209
+ });
1210
+ try {
1211
+ for (const rel of listed) {
1212
+ const full = join5(cwd, ...rel.split("/"));
1213
+ if (SECRET_FILE2.test(rel)) {
1214
+ leftOut.push({ path: rel, reason: "credential file" });
1215
+ continue;
1216
+ }
1217
+ let stat;
1218
+ try {
1219
+ stat = lstatSync(full);
1220
+ } catch {
1221
+ continue;
1222
+ }
1223
+ if (!stat.isFile()) {
1224
+ leftOut.push({ path: rel, reason: "not a regular file" });
1225
+ continue;
1226
+ }
1227
+ const text = textOf(full, stat.size);
1228
+ if (text) {
1229
+ const masked = redactSecretValues(text.toString("utf8"));
1230
+ if (Object.keys(masked.hits).length) redacted2 += 1;
1231
+ const data = Buffer.from(masked.text, "utf8");
1232
+ await write(tarHeader(rel, data.length));
1233
+ await write(data);
1234
+ await write(tarPadding(data.length));
1235
+ } else {
1236
+ await write(tarHeader(rel, stat.size));
1237
+ const fd = openSync2(full, "r");
1238
+ try {
1239
+ const chunk = Buffer.alloc(1024 * 1024);
1240
+ let left = stat.size;
1241
+ for (let offset = 0; left > 0; ) {
1242
+ const read = readSync2(fd, chunk, 0, Math.min(chunk.length, left), offset);
1243
+ if (read <= 0) throw new Error(`${rel} changed while it was packed; import again.`);
1244
+ await write(Buffer.from(chunk.subarray(0, read)));
1245
+ offset += read;
1246
+ left -= read;
1247
+ }
1248
+ } finally {
1249
+ closeSync2(fd);
1250
+ }
1251
+ await write(tarPadding(stat.size));
820
1252
  }
1253
+ written2.add(rel);
1254
+ }
1255
+ const layout = importLayout(resources, cwd, platform, { files: written2.size, paths: written2 });
1256
+ for (const [entry2, data] of layout?.entries ?? []) {
1257
+ await write(tarHeader(entry2, data.length));
1258
+ await write(data);
1259
+ await write(tarPadding(data.length));
821
1260
  }
1261
+ await write(TAR_END);
1262
+ gzip.end();
1263
+ await done;
1264
+ 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.`);
1265
+ return {
1266
+ path,
1267
+ folder: layout?.folder ?? "imported-session",
1268
+ bytes,
1269
+ sha256: hash.digest("hex"),
1270
+ files: written2.size + (layout?.entries.size ?? 0),
1271
+ redacted: redacted2,
1272
+ leftOut,
1273
+ dispose: () => rmSync2(scratch, { recursive: true, force: true })
1274
+ };
1275
+ } catch (error) {
1276
+ gzip.destroy();
1277
+ sink.destroy();
1278
+ rmSync2(scratch, { recursive: true, force: true });
1279
+ throw error;
822
1280
  }
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
1281
  }
835
- function detectHarness(explicit) {
836
- if (explicit) {
837
- if (!HARNESSES.includes(explicit)) {
838
- throw new Error(`unknown harness ${explicit} \u2014 expected ${HARNESSES.join(" | ")}`);
1282
+ function resourcesOf(lines) {
1283
+ return lines.flatMap((line) => {
1284
+ try {
1285
+ const record = JSON.parse(line);
1286
+ if (record.type !== "import.resource" || typeof record.name !== "string" || typeof record.media_type !== "string" || typeof record.data !== "string") return [];
1287
+ return [{ name: record.name, mediaType: record.media_type, data: Buffer.from(record.data, "base64") }];
1288
+ } catch {
1289
+ return [];
839
1290
  }
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
- );
1291
+ });
848
1292
  }
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
- );
1293
+
1294
+ // src/import-local.ts
1295
+ function redactLine2(raw) {
1296
+ return redactLine(raw).line;
854
1297
  }
855
- function linkPath(configRoot) {
856
- return join3(configRoot, "agentsky-link.json");
1298
+ function decodeWorkspace(encoded) {
1299
+ try {
1300
+ return decodeURIComponent(encoded.replace(/-/g, "%").replace(/_/g, "%5F"));
1301
+ } catch {
1302
+ return encoded;
1303
+ }
857
1304
  }
858
- function readLink(configRoot) {
859
- const link2 = readJson(linkPath(configRoot));
860
- return link2?.agentId ? link2 : void 0;
1305
+ function readPrefix(filePath) {
1306
+ const fd = openSync3(filePath, "r");
1307
+ try {
1308
+ const bytes = Buffer.alloc(256 * 1024);
1309
+ return bytes.subarray(0, readSync3(fd, bytes, 0, bytes.length, 0)).toString("utf8");
1310
+ } finally {
1311
+ closeSync3(fd);
1312
+ }
861
1313
  }
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;
1314
+ function sniffTitle(filePath) {
877
1315
  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;
1316
+ const data = readPrefix(filePath);
1317
+ for (const raw of data.split("\n")) {
1318
+ if (!raw.trim()) continue;
1319
+ let line;
1320
+ try {
1321
+ line = JSON.parse(redactLine2(raw));
1322
+ } catch {
1323
+ continue;
1324
+ }
1325
+ if (!line || typeof line !== "object") continue;
1326
+ const rec = line;
1327
+ if (rec.type === "user" && rec.message && typeof rec.message === "object") {
1328
+ const msg = rec.message;
1329
+ if (Array.isArray(msg.content)) {
1330
+ for (const block of msg.content) {
1331
+ if (typeof block === "object" && block !== null) {
1332
+ const b = block;
1333
+ if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
1334
+ return b.text.trim().slice(0, 120);
1335
+ }
1336
+ }
1337
+ }
1338
+ }
1339
+ if (typeof msg.content === "string" && msg.content.trim()) {
1340
+ return msg.content.trim().slice(0, 120);
1341
+ }
1342
+ }
1343
+ if (rec.type === "event_msg" && rec.payload && typeof rec.payload === "object") {
1344
+ const payload = rec.payload;
1345
+ if (payload.type === "user_message" && typeof payload.message === "string") return redactLine2(payload.message).slice(0, 120);
1346
+ }
1347
+ if (rec.type === "response_item" && rec.payload && typeof rec.payload === "object") {
1348
+ const payload = rec.payload;
1349
+ if (payload.type === "message" && payload.role === "user" && Array.isArray(payload.content)) {
1350
+ const text = payload.content.find((part) => typeof part?.text === "string")?.text;
1351
+ if (typeof text === "string" && text.trim()) return redactLine2(text.trim()).slice(0, 120);
1352
+ }
1353
+ }
1354
+ if (rec.type === "session_meta") continue;
1355
+ if (rec.type === "user_turn" || rec.type === "message" && rec.role === "user") {
1356
+ const content = rec.content;
1357
+ if (typeof content === "string" && content.trim()) return content.trim().slice(0, 120);
1358
+ }
886
1359
  }
887
- throw error;
1360
+ return null;
1361
+ } catch {
1362
+ return null;
888
1363
  }
889
1364
  }
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;
1365
+ function listClaudeCodeSessions(limit) {
1366
+ const root = process.env.CLAUDE_CONFIG_DIR ?? join6(homedir3(), ".claude");
1367
+ const projectsDir = join6(root, "projects");
1368
+ if (!existsSync4(projectsDir)) return [];
1369
+ const sessions = [];
1370
+ try {
1371
+ for (const encoded of readdirSync4(projectsDir)) {
1372
+ const projectDir = join6(projectsDir, encoded);
1373
+ let entries;
1374
+ try {
1375
+ entries = readdirSync4(projectDir);
1376
+ } catch {
1377
+ continue;
1378
+ }
1379
+ const workspace2 = decodeWorkspace(encoded);
1380
+ for (const entry2 of entries) {
1381
+ if (!entry2.endsWith(".jsonl")) continue;
1382
+ const sessionId = entry2.slice(0, -6);
1383
+ const mainFile = join6(projectDir, entry2);
1384
+ let stat;
1385
+ try {
1386
+ stat = statSync5(mainFile);
1387
+ } catch {
1388
+ continue;
1389
+ }
1390
+ if (!stat.isFile()) continue;
1391
+ let discoveryError;
1392
+ const subagentFiles = [];
1393
+ const subagentDir = join6(projectDir, sessionId, "subagents");
1394
+ if (existsSync4(subagentDir)) {
1395
+ try {
1396
+ const subFiles = readdirSync4(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
1397
+ for (const f of subFiles) {
1398
+ subagentFiles.push(join6(subagentDir, f));
1399
+ }
1400
+ } catch {
1401
+ discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
1402
+ }
1403
+ }
1404
+ sessions.push({
1405
+ harness: "claude_code",
1406
+ sessionId,
1407
+ workspace: workspace2,
1408
+ title: sniffTitle(mainFile),
1409
+ mtimeMs: stat.mtimeMs,
1410
+ files: [mainFile, ...subagentFiles],
1411
+ ...discoveryError ? { discoveryError } : {}
1412
+ });
1413
+ }
909
1414
  }
1415
+ } catch {
1416
+ }
1417
+ return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
1418
+ }
1419
+ function listCodexSessions(limit) {
1420
+ const sessionsDir = join6(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join6(homedir3(), ".codex"), "sessions");
1421
+ const records = [];
1422
+ function walk(dir) {
1423
+ let entries;
910
1424
  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}`);
1425
+ entries = readdirSync4(dir, { withFileTypes: true });
1426
+ } catch {
1427
+ return;
1428
+ }
1429
+ for (const entry2 of entries) {
1430
+ const full = join6(dir, entry2.name);
1431
+ if (entry2.isDirectory()) {
1432
+ walk(full);
1433
+ continue;
1434
+ }
1435
+ if (!entry2.isFile() || !entry2.name.startsWith("rollout-") || !entry2.name.endsWith(".jsonl")) continue;
1436
+ try {
1437
+ const first = readPrefix(full).split("\n", 1)[0];
1438
+ const meta = JSON.parse(first);
1439
+ if (meta.type !== "session_meta" || typeof meta.payload?.id !== "string") continue;
1440
+ const data = meta.payload;
1441
+ const parent = data.forked_from_id ?? data.source?.spawn?.parent_thread_id ?? data.source?.subagent?.spawn?.parent_thread_id;
1442
+ records.push({
1443
+ session: {
1444
+ harness: "codex",
1445
+ sessionId: data.id,
1446
+ workspace: typeof data.cwd === "string" ? data.cwd : dir,
1447
+ title: sniffTitle(full),
1448
+ mtimeMs: statSync5(full).mtimeMs,
1449
+ files: [full]
1450
+ },
1451
+ ...typeof parent === "string" ? { parent } : {}
1452
+ });
1453
+ } catch {
1454
+ continue;
929
1455
  }
930
1456
  }
931
1457
  }
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
- );
1458
+ walk(sessionsDir);
1459
+ function descendants(id, visited) {
1460
+ if (visited.has(id)) return [];
1461
+ visited.add(id);
1462
+ return records.filter((item) => item.parent === id).flatMap((item) => [
1463
+ ...item.session.files,
1464
+ ...descendants(item.session.sessionId, visited)
1465
+ ]);
937
1466
  }
1467
+ return records.filter((item) => !item.parent).map(({ session: session2 }) => ({
1468
+ ...session2,
1469
+ files: [.../* @__PURE__ */ new Set([...session2.files, ...descendants(session2.sessionId, /* @__PURE__ */ new Set())])]
1470
+ })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
938
1471
  }
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
- });
1472
+ function listRecentSessions(harness2, limit) {
1473
+ const claudeSessions = harness2 === "codex" ? [] : listClaudeCodeSessions(limit);
1474
+ const codexSessions = harness2 === "claude_code" ? [] : listCodexSessions(limit);
1475
+ return [...claudeSessions, ...codexSessions].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
1476
+ }
1477
+ function agentUseBase() {
1478
+ const env = process.env.AGENT_USE_URL?.trim().replace(/\/$/, "");
1479
+ if (env) return env;
1480
+ const api2 = apiBase().replace(/\/$/, "");
1481
+ const derived = api2.replace(/\/api\/v1$/, "").replace(/\/v1$/, "").replace("api.agentsky.dev", "agentsky.dev");
1482
+ if (derived.includes("localhost:3000")) return derived.replace("localhost:3000", "localhost:3002");
1483
+ return derived;
1484
+ }
1485
+ function currentSessionEnv() {
1486
+ const claudeId = process.env.CLAUDE_CODE_SESSION_ID;
1487
+ if (claudeId) return { harness: "claude_code", sessionId: claudeId };
1488
+ const codexId = process.env.CODEX_THREAD_ID;
1489
+ if (codexId) return { harness: "codex", sessionId: codexId };
1490
+ return void 0;
1491
+ }
1492
+ async function mintTrace(base, harness2) {
1493
+ const token = apiToken();
1494
+ if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
1495
+ const res = await fetch(`${base}/api/traces`, {
1496
+ method: "POST",
1497
+ redirect: "error",
1498
+ signal: AbortSignal.timeout(15e3),
1499
+ headers: {
1500
+ "content-type": "application/json",
1501
+ authorization: `Bearer ${token}`,
1502
+ "user-agent": USER_AGENT
1503
+ },
1504
+ body: JSON.stringify({ harness: harness2 })
946
1505
  });
1506
+ if (!res.ok) {
1507
+ const body = await res.json().catch(() => null);
1508
+ throw new Error(
1509
+ typeof body?.error === "string" ? body.error : `Failed to create import (${res.status}).`
1510
+ );
1511
+ }
1512
+ return await res.json();
947
1513
  }
948
- function openBrowser(url) {
949
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1514
+ function loadSummaryFile(summaryPath) {
1515
+ let raw;
950
1516
  try {
951
- spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
1517
+ raw = readFileSync6(summaryPath, "utf8");
952
1518
  } catch {
1519
+ throw new Error(`Cannot read summary file: ${summaryPath}`);
953
1520
  }
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;
1309
- }
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
- };
1326
- }
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;
1346
- }
1347
- function codexThreadName(sessionId, home = homedir2()) {
1348
- let name = null;
1521
+ let pkg;
1349
1522
  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;
1352
- 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();
1355
- } catch {
1356
- }
1357
- }
1523
+ pkg = JSON.parse(raw);
1358
1524
  } catch {
1525
+ throw new Error("Summary file is not valid JSON.");
1359
1526
  }
1360
- return name;
1361
- }
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;
1414
- }
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");
1527
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) throw new Error("Summary file must be a JSON object.");
1528
+ const p = pkg;
1529
+ if (p.format !== "asteroids.session-summary") throw new Error('Summary file must have format "asteroids.session-summary".');
1530
+ if (p.format_version !== 1) throw new Error("Summary file must have format_version 1.");
1531
+ const redacted2 = redactLine2(raw.trimEnd());
1532
+ return redacted2;
1425
1533
  }
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);
1534
+ var MAX_IMPORT_BYTES = 30 * 1024 * 1024;
1535
+ var SUMMARY_RESERVE_BYTES = 128 * 1024;
1536
+ var MAX_LISTED_ARTIFACTS = 99;
1537
+ function leftOutLine(left) {
1538
+ const counts = /* @__PURE__ */ new Map();
1539
+ for (const file of left) counts.set(file.reason, (counts.get(file.reason) ?? 0) + 1);
1540
+ const files2 = [...counts].map(([reason, n]) => `${n} ${n === 1 ? "file" : "files"} (${reason})`).join(", ");
1541
+ return `Not transferred: ${files2 ? `${files2}; ` : ""}runtime state such as running processes, installed tools and environment variables; and reasoning the source harness stored unreadably.`;
1542
+ }
1543
+ function prepareImport(session2, summaryPath, options = {}) {
1544
+ if (session2.discoveryError) throw new Error(session2.discoveryError);
1545
+ const maximum = 16 * 1024 * 1024;
1546
+ let inputBytes = 0;
1547
+ let outputBytes = 0;
1548
+ const lines = [];
1549
+ const digest = createHash3("sha256");
1550
+ let nativeId;
1551
+ let redactionCount = 0;
1552
+ let encryptedReasoningCount = 0;
1553
+ let compactionCount = 0;
1554
+ let unreadableCount = 0;
1555
+ const source = new SessionSourceCollector();
1556
+ const records = [];
1557
+ for (const path of session2.files) {
1558
+ const stat = statSync5(path);
1559
+ if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
1560
+ const text = readFileSync6(path, "utf8");
1561
+ for (const raw of text.split("\n")) {
1562
+ if (!raw.trim()) continue;
1563
+ let record;
1564
+ try {
1565
+ record = JSON.parse(raw);
1566
+ } catch {
1567
+ unreadableCount++;
1568
+ continue;
1451
1569
  }
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;
1570
+ if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("Transcript records must be JSON objects.");
1571
+ const rec = record;
1572
+ if (String(rec.type).startsWith("import.")) throw new Error("Native transcripts cannot supply import protocol records.");
1573
+ if (path === session2.files[0]) source.push(rec);
1574
+ records.push(rec);
1575
+ if (rec.type === "response_item") {
1576
+ const payload = rec.payload;
1577
+ if (payload?.type === "reasoning" && typeof payload?.encrypted_content === "string" && payload.encrypted_content) {
1578
+ encryptedReasoningCount++;
1461
1579
  }
1462
1580
  }
1463
- call(str(payload.name), input);
1581
+ if (rec.type === "compacted") {
1582
+ compactionCount++;
1583
+ }
1584
+ const id = rec.type === "session_meta" ? rec.payload?.id : rec.sessionId;
1585
+ if (!nativeId && typeof id === "string") nativeId = id;
1586
+ const result = redactLine(raw.replace(/\r$/, ""), { requireStructured: true });
1587
+ if (Object.keys(result.hits).length > 0) redactionCount++;
1588
+ const line = result.line + "\n";
1589
+ outputBytes += Buffer.byteLength(line);
1590
+ if (outputBytes > maximum) throw new Error("Redacted transcript exceeds the 16 MiB limit.");
1591
+ digest.update(line);
1592
+ lines.push(line);
1464
1593
  }
1465
1594
  }
1466
- return [...out];
1467
- }
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;
1595
+ if (!lines.length) throw new Error("The selected transcript is empty.");
1596
+ const sourceId = nativeId ?? session2.sessionId;
1597
+ 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.");
1598
+ const meta = source.result();
1599
+ const files2 = options.files === false ? { lines: [], bytes: 0, attached: [], leftOut: [], setup: { skills: [], missingSkills: [], mcpServers: [], git: false } } : collectSessionFiles(records, {
1600
+ harness: session2.harness,
1601
+ cwd: meta.cwd ?? (session2.harness === "claude_code" ? session2.workspace : ""),
1602
+ budget: MAX_IMPORT_BYTES - outputBytes - SUMMARY_RESERVE_BYTES,
1603
+ exclude: session2.files,
1604
+ title: meta.title ?? (session2.harness === "codex" ? codexThreadName(session2.sessionId) : null)
1605
+ });
1606
+ 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).` : "";
1607
+ let summaryJson;
1608
+ if (summaryPath) {
1609
+ summaryJson = loadSummaryFile(summaryPath);
1610
+ } else {
1611
+ const summary = {
1612
+ format: "asteroids.session-summary",
1613
+ format_version: 1,
1614
+ source_harness: session2.harness,
1615
+ source_harness_version: meta.harness_version?.slice(0, 100) ?? null,
1616
+ source_session_id: sourceId,
1617
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1618
+ 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.`,
1619
+ context: {
1620
+ objective: "Review the imported conversation and continue its latest unfinished authorized objective.",
1621
+ constraints: [],
1622
+ decisions: [],
1623
+ completed: [],
1624
+ pending: ["Read the complete imported transcript, reconcile the latest human instructions and completed work, then continue."],
1625
+ verification: [],
1626
+ 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(", ")}.`] : []],
1627
+ 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).` })),
1628
+ provenance: { native_checkpoint_sha256: digest.digest("hex"), selected_native_session_id: sourceId, context_bytes: outputBytes }
1629
+ }
1630
+ };
1631
+ summaryJson = JSON.stringify(summary);
1472
1632
  }
1633
+ 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";
1634
+ const gaps = [];
1635
+ if (encryptedReasoningCount > 0) gaps.push({ kind: "encrypted_reasoning", count: encryptedReasoningCount });
1636
+ if (compactionCount > 0) gaps.push({ kind: "compaction", count: compactionCount });
1637
+ if (unreadableCount > 0) gaps.push({ kind: "unreadable", count: unreadableCount });
1638
+ const cwd = meta.cwd ?? (session2.harness === "claude_code" ? session2.workspace : "");
1639
+ return { body, transcriptLines: lines.length, outputBytes, redactionCount, gaps, attachedFiles: files2.attached, leftOutFiles: files2.leftOut, setup: files2.setup, cwd, resourceLines: files2.lines };
1473
1640
  }
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
- }
1641
+ async function importSession(opts) {
1642
+ const base = (opts.agentUseUrl ?? agentUseBase()).replace(/\/$/, "");
1643
+ const destination2 = new URL(base);
1644
+ if (destination2.username || destination2.password || destination2.protocol !== "https:" && !(destination2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(destination2.hostname))) {
1645
+ throw new Error("Import requires HTTPS, or a loopback development server.");
1482
1646
  }
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
- });
1493
- }
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("/")}`;
1500
- }
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";
1504
- }
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);
1543
- 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 });
1647
+ opts.onProgress?.("Reading transcript and redacting secrets locally\u2026");
1648
+ const { body, transcriptLines, outputBytes, redactionCount, gaps, attachedFiles, leftOutFiles, setup, cwd, resourceLines } = prepareImport(opts.session, opts.summaryPath, { files: opts.files });
1649
+ let traceId;
1650
+ let key;
1651
+ if (opts.importKey && opts.grantTraceId) {
1652
+ traceId = opts.grantTraceId;
1653
+ key = opts.importKey;
1654
+ } else {
1655
+ opts.onProgress?.("Minting import key\u2026");
1656
+ ({ traceId, key } = await mintTrace(base, opts.session.harness));
1568
1657
  }
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" });
1586
- continue;
1587
- }
1588
- const size = Buffer.byteLength(line);
1589
- if (bytes + size > opts.budget) {
1590
- leftOut.push({ path: file.path, reason: "over the upload budget" });
1591
- continue;
1592
- }
1593
- bytes += size;
1594
- lines.push(line);
1595
- attached.push({ name: file.name, kind: file.kind, bytes: data.length });
1658
+ opts.onProgress?.("Uploading the complete transcript\u2026");
1659
+ const response = await fetch(`${base}/api/traces/ingest`, {
1660
+ method: "POST",
1661
+ redirect: "error",
1662
+ signal: AbortSignal.timeout(12e4),
1663
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/x-ndjson", "x-source-harness": opts.session.harness, "user-agent": USER_AGENT },
1664
+ body
1665
+ });
1666
+ if (!response.ok) {
1667
+ const result2 = await response.json().catch(() => null);
1668
+ throw new Error(typeof result2?.error === "string" ? result2.error : result2?.error?.message ?? `Upload failed (${response.status}).`);
1596
1669
  }
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
1670
+ const result = await response.json();
1671
+ const finalTraceId = result.traceId ?? traceId;
1672
+ 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) }));
1673
+ return {
1674
+ traceId: finalTraceId,
1675
+ url: `${base}/playground/new?import=${encodeURIComponent(finalTraceId)}`,
1676
+ harness: opts.session.harness,
1677
+ sessionId: opts.session.sessionId,
1678
+ transcriptFiles: opts.session.files.length,
1679
+ bytes: outputBytes,
1680
+ redactionCount,
1681
+ gaps,
1682
+ attachedFiles,
1683
+ leftOutFiles,
1684
+ setup,
1685
+ environment
1602
1686
  };
1603
- return { lines, bytes, attached, leftOut, setup };
1604
1687
  }
1605
-
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";
1688
+ async function grantCall(base, key, path, body) {
1689
+ const response = await fetch(`${base}${path}`, {
1690
+ method: "POST",
1691
+ redirect: "error",
1692
+ signal: AbortSignal.timeout(3e5),
1693
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json", "user-agent": USER_AGENT },
1694
+ body: JSON.stringify(body)
1695
+ });
1696
+ const answer = await response.json().catch(() => null);
1697
+ if (!response.ok) throw new Error(typeof answer?.error === "string" ? answer.error : answer?.error?.message ?? `The environment upload failed (${response.status}).`);
1698
+ return answer;
1621
1699
  }
1622
- function envName(server, field) {
1623
- return `${server}_${field}`.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
1700
+ async function uploadEnvironment(base, key, traceId, cwd, resourceLines, onProgress) {
1701
+ onProgress?.("Packing the working folder (secrets redacted locally)\u2026");
1702
+ const packed = await packEnvironment(cwd, resourcesOf(resourceLines));
1703
+ try {
1704
+ const path = `/api/traces/${encodeURIComponent(traceId)}/environment`;
1705
+ const { upload } = await grantCall(base, key, path, { name: packed.folder, size: packed.bytes, sha256: packed.sha256, files: packed.files });
1706
+ let shown = -1;
1707
+ await transfer(upload, packed.path, (sent, total) => {
1708
+ const percent = Math.floor(sent / total * 100);
1709
+ if (percent >= shown + 10 || sent === total) {
1710
+ shown = percent;
1711
+ onProgress?.(`Uploading the working folder\u2026 ${percent}% of ${(total / 1024 / 1024).toFixed(1)} MiB`);
1712
+ }
1713
+ });
1714
+ await grantCall(base, key, `${path}/complete`, {});
1715
+ return { folder: packed.folder, files: packed.files, bytes: packed.bytes, redacted: packed.redacted, leftOut: packed.leftOut.length };
1716
+ } finally {
1717
+ packed.dispose();
1718
+ }
1624
1719
  }
1625
- function ownServer(server, platform) {
1720
+
1721
+ // src/context.ts
1722
+ async function contextRequest(path, universe2) {
1626
1723
  try {
1627
- return server.transport !== "stdio" && platform.ownHost.test(new URL(server.url ?? "").hostname);
1628
- } catch {
1629
- return false;
1724
+ return await api("GET", `/api${path}`, void 0, { base: agentUseBase(), universe: universe2 });
1725
+ } catch (error) {
1726
+ if (!(error instanceof ApiError) || ![401, 403, 409].includes(error.status)) throw error;
1727
+ return api("GET", path, void 0, { universe: universe2 });
1630
1728
  }
1631
1729
  }
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";
1730
+ async function searchContext(query, opts, universe2) {
1731
+ const page = await contextRequest(`/context/search${searchQuery(query, opts)}`, universe2);
1732
+ return { ...page, hits: page.hits.map((entry2) => "hit" in entry2 ? entry2.hit : entry2) };
1649
1733
  }
1650
- function bullet(items, none) {
1651
- return items.length ? items.map((item) => `- ${item}`).join("\n") : `- ${none}`;
1734
+ function memoryBody(operation, version) {
1735
+ return { ...operation, expectedVersion: version };
1652
1736
  }
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})` : "";
1663
- 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."],
1682
- "",
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(", ")}.`] : [],
1686
- "",
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
- ""
1691
- ].join("\n");
1737
+ function memoryIndex(value) {
1738
+ if (!/^\d+$/.test(value.trim())) throw new Error(`the entry number must be a whole number, got ${JSON.stringify(value)}`);
1739
+ return Number(value.trim());
1692
1740
  }
1693
- function parseWorkspace(file) {
1694
- if (!file) return null;
1695
- try {
1696
- const value = JSON.parse(file.data.toString("utf8"));
1697
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1698
- } catch {
1699
- return null;
1741
+ function renderMemory(document) {
1742
+ if (!document.entries.length) return "no saved memory \u2014 `sky memory save <text>` adds an entry";
1743
+ return document.entries.map((entry2) => `${String(entry2.index).padStart(3)} ${entry2.text}`).join("\n");
1744
+ }
1745
+ function searchQuery(query, opts) {
1746
+ const params = new URLSearchParams({ q: query });
1747
+ params.set("scope", opts.scope ?? "everything");
1748
+ if (opts.limit) params.set("limit", opts.limit);
1749
+ if (opts.cursor) params.set("cursor", opts.cursor);
1750
+ if (opts.since) params.set("since", opts.since);
1751
+ if (opts.until) params.set("until", opts.until);
1752
+ return `?${params.toString()}`;
1753
+ }
1754
+ function refArgument(ref) {
1755
+ return `'${JSON.stringify(ref).replace(/'/g, "'\\''")}'`;
1756
+ }
1757
+ function renderContextPage(page) {
1758
+ const lines = [];
1759
+ for (const hit of page.hits) {
1760
+ lines.push(`${hit.path ?? hit.title}${hit.line ? `:${hit.line}` : ""}: ${hit.snippet.replace(/\s+/g, " ").slice(0, 160)}`);
1761
+ lines.push(` sky context read ${refArgument(hit.ref)}`);
1700
1762
  }
1763
+ if (!page.hits.length) lines.push("no matches on this page");
1764
+ const missed = (page.sources ?? []).filter((source) => source.status === "failed" || source.status === "partial").map((source) => source.source);
1765
+ if (page.partial) lines.push(`partial: ${missed.length ? `${missed.join(", ")} not fully searched` : "not every source was searched"}`);
1766
+ if (page.nextCursor) lines.push(`more: --cursor ${page.nextCursor}`);
1767
+ return lines.join("\n");
1701
1768
  }
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);
1769
+
1770
+ // src/agent-file.ts
1771
+ import { existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
1772
+ import { dirname as dirname4, resolve as resolve3 } from "path";
1773
+ import { parse as parseToml2 } from "smol-toml";
1774
+ var fileInstructionSchema = external_exports.object({
1775
+ name: external_exports.string(),
1776
+ content: external_exports.string().optional(),
1777
+ file: external_exports.string().optional()
1778
+ });
1779
+ var agentFileSchema = external_exports.object({
1780
+ name: external_exports.string().min(1),
1781
+ id: external_exports.string().optional(),
1782
+ // stamped by launch/deploy; identifies the live agent
1783
+ slug: external_exports.string().optional(),
1784
+ // pre-RFC-0079 stamp; ignored (agents are id-addressed)
1785
+ description: external_exports.string().optional(),
1786
+ type: external_exports.enum(AGENT_TYPES).optional(),
1787
+ llm: external_exports.string().optional(),
1788
+ reasoning_effort: external_exports.enum(REASONING_EFFORT_VALUES).optional(),
1789
+ capabilities: capabilitiesSchema.removeDefault().optional(),
1790
+ // omitted = leave the agent's as they are
1791
+ prompt: external_exports.string().optional(),
1792
+ prompt_file: external_exports.string().optional(),
1793
+ instructions: external_exports.array(fileInstructionSchema).optional(),
1794
+ skills: external_exports.array(skillRefSchema).optional(),
1795
+ customData: external_exports.array(customDataSchema).optional(),
1796
+ /** "deny" makes the agent work alone: its sessions may not hire helpers. */
1797
+ hiring_policy: external_exports.enum(["allow", "deny"]).optional(),
1798
+ /** The environment its sessions boot in when they name none. */
1799
+ environment: external_exports.string().min(1).optional(),
1800
+ /** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
1801
+ vcpus: external_exports.number().int().optional(),
1802
+ memory_mb: external_exports.number().int().optional()
1803
+ });
1804
+ function readAgentFile(path) {
1805
+ const raw = readFileSync7(path, "utf8");
1806
+ const parsed = agentFileSchema.safeParse(parseToml2(raw));
1807
+ if (!parsed.success) {
1808
+ const issue = parsed.error.issues[0];
1809
+ throw new Error(`${path}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? "invalid"}`);
1810
+ }
1811
+ const file = parsed.data;
1812
+ if (file.reasoning_effort && file.type && !isModelEffortSupported(file.type, file.llm ?? defaultLlm(file.type), file.reasoning_effort)) {
1813
+ throw new Error(
1814
+ `${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
1815
+ );
1816
+ }
1817
+ const base = dirname4(resolve3(path));
1818
+ const inline = (ref, inlineContent) => {
1819
+ if (inlineContent !== void 0) return inlineContent;
1820
+ if (!ref) return void 0;
1821
+ return readFileSync7(resolve3(base, ref), "utf8");
1822
+ };
1823
+ const spec = {
1824
+ name: file.name,
1825
+ description: file.description,
1826
+ agentType: file.type,
1827
+ llm: file.llm,
1828
+ reasoningEffort: file.reasoning_effort,
1829
+ displayName: file.name,
1830
+ capabilities: file.capabilities,
1831
+ prompt: inline(file.prompt_file, file.prompt),
1832
+ instructions: file.instructions?.map((f) => {
1833
+ const content = inline(f.file, f.content);
1834
+ if (content === void 0) throw new Error(`instruction ${f.name}: content or file required`);
1835
+ return { name: f.name, content };
1836
+ }),
1837
+ skills: file.skills,
1838
+ customData: file.customData,
1839
+ hiringPolicy: file.hiring_policy,
1840
+ environmentId: file.environment,
1841
+ vcpus: file.vcpus,
1842
+ memoryMb: file.memory_mb
1843
+ };
1844
+ return { file, spec };
1845
+ }
1846
+ function stampId(path, id) {
1847
+ writeFileSync2(path, stampIdText(readFileSync7(path, "utf8"), id));
1848
+ }
1849
+ function stampIdText(text, id) {
1850
+ const out = [];
1851
+ let topLevel = true;
1852
+ let openString = null;
1853
+ let stamped = false;
1854
+ let nameAt = -1;
1855
+ for (const line of text.split("\n")) {
1856
+ if (openString) {
1857
+ if (line.includes(openString)) openString = null;
1858
+ out.push(line);
1859
+ continue;
1860
+ }
1861
+ if (/^\s*\[/.test(line)) topLevel = false;
1862
+ if (topLevel && /^\s*(id|slug)\s*=/.test(line)) {
1863
+ if (/^\s*id\s*=/.test(line) && !stamped) {
1864
+ out.push(`id = ${JSON.stringify(id)}`);
1865
+ stamped = true;
1866
+ }
1867
+ continue;
1729
1868
  }
1869
+ if (topLevel && nameAt < 0 && /^\s*name\s*=/.test(line)) nameAt = out.length;
1870
+ const opener = /=\s*("""|''')/.exec(line);
1871
+ if (opener && !line.slice(opener.index + opener[0].length).includes(opener[1])) openString = opener[1];
1872
+ out.push(line);
1730
1873
  }
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;
1874
+ if (!stamped) out.splice(nameAt + 1, 0, `id = ${JSON.stringify(id)}`);
1875
+ const next = out.join("\n");
1876
+ if (parseToml2(next).id !== id) throw new Error("could not stamp the agent id into the file");
1877
+ return next;
1755
1878
  }
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;
1764
- }
1879
+ function scaffoldAgentFile(path, name) {
1880
+ if (existsSync5(path)) throw new Error(`${path} already exists`);
1881
+ writeFileSync2(
1882
+ path,
1883
+ `# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
1884
+ name = "${name}"
1885
+ # type = "pi" # pi (default) | hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode
1886
+ # llm = "deepseek-flash" # must be compatible with type
1887
+ # reasoning_effort = "high" # per harness \u2014 ${reasoningEffortDocLine()}
1888
+ # capabilities = []
1889
+ # hiring_policy = "deny" # its sessions work alone; "allow" (default) lets them hire helpers
1890
+ # environment = "env_..." # the environment its sessions boot in when they name none
1891
+ # [[skills]] # a skill from \`sky skills upload <dir>\`
1892
+ # type = "skill"
1893
+ # skillId = "skill_..."
1894
+
1895
+ prompt = """
1896
+ You are ${name}, a helpful long-lived agent.
1897
+ """
1898
+ `
1899
+ );
1765
1900
  }
1766
- function tarPadding(size) {
1767
- const rest = size % TAR_BLOCK;
1768
- return Buffer.alloc(rest ? TAR_BLOCK - rest : 0);
1901
+ function deployPatch(spec) {
1902
+ return {
1903
+ displayName: spec.displayName,
1904
+ ...spec.agentType ? { agentType: spec.agentType } : {},
1905
+ ...spec.llm ? { llm: spec.llm } : {},
1906
+ ...spec.capabilities ? { capabilities: spec.capabilities } : {},
1907
+ ...spec.reasoningEffort ? { reasoningEffort: spec.reasoningEffort } : {},
1908
+ ...spec.skills ? { skills: spec.skills } : {},
1909
+ ...spec.hiringPolicy ? { hiringPolicy: spec.hiringPolicy } : {},
1910
+ ...spec.environmentId ? { environmentId: spec.environmentId } : {}
1911
+ };
1769
1912
  }
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")]);
1913
+ function deployReport(id, spec, before, after, promptChanged) {
1914
+ if (spec.agentType && after.agentType !== spec.agentType) {
1915
+ throw new Error(`${id} is still ${after.agentType}: the API did not apply type = "${spec.agentType}"`);
1916
+ }
1917
+ if (spec.llm && after.llm !== spec.llm) {
1918
+ throw new Error(`${id} still runs ${after.llm}: the API did not apply llm = "${spec.llm}"`);
1919
+ }
1920
+ const changed = [];
1921
+ if (promptChanged) changed.push("prompt");
1922
+ if (after.version !== before.version) changed.push("settings");
1923
+ if (!changed.length) return [`${id} is up to date \u2014 nothing to deploy (version ${after.version})`];
1924
+ const lines = [`Deployed ${id}: ${changed.join(" and ")} updated (version ${before.version} \u2192 ${after.version})`];
1925
+ if (after.agentType !== before.agentType || after.llm !== before.llm) {
1926
+ lines.push(` now ${after.agentType} \xB7 ${after.llm} for new sessions (was ${before.agentType} \xB7 ${before.llm})`);
1927
+ }
1928
+ if (spec.skills && after.version !== before.version) {
1929
+ lines.push(` Skills apply to new sessions; running sessions keep the skills they started with \u2014 start one with \`sky session create ${id}\`.`);
1930
+ }
1931
+ return lines;
1774
1932
  }
1775
1933
 
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
- }
1934
+ // src/catalog.ts
1935
+ var bare = (handle) => handle.replace(/^@/, "");
1936
+ var optionalAuth = () => ({ auth: Boolean(apiToken()) });
1937
+ function catalogQuery(filter) {
1938
+ const params = new URLSearchParams();
1939
+ if (filter.text?.trim()) params.set("q", filter.text.trim());
1940
+ if (filter.category) params.set("category", filter.category);
1941
+ const query = params.toString();
1942
+ return query ? `?${query}` : "";
1943
+ }
1944
+ function listingBody(agentId, options) {
1945
+ return {
1946
+ agent_id: agentId,
1947
+ handle: bare(options.handle),
1948
+ tagline: options.tagline,
1949
+ ...options.description ? { description: options.description } : {},
1950
+ ...options.category?.length ? { categories: options.category } : {},
1951
+ ...options.does?.length ? { does: options.does } : {},
1952
+ ...options.doesNot?.length ? { does_not: options.doesNot } : {},
1953
+ ...options.example?.length ? { examples: options.example } : {}
1801
1954
  };
1802
- walk(cwd, "");
1803
- return out;
1804
1955
  }
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;
1956
+ async function listCatalog(filter = {}) {
1957
+ return (await api("GET", `/catalog/agents${catalogQuery(filter)}`, void 0, optionalAuth())).agents;
1810
1958
  }
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));
1891
- }
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;
1959
+ async function showCatalogAgent(handle) {
1960
+ return (await api("GET", `/catalog/agents/${encodeURIComponent(bare(handle))}`, void 0, optionalAuth())).agent;
1961
+ }
1962
+ async function listCategories() {
1963
+ return (await api("GET", "/catalog/categories", void 0, { auth: false })).categories;
1964
+ }
1965
+ async function publishListing(agentId, options) {
1966
+ return (await api("POST", "/catalog/listings", listingBody(agentId, options))).agent;
1967
+ }
1968
+ async function removeListing(handle) {
1969
+ await api("DELETE", `/catalog/listings/${encodeURIComponent(bare(handle))}`);
1970
+ }
1971
+ async function setEngineStatus(engine, status, reason) {
1972
+ await api("PATCH", `/catalog/engines/${encodeURIComponent(engine)}`, { status, reason });
1973
+ const state = await api("GET", "/catalog/engines", void 0, { auth: false });
1974
+ if (state.delisted.includes(engine) !== (status === "delisted")) {
1975
+ throw new Error("The catalog does not show the requested state; check whether another operator changed it.");
1911
1976
  }
1912
1977
  }
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 [];
1921
- }
1922
- });
1978
+ var uses = (agent2) => `${agent2.stats.uses} ${agent2.stats.uses === 1 ? "use" : "uses"}`;
1979
+ var mine = (agent2) => agent2.visibility === "workspace" ? " (yours)" : "";
1980
+ function renderCatalogAgent(agent2) {
1981
+ const section = (title, lines) => lines.length ? [`${title}:`, ...lines.map((line) => ` - ${line}`)] : [];
1982
+ return [
1983
+ `@${agent2.handle} ${agent2.name}${mine(agent2)}`,
1984
+ agent2.tagline,
1985
+ ...agent2.description && agent2.description !== agent2.tagline ? ["", agent2.description] : [],
1986
+ "",
1987
+ `Categories: ${agent2.categories.join(", ") || "none"}`,
1988
+ `Used: ${uses(agent2)}`,
1989
+ ...section("Does", agent2.does),
1990
+ ...section("Does not", agent2.does_not),
1991
+ ...section("Examples", agent2.examples),
1992
+ "",
1993
+ `Hire it: sky session create @${agent2.handle} --message "\u2026"`
1994
+ ].join("\n");
1923
1995
  }
1924
1996
 
1925
- // src/import-local.ts
1926
- function redactLine2(raw) {
1927
- return redactLine(raw).line;
1997
+ // src/clone.ts
1998
+ import { spawn } from "child_process";
1999
+ import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
2000
+ import { homedir as homedir4 } from "os";
2001
+ import { basename as basename5, join as join7 } from "path";
2002
+ import { createInterface } from "readline";
2003
+ var PROMPT_CAP = 1e5;
2004
+ var HARNESSES = ["claude_code", "codex", "openclaw"];
2005
+ function claudeConfigRoot() {
2006
+ return process.env.CLAUDE_CONFIG_DIR ?? join7(homedir4(), ".claude");
1928
2007
  }
1929
- function decodeWorkspace(encoded) {
2008
+ function readIfExists(path) {
1930
2009
  try {
1931
- return decodeURIComponent(encoded.replace(/-/g, "%").replace(/_/g, "%5F"));
2010
+ return readFileSync8(path, "utf8");
1932
2011
  } catch {
1933
- return encoded;
1934
- }
1935
- }
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);
2012
+ return void 0;
1943
2013
  }
1944
2014
  }
1945
- function sniffTitle(filePath) {
2015
+ function readJson2(path) {
2016
+ const raw = readIfExists(path);
2017
+ if (raw === void 0) return void 0;
1946
2018
  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;
2019
+ return JSON.parse(raw);
1992
2020
  } catch {
1993
- return null;
2021
+ return void 0;
1994
2022
  }
1995
2023
  }
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 = [];
2001
- 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
- }
2045
- }
2046
- } catch {
2024
+ function claudeModel(configRoot) {
2025
+ const candidates = [
2026
+ join7(process.cwd(), ".claude", "settings.local.json"),
2027
+ join7(process.cwd(), ".claude", "settings.json"),
2028
+ join7(configRoot, "settings.json")
2029
+ ];
2030
+ for (const path of candidates) {
2031
+ const model = readJson2(path)?.model;
2032
+ if (typeof model === "string" && model.trim()) return model.trim();
2047
2033
  }
2048
- return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
2034
+ return void 0;
2049
2035
  }
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;
2036
+ function claudeMcpServers() {
2037
+ const merged = {};
2038
+ const userConfig = readJson2(join7(homedir4(), ".claude.json"));
2039
+ Object.assign(merged, userConfig?.mcpServers ?? {});
2040
+ Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
2041
+ const projectConfig = readJson2(
2042
+ join7(process.cwd(), ".mcp.json")
2043
+ );
2044
+ Object.assign(merged, projectConfig?.mcpServers ?? {});
2045
+ return merged;
2046
+ }
2047
+ function assemblePrompt(parts) {
2048
+ const joined = parts.map((p) => `# \u2500\u2500 ${p.label} \u2500\u2500
2049
+
2050
+ ${p.content.trim()}`).join("\n\n").trim();
2051
+ if (joined.length <= PROMPT_CAP) return { prompt: joined, truncated: false };
2052
+ const notice = "\n\n[truncated by sky clone: local instructions exceeded the 100k prompt cap]";
2053
+ return { prompt: joined.slice(0, PROMPT_CAP - notice.length) + notice, truncated: true };
2054
+ }
2055
+ function readClaudeCode() {
2056
+ const configRoot = claudeConfigRoot();
2057
+ const parts = [];
2058
+ const sources = [];
2059
+ const global = readIfExists(join7(configRoot, "CLAUDE.md"));
2060
+ if (global?.trim()) {
2061
+ parts.push({ label: "Global CLAUDE.md", content: global });
2062
+ sources.push("global CLAUDE.md");
2063
+ }
2064
+ for (const name of ["CLAUDE.md", "AGENTS.md"]) {
2065
+ const project2 = readIfExists(join7(process.cwd(), name));
2066
+ if (project2?.trim()) {
2067
+ parts.push({ label: `Project ${name}`, content: project2 });
2068
+ sources.push(`project ${name}`);
2069
+ break;
2059
2070
  }
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;
2071
+ }
2072
+ const { prompt, truncated } = assemblePrompt(parts);
2073
+ const allServers = claudeMcpServers();
2074
+ const { kept, dropped } = splitMcpServers(allServers);
2075
+ const secretCandidates = [];
2076
+ for (const d of dropped) {
2077
+ const def = allServers[d.name];
2078
+ if (!def?.env) continue;
2079
+ for (const key of d.envKeys) {
2080
+ const value = def.env[key];
2081
+ if (value !== void 0 && value.trim()) {
2082
+ secretCandidates.push({ serverName: d.name, envKey: key, value, url: d.url });
2086
2083
  }
2087
2084
  }
2088
2085
  }
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
- ]);
2097
- }
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);
2086
+ return {
2087
+ harness: "claude_code",
2088
+ configRoot,
2089
+ prompt,
2090
+ promptSources: sources,
2091
+ truncated,
2092
+ model: claudeModel(configRoot),
2093
+ keptServers: kept,
2094
+ droppedServers: dropped,
2095
+ secretCandidates
2096
+ };
2097
+ }
2098
+ function detectHarness(explicit) {
2099
+ if (explicit) {
2100
+ if (!HARNESSES.includes(explicit)) {
2101
+ throw new Error(`unknown harness ${explicit} \u2014 expected ${HARNESSES.join(" | ")}`);
2102
+ }
2103
+ return explicit;
2104
+ }
2105
+ if (existsSync6(claudeConfigRoot())) return "claude_code";
2106
+ if (existsSync6(join7(homedir4(), ".codex"))) return "codex";
2107
+ if (existsSync6(join7(homedir4(), ".openclaw"))) return "openclaw";
2108
+ throw new Error(
2109
+ "no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
2110
+ );
2102
2111
  }
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);
2112
+ function readLocal(harness2) {
2113
+ if (harness2 === "claude_code") return readClaudeCode();
2114
+ throw new Error(
2115
+ `${harness2} cloning lands next \u2014 claude_code is the first supported harness`
2116
+ );
2107
2117
  }
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;
2118
+ function linkPath(configRoot) {
2119
+ return join7(configRoot, "agentsky-link.json");
2115
2120
  }
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;
2121
+ function readLink(configRoot) {
2122
+ const link2 = readJson2(linkPath(configRoot));
2123
+ return link2?.agentId ? link2 : void 0;
2122
2124
  }
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 })
2136
- });
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();
2125
+ function writeLink(configRoot, link2) {
2126
+ writeFileSync3(linkPath(configRoot), `${JSON.stringify(link2, null, 2)}
2127
+ `);
2144
2128
  }
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;
2129
+ var DEFAULT_VAULT_NAME = "default";
2130
+ async function fetchDefaultVaultId() {
2131
+ const { vaults } = await api(
2132
+ "GET",
2133
+ "/vaults"
2134
+ );
2135
+ return vaults.find((v) => v.display_name === DEFAULT_VAULT_NAME)?.id ?? null;
2136
+ }
2137
+ async function ensureDefaultVaultId() {
2138
+ const existing = await fetchDefaultVaultId();
2139
+ if (existing) return existing;
2153
2140
  try {
2154
- pkg = JSON.parse(raw);
2155
- } catch {
2156
- throw new Error("Summary file is not valid JSON.");
2141
+ const { vault: vault2 } = await api("POST", "/vaults", {
2142
+ display_name: DEFAULT_VAULT_NAME
2143
+ });
2144
+ return vault2.id;
2145
+ } catch (error) {
2146
+ if (error instanceof ApiError && error.status === 409) {
2147
+ const raced = await fetchDefaultVaultId();
2148
+ if (raced) return raced;
2149
+ }
2150
+ throw error;
2157
2151
  }
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
- }
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.`;
2173
2152
  }
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++;
2153
+ async function confirmAndVaultSecrets(candidates, yes) {
2154
+ if (!candidates.length) return;
2155
+ if (yes) {
2156
+ console.log(
2157
+ ` ${candidates.length} secret${candidates.length === 1 ? "" : "s"} from dropped MCP servers not saved (--yes skips vault confirmation).`
2158
+ );
2159
+ return;
2160
+ }
2161
+ console.log("\nSecret-bearing MCP servers were left out of the clone payload.");
2162
+ console.log("You can save their keys to your workspace vault now (values are sent encrypted, never displayed).\n");
2163
+ let vaultId;
2164
+ let saved = 0;
2165
+ let skipped = 0;
2166
+ for (const c of candidates) {
2167
+ const chosen = await confirm(` Add ${c.envKey} (from ${c.serverName}) to your default vault?`);
2168
+ if (!chosen) {
2169
+ console.log(` \u2014 skipped ${c.envKey}`);
2170
+ skipped += 1;
2171
+ continue;
2172
+ }
2173
+ try {
2174
+ vaultId ??= await ensureDefaultVaultId();
2175
+ await api("POST", `/vaults/${encodeURIComponent(vaultId)}/secrets`, {
2176
+ display_name: `${c.envKey} (${c.serverName})`,
2177
+ auth: {
2178
+ type: "environment_variable",
2179
+ secret_name: c.envKey,
2180
+ value: c.value,
2181
+ allowed_hosts: c.url ? [c.url] : []
2210
2182
  }
2183
+ });
2184
+ console.log(` \u2713 saved ${c.envKey}`);
2185
+ saved += 1;
2186
+ } catch (error) {
2187
+ if (error instanceof ApiError && error.status === 409) {
2188
+ console.log(` \u2014 ${c.envKey} already in vault`);
2189
+ } else {
2190
+ const msg = error instanceof Error ? error.message : String(error);
2191
+ console.log(` \u2717 ${c.envKey} failed: ${msg}`);
2211
2192
  }
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);
2224
2193
  }
2225
2194
  }
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)
2195
+ if (saved || skipped) {
2196
+ console.log(
2197
+ `
2198
+ Vault: ${saved} saved, ${skipped} skipped. Attach the "default" vault to a session to use these keys.`
2199
+ );
2200
+ }
2201
+ }
2202
+ function confirm(question) {
2203
+ return new Promise((done) => {
2204
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2205
+ rl.question(`${question} [Y/n] `, (answer) => {
2206
+ rl.close();
2207
+ done(/^(y(es)?)?$/i.test(answer.trim()));
2208
+ });
2236
2209
  });
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);
2210
+ }
2211
+ function openBrowser(url) {
2212
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2213
+ try {
2214
+ spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
2215
+ } catch {
2263
2216
  }
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
2217
  }
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.");
2218
+ function kb(text) {
2219
+ return `${Math.max(1, Math.round(text.length / 1024))} KB`;
2220
+ }
2221
+ async function createClone(local, opts) {
2222
+ const keptNames = Object.keys(local.keptServers);
2223
+ const summary = [
2224
+ local.promptSources.length ? `${local.promptSources.join(" + ")} (${kb(local.prompt)})` : "no instructions found",
2225
+ `model ${local.model ?? "(harness default)"}`,
2226
+ `${keptNames.length} MCP server${keptNames.length === 1 ? "" : "s"}${keptNames.length ? ` (${keptNames.join(", ")})` : ""}`
2227
+ ].join(" \xB7 ");
2228
+ console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename5(process.cwd())})`);
2229
+ console.log(`Will clone: ${summary}`);
2230
+ if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
2231
+ if (local.droppedServers.length) {
2232
+ console.log(
2233
+ `Left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
2234
+ );
2277
2235
  }
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));
2236
+ if (opts.dryRun) {
2237
+ console.log("Dry run \u2014 nothing sent.");
2238
+ return;
2288
2239
  }
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}).`);
2240
+ if (!opts.yes && !await confirm("Launch this as a new cloud agent?")) {
2241
+ console.log("Aborted \u2014 nothing sent.");
2242
+ return;
2300
2243
  }
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
2244
+ const { servers: mcpServers2, skipped } = mcpServersFromKept(local.keptServers);
2245
+ if (skipped.length) {
2246
+ console.log(` note: local (stdio) MCP servers have no cloud equivalent and were skipped: ${skipped.join(", ")}`);
2247
+ }
2248
+ const body = {
2249
+ agentType: local.harness,
2250
+ ...local.model ? { llm: local.model } : {},
2251
+ ...local.prompt ? { prompt: local.prompt } : {},
2252
+ ...mcpServers2.length ? { mcpServers: mcpServers2 } : {}
2317
2253
  };
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)
2254
+ let sentLlm = local.model ?? null;
2255
+ let created;
2256
+ try {
2257
+ ({ agent: created } = await api("POST", "/agents", body));
2258
+ } catch (error) {
2259
+ if (error instanceof ApiError && error.status === 422 && local.model) {
2260
+ const successor = supersededBy(local.model);
2261
+ console.log(
2262
+ successor ? ` note: ${local.model} is superseded \u2014 using ${successor}` : ` note: ${local.model} isn't available for ${local.harness} \u2014 using the harness default`
2263
+ );
2264
+ sentLlm = successor;
2265
+ ({ agent: created } = await api("POST", "/agents", {
2266
+ ...body,
2267
+ llm: successor ?? void 0
2268
+ }));
2269
+ } else {
2270
+ throw error;
2271
+ }
2272
+ }
2273
+ console.log("Starting its runtime\u2026");
2274
+ await api("POST", "/sessions", { agent: created.id });
2275
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2276
+ writeLink(local.configRoot, {
2277
+ baseUrl: apiBase(),
2278
+ agentId: created.id,
2279
+ clonedAt: now,
2280
+ lastSyncedAt: now,
2281
+ llm: sentLlm,
2282
+ mcpServers: keptNames
2326
2283
  });
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;
2284
+ const url = `${apiBase()}/agents/${created.id}`;
2285
+ console.log(`Cloned \u2192 ${created.id} ${url}`);
2286
+ console.log(
2287
+ `Linked this machine (${linkPath(local.configRoot)}). Push prompt changes anytime by running \`sky clone\` again.`
2288
+ );
2289
+ await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
2290
+ if (opts.open) openBrowser(url);
2330
2291
  }
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));
2292
+ async function syncClone(local, link2, opts) {
2293
+ const since = link2.lastSyncedAt ?? link2.clonedAt;
2294
+ console.log(
2295
+ `Syncing ${link2.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
2296
+ );
2297
+ if (opts.dryRun) {
2298
+ console.log(
2299
+ ` would push prompt (${kb(local.prompt)}) to ${link2.agentId}. Dry run \u2014 nothing sent.`
2300
+ );
2301
+ return;
2302
+ }
2303
+ let agent2;
2334
2304
  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();
2305
+ ({ agent: agent2 } = await api(
2306
+ "GET",
2307
+ `/agents/${encodeURIComponent(link2.agentId)}`
2308
+ ));
2309
+ } catch (error) {
2310
+ if (error instanceof ApiError && error.status === 404) {
2311
+ throw new Error(
2312
+ `linked agent ${link2.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
2313
+ );
2314
+ }
2315
+ throw error;
2316
+ }
2317
+ if (agent2.agentType !== local.harness) {
2318
+ throw new Error(
2319
+ `linked agent ${link2.agentId} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
2320
+ );
2321
+ }
2322
+ const result = await api(
2323
+ "PUT",
2324
+ `/agents/${encodeURIComponent(link2.agentId)}/prompt`,
2325
+ { prompt: local.prompt }
2326
+ );
2327
+ console.log(
2328
+ result.unchanged ? " prompt unchanged" : ` prompt updated (v${result.version}${result.applied === "live" ? ", hot-reloaded" : ", applies on next boot"})`
2329
+ );
2330
+ const drift = [];
2331
+ const localModel = local.model ?? null;
2332
+ const linkedModel = link2.llm ?? agent2.llm;
2333
+ if (localModel && linkedModel && localModel !== linkedModel) {
2334
+ drift.push(`model changed ${linkedModel} \u2192 ${localModel}`);
2335
+ }
2336
+ const before = new Set(link2.mcpServers ?? []);
2337
+ const nowKept = Object.keys(local.keptServers);
2338
+ const newServers = nowKept.filter((n) => !before.has(n));
2339
+ const goneServers = [...before].filter((n) => !nowKept.includes(n));
2340
+ if (newServers.length) drift.push(`${newServers.length} new MCP server${newServers.length === 1 ? "" : "s"} (${newServers.join(", ")})`);
2341
+ if (goneServers.length) drift.push(`${goneServers.length} removed MCP server${goneServers.length === 1 ? "" : "s"} (${goneServers.join(", ")})`);
2342
+ if (drift.length) {
2343
+ console.log(` skipped (set at launch): ${drift.join("; ")}`);
2344
+ console.log(" \u2192 a running agent can't change these; run `sky clone --new` to launch a fresh agent.");
2345
+ }
2346
+ if (local.droppedServers.length) {
2347
+ console.log(
2348
+ ` left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
2349
+ );
2349
2350
  }
2351
+ writeLink(local.configRoot, { ...link2, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
2352
+ console.log(`Synced ${link2.agentId}.`);
2353
+ await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
2354
+ if (opts.open) openBrowser(`${apiBase()}/agents/${link2.agentId}`);
2355
+ }
2356
+ async function clone(opts) {
2357
+ const harness2 = detectHarness(opts.harness);
2358
+ const local = readLocal(harness2);
2359
+ const link2 = opts.new ? void 0 : readLink(local.configRoot);
2360
+ if (link2) await syncClone(local, link2, opts);
2361
+ else await createClone(local, opts);
2350
2362
  }
2351
2363
 
2352
2364
  // src/channels.ts
@@ -5092,41 +5104,43 @@ async function listBrowserProfiles() {
5092
5104
  const { data } = await api("GET", "/browser_profiles");
5093
5105
  return data;
5094
5106
  }
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
5107
  async function importBrowserProfileState(scope, state) {
5102
5108
  return api("POST", `/browser_profiles/${encodeURIComponent(scope)}/imports`, state);
5103
5109
  }
5104
- async function listBrowserProfileSites(scope) {
5105
- const profile = await getBrowserProfile(scope);
5106
- return profile.sites;
5110
+ async function listBrowserProfileSites() {
5111
+ const profiles = await listBrowserProfiles();
5112
+ return (profiles.find((entry2) => entry2.scope === "mine") ?? profiles.find((entry2) => entry2.scope === "workspace"))?.sites ?? [];
5107
5113
  }
5108
- async function browserSiteReference(scope, site) {
5109
- const sites = await listBrowserProfileSites(scope);
5114
+ async function removeBrowserProfileSite(site) {
5115
+ const sites = await listBrowserProfileSites();
5110
5116
  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);
5117
+ if (!row) throw new ApiError(404, "not_found", "site not found in your browser");
5118
+ await api("DELETE", `/browser_profiles/mine/sites/${row.site_id ? `${encodeURIComponent(row.site_id)}?by=id` : encodeURIComponent(row.site)}`);
5113
5119
  }
5114
- async function removeBrowserProfileSite(scope, site) {
5115
- await api("DELETE", `/browser_profiles/${encodeURIComponent(scope)}/sites/${await browserSiteReference(scope, site)}`);
5120
+ async function shareBrowserProfile(member, sites) {
5121
+ const { share } = await api("POST", "/browser_profiles/mine/shares", {
5122
+ grantee: member ? { user_id: member } : "workspace",
5123
+ sites: sites.length ? sites : null
5124
+ });
5125
+ return share;
5116
5126
  }
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 });
5127
+ async function unshareBrowserProfile(shareId) {
5128
+ await api("DELETE", `/browser_profiles/mine/shares/${encodeURIComponent(shareId)}`);
5121
5129
  }
5122
5130
  function renderBrowserProfileSite(site) {
5123
5131
  const who = site.signed_in_by?.email ?? site.signed_in_by?.name ?? "unknown";
5124
5132
  const used = site.last_used_at ? `used ${site.last_used_at.slice(0, 10)}` : "never used";
5125
5133
  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
5134
  }
5135
+ function renderBrowserProfileShare(share) {
5136
+ const who = share.grantee.type === "workspace" ? "everyone in this workspace" : share.grantee.user.email ?? share.grantee.user.name ?? share.grantee.user.id;
5137
+ return `shared with ${who}: ${share.sites ? share.sites.join(", ") : "every site"} (share ${share.id})`;
5138
+ }
5127
5139
  function renderBrowserProfile(profile) {
5128
- const lines = [`scope: ${profile.scope} revision: ${profile.revision} sites: ${profile.sites.length}`];
5140
+ 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";
5141
+ const lines = [`${title} revision: ${profile.revision} sites: ${profile.sites.length}`];
5129
5142
  for (const site of profile.sites) lines.push(` ${renderBrowserProfileSite(site)}`);
5143
+ for (const share of profile.shares ?? []) lines.push(` ${renderBrowserProfileShare(share)}`);
5130
5144
  return lines.join("\n");
5131
5145
  }
5132
5146
 
@@ -5282,12 +5296,80 @@ function renderLibraryArtifact(artifact2) {
5282
5296
  return `${artifact2.id} ${artifact2.name} ${version}${artifact2.shared ? " shared" : ""}`;
5283
5297
  }
5284
5298
 
5299
+ // src/design-systems.ts
5300
+ import { readdirSync as readdirSync8, readFileSync as readFileSync16, statSync as statSync9 } from "fs";
5301
+ import { join as join18, relative as relative5, sep as sep3 } from "path";
5302
+ var TEXT2 = /\.(mdx?|markdown|txt|json|m?js|cjs|jsx|tsx?|css|html?|svg|xml|csv|ya?ml|toml)$/i;
5303
+ var MEDIA = {
5304
+ woff2: "font/woff2",
5305
+ woff: "font/woff",
5306
+ ttf: "font/ttf",
5307
+ otf: "font/otf",
5308
+ png: "image/png",
5309
+ jpg: "image/jpeg",
5310
+ jpeg: "image/jpeg",
5311
+ gif: "image/gif",
5312
+ webp: "image/webp",
5313
+ pdf: "application/pdf",
5314
+ mp4: "video/mp4",
5315
+ webm: "video/webm"
5316
+ };
5317
+ async function listDesignSystems() {
5318
+ return (await api("GET", "/artifacts?kind=design_system")).artifacts;
5319
+ }
5320
+ async function getDesignSystem(id) {
5321
+ return (await api("GET", `/artifacts/${encodeURIComponent(id)}/design-system`)).design_system;
5322
+ }
5323
+ async function importDesignSystem(task2, files2, title) {
5324
+ return (await api("POST", "/artifacts/design-systems", { task_id: task2, files: files2, ...title ? { title } : {} })).artifact;
5325
+ }
5326
+ async function saveDesignTokens(id, baseVersion, tokens, note) {
5327
+ return (await api("PUT", `/artifacts/${encodeURIComponent(id)}/design-system/tokens`, {
5328
+ base_version: baseVersion,
5329
+ tokens,
5330
+ ...note ? { note } : {}
5331
+ })).artifact;
5332
+ }
5333
+ function fileTableFrom(dir) {
5334
+ const files2 = {};
5335
+ const walk = (current) => {
5336
+ for (const name of readdirSync8(current)) {
5337
+ if (name.startsWith(".") || name === "node_modules") continue;
5338
+ const path = join18(current, name);
5339
+ if (statSync9(path).isDirectory()) {
5340
+ walk(path);
5341
+ continue;
5342
+ }
5343
+ const key = relative5(dir, path).split(sep3).join("/");
5344
+ const bytes = readFileSync16(path);
5345
+ files2[key] = TEXT2.test(name) ? bytes.toString("utf8") : `data:${MEDIA[name.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream"};base64,${bytes.toString("base64")}`;
5346
+ }
5347
+ };
5348
+ walk(dir);
5349
+ return files2;
5350
+ }
5351
+ function renderDesignSystem(system) {
5352
+ const tokens = system.tokens;
5353
+ const lines = [
5354
+ `${system.artifact_id} ${system.title} v${system.version}${system.editable ? "" : " read-only"}`,
5355
+ 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",
5356
+ `components: ${system.components.map((component) => component.name).join(", ") || "none"}`,
5357
+ `assets: ${system.assets.map((group) => `${group.name} (${group.files.length})`).join(", ") || "none"}`
5358
+ ];
5359
+ 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}`);
5360
+ for (const token of tokens?.dropped ?? []) lines.push(`not read: ${token.family} ${token.name || "(unnamed)"}: ${token.reason}`);
5361
+ return lines.join("\n");
5362
+ }
5363
+ async function designComments(id, input) {
5364
+ return api(input ? "POST" : "GET", `/artifacts/${encodeURIComponent(id)}/design-system/comments`, input);
5365
+ }
5366
+
5285
5367
  // src/api-command.ts
5286
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
5368
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
5287
5369
  var API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
5288
- function readData(data, stdin = () => readFileSync16(0, "utf8")) {
5370
+ function readData(data, stdin = () => readFileSync17(0, "utf8")) {
5289
5371
  if (data === void 0) return void 0;
5290
- const raw = data === "-" ? stdin() : data.startsWith("@") ? readFileSync16(data.slice(1), "utf8") : data;
5372
+ const raw = data === "-" ? stdin() : data.startsWith("@") ? readFileSync17(data.slice(1), "utf8") : data;
5291
5373
  try {
5292
5374
  JSON.parse(raw);
5293
5375
  } catch {
@@ -5304,9 +5386,9 @@ function requestPath(path, query = []) {
5304
5386
  if (at <= 0) throw new Error(`--query needs key=value, got "${pair}"`);
5305
5387
  params.append(pair.slice(0, at), pair.slice(at + 1));
5306
5388
  }
5307
- const sep3 = clean.includes("?") ? "&" : "?";
5389
+ const sep4 = clean.includes("?") ? "&" : "?";
5308
5390
  const encoded = params.toString();
5309
- return encoded ? `${clean}${sep3}${encoded}` : clean;
5391
+ return encoded ? `${clean}${sep4}${encoded}` : clean;
5310
5392
  }
5311
5393
  function matchOperations(doc, words) {
5312
5394
  const needles = words.map((w) => w.toLowerCase());
@@ -5442,11 +5524,11 @@ function workspaceFlagIsCommands(argv, commandPath) {
5442
5524
  }
5443
5525
 
5444
5526
  // 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";
5527
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
5528
+ import { dirname as dirname9, join as join19, resolve as resolve8 } from "path";
5447
5529
  var HARNESS_FILE = "harness.json";
5448
5530
  function harnessFilePath(dir) {
5449
- return join18(resolve8(dir), HARNESS_FILE);
5531
+ return join19(resolve8(dir), HARNESS_FILE);
5450
5532
  }
5451
5533
  function readHarnessFile(dir) {
5452
5534
  const path = harnessFilePath(dir);
@@ -5454,7 +5536,7 @@ function readHarnessFile(dir) {
5454
5536
  throw new Error(`no ${HARNESS_FILE} found in ${resolve8(dir)} \u2014 run \`sky harness init\` first`);
5455
5537
  }
5456
5538
  try {
5457
- return JSON.parse(readFileSync17(path, "utf8"));
5539
+ return JSON.parse(readFileSync18(path, "utf8"));
5458
5540
  } catch (err) {
5459
5541
  throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
5460
5542
  }
@@ -6753,7 +6835,7 @@ task.command("get <task> <path>").description("read one task file; prints it, or
6753
6835
  );
6754
6836
  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
6837
  run(async (taskId, path, local) => {
6756
- const { file, conflict } = await writeTaskFile(taskId, path, readFileSync18(local), mediaTypeFor(local));
6838
+ const { file, conflict } = await writeTaskFile(taskId, path, readFileSync19(local), mediaTypeFor(local));
6757
6839
  console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
6758
6840
  })
6759
6841
  );
@@ -6917,7 +6999,7 @@ library.command("list").description("your Library, newest first").option("--sear
6917
6999
  );
6918
7000
  library.command("upload <path>").description("save a file from this computer to your Library").option("--workspace <key>", "place it in this workspace").action(
6919
7001
  run(async (path, opts) => {
6920
- const bytes = readFileSync18(path);
7002
+ const bytes = readFileSync19(path);
6921
7003
  const { artifact: artifact2 } = await uploadToLibrary({ bytes, name: basename9(path), mediaType: "application/octet-stream" }, `sky-upload-${crypto.randomUUID()}`, opts.workspace);
6922
7004
  console.log(renderLibraryArtifact(artifact2));
6923
7005
  })
@@ -6986,6 +7068,35 @@ artifact.command("rm <id>").description("delete an artifact").action(
6986
7068
  console.log(`Deleted ${id}`);
6987
7069
  })
6988
7070
  );
7071
+ var designSystem = program.command("design-system").description("design systems: brand books, tokens and components agents design with");
7072
+ designSystem.command("list").description("the workspace's design systems").action(
7073
+ run(async () => {
7074
+ const list = await listDesignSystems();
7075
+ if (!list.length) console.log("no design systems");
7076
+ for (const a of list) console.log(renderArtifact(a));
7077
+ })
7078
+ );
7079
+ 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(
7080
+ run(async (id, opts) => {
7081
+ const system = await getDesignSystem(id);
7082
+ console.log(opts.readme ? system.readme ?? "this design system has no README.md" : renderDesignSystem(system));
7083
+ })
7084
+ );
7085
+ 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(
7086
+ run(async (task2, folder, opts) => {
7087
+ console.log(renderArtifact(await importDesignSystem(task2, fileTableFrom(resolve9(folder)), opts.title)));
7088
+ })
7089
+ );
7090
+ 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(
7091
+ run(async (id, file, opts) => {
7092
+ const tokens = JSON.parse(readFileSync19(resolve9(file), "utf8"));
7093
+ console.log(renderArtifact(await saveDesignTokens(id, opts.base, tokens, opts.note)));
7094
+ })
7095
+ );
7096
+ 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) => {
7097
+ if (opts.text !== void 0 && (!Number.isInteger(opts.base) || opts.base < 1)) throw Error("Adding a comment requires --base <version>.");
7098
+ 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));
7099
+ }));
6989
7100
  function routineLine(r) {
6990
7101
  const target = r.target.type === "session" ? `\u2192 ${r.target.session_id}` : `+ ${r.target.agent}`;
6991
7102
  const cron = r.schedule ? `${r.schedule.expression} ${r.schedule.timezone}` : "manual-only";
@@ -7369,10 +7480,10 @@ memory.command("save <text...>").description("add an entry").action(run(async (t
7369
7480
  memory.command("edit <index> <text...>").description("replace entry <index>").action(run(async (index, text) => writeMemory({ op: "update", index: memoryIndex(index), text: text.join(" ") })));
7370
7481
  memory.command("rm <index>").description("remove entry <index>").action(run(async (index) => writeMemory({ op: "remove", index: memoryIndex(index) })));
7371
7482
  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(
7483
+ var context = program.command("context").description("search everything you can open in the workspace, and read a hit");
7484
+ 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
7485
  run(async (words, opts) => {
7375
- const page = await api("GET", `/context/search${searchQuery(words.join(" "), opts)}`);
7486
+ const page = await searchContext(words.join(" "), opts);
7376
7487
  console.log(opts.json ? JSON.stringify(page, null, 2) : renderContextPage(page));
7377
7488
  })
7378
7489
  );
@@ -7380,7 +7491,7 @@ context.command("read <ref>").description("read a search hit in full: <ref> is t
7380
7491
  run(async (ref, opts) => {
7381
7492
  const params = new URLSearchParams({ ref });
7382
7493
  if (opts.offset) params.set("offset", opts.offset);
7383
- const read = await api("GET", `/context/read?${params}`);
7494
+ const read = await contextRequest(`/context/read?${params}`);
7384
7495
  console.log(read.text);
7385
7496
  if (read.nextOffset !== null) console.error(`more: --offset ${read.nextOffset}`);
7386
7497
  })
@@ -7690,7 +7801,7 @@ mac.command("take-back <mount>").description("end a folder, browser window, scre
7690
7801
  })
7691
7802
  );
7692
7803
  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(
7804
+ 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
7805
  run(async (opts) => {
7695
7806
  const profiles = await listBrowserProfiles();
7696
7807
  if (opts.json) {
@@ -7701,9 +7812,9 @@ browserProfile.command("list").description("your workspace's browser profiles: t
7701
7812
  for (const p of profiles) console.log(renderBrowserProfile(p));
7702
7813
  })
7703
7814
  );
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);
7815
+ browserProfile.command("sites").description("list the sites your browser is signed in to").option("--json", "print as JSON").action(
7816
+ run(async (opts) => {
7817
+ const sites = await listBrowserProfileSites();
7707
7818
  if (opts.json) {
7708
7819
  console.log(JSON.stringify(sites, null, 2));
7709
7820
  return;
@@ -7713,8 +7824,8 @@ browserProfile.command("sites <scope>").description("list the sites in a profile
7713
7824
  for (const s of sites) console.log(renderBrowserProfileSite(s));
7714
7825
  })
7715
7826
  );
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) => {
7827
+ 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(
7828
+ run(async (opts) => {
7718
7829
  let raw;
7719
7830
  if (opts.file) {
7720
7831
  const { readFileSync: rf } = await import("fs");
@@ -7727,21 +7838,26 @@ browserProfile.command("import <scope>").description("import saved browser state
7727
7838
  });
7728
7839
  }
7729
7840
  const state = JSON.parse(raw);
7730
- const result = await importBrowserProfileState(scope, { state });
7841
+ const result = await importBrowserProfileState("mine", { state });
7731
7842
  console.log(`Imported ${result.sites.length} site${result.sites.length === 1 ? "" : "s"}: ${result.sites.join(", ")}`);
7732
7843
  })
7733
7844
  );
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}`);
7845
+ browserProfile.command("remove-site <site>").description("sign your browser out of a site everywhere (the agent will need to sign in again)").action(
7846
+ run(async (site) => {
7847
+ await removeBrowserProfileSite(site);
7848
+ console.log(`Signed out of ${site}`);
7849
+ })
7850
+ );
7851
+ 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(
7852
+ run(async (opts) => {
7853
+ const share = await shareBrowserProfile(opts.member, (opts.sites ?? "").split(",").map((s) => s.trim()).filter(Boolean));
7854
+ console.log(renderBrowserProfileShare(share));
7738
7855
  })
7739
7856
  );
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}`);
7857
+ browserProfile.command("unshare <shareId>").description("stop a share of your browser; it ends from the grantee's next turn").action(
7858
+ run(async (shareId) => {
7859
+ await unshareBrowserProfile(shareId);
7860
+ console.log(`Stopped share ${shareId}`);
7745
7861
  })
7746
7862
  );
7747
7863
  var notifications = program.command("notifications").description("email and browser push notification preferences for tasks");
@@ -7801,21 +7917,21 @@ async function helpFetch(path) {
7801
7917
  return await res.json();
7802
7918
  }
7803
7919
  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(
7920
+ 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
7921
  run(async (words, opts) => {
7806
7922
  const q = words.join(" ");
7807
7923
  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}`);
7924
+ const { hits } = await helpFetch(`/api/context/search?${new URLSearchParams({ q, scope: "docs/", limit: String(topK) })}`);
7809
7925
  if (opts.json) {
7810
- console.log(JSON.stringify(results, null, 2));
7926
+ console.log(JSON.stringify(hits, null, 2));
7811
7927
  return;
7812
7928
  }
7813
- if (!results.length) {
7929
+ if (!hits.length) {
7814
7930
  console.log("no results");
7815
7931
  return;
7816
7932
  }
7817
- for (const r of results) console.log(`${r.slug.padEnd(40)} [${r.category}] ${r.title}
7818
- ${r.snippet.slice(0, 100)}`);
7933
+ for (const hit of hits) console.log(`${String(hit.ref.slug).padEnd(40)} ${hit.title}
7934
+ ${hit.snippet.slice(0, 100)}`);
7819
7935
  })
7820
7936
  );
7821
7937
  help.command("get <slug>").description("read a help article by its slug").option("--json", "print as JSON").action(
@@ -7898,7 +8014,7 @@ notes.command("read <storeId> <noteId>").description("print one note").action(
7898
8014
  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
8015
  run(async (storeId, file, opts) => {
7900
8016
  if (!opts.path === !opts.note) throw new Error("pass exactly one of --path (new note) or --note (update)");
7901
- const content = readFileSync18(file, "utf8");
8017
+ const content = readFileSync19(file, "utf8");
7902
8018
  const target = `/memory_stores/${encodeURIComponent(storeId)}/memories${opts.note ? `/${encodeURIComponent(opts.note)}` : ""}`;
7903
8019
  const { memory: memory2 } = await api("POST", target, { content, ...opts.path ? { path: opts.path } : {} });
7904
8020
  console.log(`Saved ${memory2.path} (${memory2.id})`);
@@ -7998,7 +8114,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
7998
8114
  }));
7999
8115
  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
8116
  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");
8117
+ const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-WLTD4C22.js");
8002
8118
  if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
8003
8119
  const scopes = parseContentScopes(opts.capture);
8004
8120
  const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
@@ -8033,7 +8149,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
8033
8149
  }));
8034
8150
  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
8151
  try {
8036
- const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-CFHCOGKB.js");
8152
+ const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-WLTD4C22.js");
8037
8153
  const chunks = [];
8038
8154
  for await (const chunk of process.stdin) chunks.push(chunk);
8039
8155
  const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
@@ -8046,14 +8162,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
8046
8162
  }
8047
8163
  });
8048
8164
  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");
8165
+ const { readState, flush, grantSender } = await import("./trace-WLTD4C22.js");
8050
8166
  const state = readState();
8051
8167
  if (!state) throw new Error("no trace is running here");
8052
8168
  const outcome = await flush(grantSender(state), { attempts: 4 });
8053
8169
  console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
8054
8170
  }));
8055
8171
  traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
8056
- const { readState, spoolStatus } = await import("./trace-CFHCOGKB.js");
8172
+ const { readState, spoolStatus } = await import("./trace-WLTD4C22.js");
8057
8173
  const state = readState();
8058
8174
  if (!state) {
8059
8175
  console.log("No trace is running here.");
@@ -8063,7 +8179,7 @@ traceCmd.command("status").description("the running trace and its spool").action
8063
8179
  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
8180
  }));
8065
8181
  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");
8182
+ const { readState } = await import("./trace-WLTD4C22.js");
8067
8183
  const ref = id ?? readState()?.trace_id;
8068
8184
  if (!ref) throw new Error("name a trace, or start one here");
8069
8185
  const bundle = await api("GET", `/traces/${ref}`);
@@ -8090,7 +8206,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
8090
8206
  for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
8091
8207
  }));
8092
8208
  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");
8209
+ const { readState, record, flush, grantSender, clearState } = await import("./trace-WLTD4C22.js");
8094
8210
  const state = readState();
8095
8211
  if (!state) throw new Error("no trace is running here");
8096
8212
  if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
@@ -8100,7 +8216,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
8100
8216
  console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
8101
8217
  }));
8102
8218
  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");
8219
+ const lib = await import("./trace-library-YMAVLH4Y.js");
8104
8220
  if (opts.hook) {
8105
8221
  try {
8106
8222
  let raw = payload ?? "";
@@ -8159,10 +8275,10 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
8159
8275
  console.log(`Added ${imported.sessions} ${imported.agent} session${imported.sessions === 1 ? "" : "s"} from ${input}.`);
8160
8276
  }
8161
8277
  if (opts.installSchedule) {
8162
- const { join: join19 } = await import("path");
8278
+ const { join: join20 } = await import("path");
8163
8279
  const { homedir: homedir10 } = await import("os");
8164
8280
  const { execFileSync } = await import("child_process");
8165
- const log = join19(homedir10(), ".config", "sky", "trace-sync.log");
8281
+ const log = join20(homedir10(), ".config", "sky", "trace-sync.log");
8166
8282
  const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
8167
8283
  try {
8168
8284
  execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
@@ -8172,11 +8288,11 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
8172
8288
  console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
8173
8289
  }
8174
8290
  if (opts.status) {
8175
- const { readFileSync: readFileSync19 } = await import("fs");
8176
- const { join: join19 } = await import("path");
8291
+ const { readFileSync: readFileSync20 } = await import("fs");
8292
+ const { join: join20 } = await import("path");
8177
8293
  let state = { sessions: {} };
8178
8294
  try {
8179
- state = JSON.parse(readFileSync19(join19(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
8295
+ state = JSON.parse(readFileSync20(join20(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
8180
8296
  } catch {
8181
8297
  }
8182
8298
  const counts = /* @__PURE__ */ new Map();
@@ -8208,25 +8324,14 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
8208
8324
  if (report.failed) process.exitCode = 1;
8209
8325
  })();
8210
8326
  });
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");
8327
+ 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
8328
  const workspace2 = opts.workspace ?? currentUniverse();
8214
8329
  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}`);
8330
+ const page = await searchContext(words.join(" "), { ...opts, scope: "traces" }, workspace2);
8331
+ console.log(opts.json ? JSON.stringify(page, null, 2) : renderContextPage(page));
8227
8332
  }));
8228
8333
  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");
8334
+ const { libraryRequest } = await import("./trace-library-YMAVLH4Y.js");
8230
8335
  const workspace2 = opts.workspace ?? currentUniverse();
8231
8336
  if (!workspace2) throw new Error("name a workspace with --workspace");
8232
8337
  const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);