@agentsky/sky 2.23.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
@@ -4,12 +4,13 @@ import {
4
4
  api,
5
5
  apiBytes,
6
6
  apiForm,
7
+ apiRaw,
7
8
  downloadFileTo,
8
9
  postEvents,
9
10
  resolveSessionRef,
10
11
  streamEvents,
11
12
  uploadFile
12
- } from "./chunk-HQPOCQRQ.js";
13
+ } from "./chunk-KWLEADJQ.js";
13
14
  import {
14
15
  CLI_VERSION,
15
16
  USER_AGENT,
@@ -20,7 +21,7 @@ import {
20
21
  currentUniverse,
21
22
  loadConfig,
22
23
  saveConfig
23
- } from "./chunk-V5XLV37X.js";
24
+ } from "./chunk-S4YG3TXA.js";
24
25
  import {
25
26
  AGENT_TYPES,
26
27
  MODEL_SUBSCRIPTION_PLAN_PHRASE,
@@ -42,17 +43,17 @@ import {
42
43
  skillRefSchema,
43
44
  splitMcpServers,
44
45
  supersededBy
45
- } from "./chunk-HDPDY77W.js";
46
+ } from "./chunk-D3WG4BMK.js";
46
47
  import "./chunk-MLKGABMK.js";
47
48
 
48
49
  // src/index.ts
49
- import { existsSync as existsSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
50
+ import { existsSync as existsSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "fs";
50
51
  import { basename as basename9, resolve as resolve9 } from "path";
51
52
  import { createInterface as createInterface6 } from "readline";
52
53
  import { Command } from "commander";
53
54
 
54
55
  // src/files.ts
55
- import { readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
56
+ import { existsSync, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
56
57
  import { basename as basename2, extname, join as join2 } from "path";
57
58
 
58
59
  // src/resumable-upload.ts
@@ -250,6 +251,16 @@ function eventsFor(text, files2, system, interactionMode2) {
250
251
  if (system?.trim()) events.push({ type: "system.message", content: [{ type: "text", text: system }] });
251
252
  return events;
252
253
  }
254
+ function isFileId(ref) {
255
+ return /^file_[A-Za-z0-9]+$/.test(ref);
256
+ }
257
+ async function attachment(ref, opts = {}) {
258
+ if (opts.byId || isFileId(ref) && !existsSync(ref)) {
259
+ if (!isFileId(ref)) throw new Error(`--file-id takes a stored file's id (file_\u2026), got ${JSON.stringify(ref)}`);
260
+ return { file: await api("GET", `/files/${encodeURIComponent(ref)}`), uploaded: false };
261
+ }
262
+ return { file: await uploadLocalFile(ref), uploaded: true };
263
+ }
253
264
 
254
265
  // src/goal.ts
255
266
  var CHECK_SEPARATOR = " :: ";
@@ -456,837 +467,266 @@ async function unsubscribeMarketing() {
456
467
  return { marketing: notification_preferences.email.marketing };
457
468
  }
458
469
 
459
- // src/context.ts
460
- function memoryBody(operation, version) {
461
- return { ...operation, expectedVersion: version };
462
- }
463
- function memoryIndex(value) {
464
- if (!/^\d+$/.test(value.trim())) throw new Error(`the entry number must be a whole number, got ${JSON.stringify(value)}`);
465
- return Number(value.trim());
466
- }
467
- function renderMemory(document) {
468
- if (!document.entries.length) return "no saved memory \u2014 `sky memory save <text>` adds an entry";
469
- return document.entries.map((entry2) => `${String(entry2.index).padStart(3)} ${entry2.text}`).join("\n");
470
- }
471
- function searchQuery(query, opts) {
472
- const params = new URLSearchParams({ q: query });
473
- params.set("scope", opts.scope ?? "sessions");
474
- if (opts.limit) params.set("limit", opts.limit);
475
- if (opts.cursor) params.set("cursor", opts.cursor);
476
- if (opts.since) params.set("since", opts.since);
477
- if (opts.until) params.set("until", opts.until);
478
- return `?${params.toString()}`;
479
- }
480
- function refArgument(ref) {
481
- return `'${JSON.stringify(ref).replace(/'/g, "'\\''")}'`;
482
- }
483
- function renderContextPage(page) {
484
- const lines = [];
485
- for (const { hit } of page.hits) {
486
- lines.push(`${hit.source.padEnd(8)} ${hit.occurredAt?.slice(0, 10) ?? " "} ${hit.title}`);
487
- lines.push(` ${hit.snippet.replace(/\s+/g, " ").slice(0, 160)}`);
488
- lines.push(` sky context read ${refArgument(hit.ref)}`);
489
- }
490
- if (!page.hits.length) lines.push("no matches on this page");
491
- if (page.partial) lines.push(`partial: ${page.coverage ?? "not every source was searched"}`);
492
- if (page.nextCursor) lines.push(`more: --cursor ${page.nextCursor}`);
493
- return lines.join("\n");
494
- }
495
-
496
- // src/agent-file.ts
497
- import { existsSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
498
- import { dirname as dirname2, resolve as resolve2 } from "path";
499
- import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
500
- var fileInstructionSchema = external_exports.object({
501
- name: external_exports.string(),
502
- content: external_exports.string().optional(),
503
- file: external_exports.string().optional()
504
- });
505
- var agentFileSchema = external_exports.object({
506
- name: external_exports.string().min(1),
507
- id: external_exports.string().optional(),
508
- // stamped by launch/deploy; identifies the live agent
509
- slug: external_exports.string().optional(),
510
- // pre-RFC-0079 stamp; ignored (agents are id-addressed)
511
- description: external_exports.string().optional(),
512
- type: external_exports.enum(AGENT_TYPES).optional(),
513
- llm: external_exports.string().optional(),
514
- reasoning_effort: external_exports.enum(REASONING_EFFORT_VALUES).optional(),
515
- capabilities: capabilitiesSchema.optional(),
516
- prompt: external_exports.string().optional(),
517
- prompt_file: external_exports.string().optional(),
518
- instructions: external_exports.array(fileInstructionSchema).optional(),
519
- skills: external_exports.array(skillRefSchema).optional(),
520
- customData: external_exports.array(customDataSchema).optional(),
521
- /** "deny" makes the agent work alone: its sessions may not hire helpers. */
522
- hiring_policy: external_exports.enum(["allow", "deny"]).optional(),
523
- /** The environment its sessions boot in when they name none. */
524
- environment: external_exports.string().min(1).optional(),
525
- /** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
526
- vcpus: external_exports.number().int().optional(),
527
- memory_mb: external_exports.number().int().optional()
528
- });
529
- function readAgentFile(path) {
530
- const raw = readFileSync3(path, "utf8");
531
- const parsed = agentFileSchema.safeParse(parseToml(raw));
532
- if (!parsed.success) {
533
- const issue = parsed.error.issues[0];
534
- throw new Error(`${path}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? "invalid"}`);
535
- }
536
- const file = parsed.data;
537
- if (file.reasoning_effort && file.type && !isModelEffortSupported(file.type, file.llm ?? defaultLlm(file.type), file.reasoning_effort)) {
538
- throw new Error(
539
- `${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
540
- );
541
- }
542
- const base = dirname2(resolve2(path));
543
- const inline = (ref, inlineContent) => {
544
- if (inlineContent !== void 0) return inlineContent;
545
- if (!ref) return void 0;
546
- return readFileSync3(resolve2(base, ref), "utf8");
547
- };
548
- const spec = {
549
- name: file.name,
550
- description: file.description,
551
- agentType: file.type,
552
- llm: file.llm,
553
- reasoningEffort: file.reasoning_effort,
554
- displayName: file.name,
555
- capabilities: file.capabilities,
556
- prompt: inline(file.prompt_file, file.prompt),
557
- instructions: file.instructions?.map((f) => {
558
- const content = inline(f.file, f.content);
559
- if (content === void 0) throw new Error(`instruction ${f.name}: content or file required`);
560
- return { name: f.name, content };
561
- }),
562
- skills: file.skills,
563
- customData: file.customData,
564
- hiringPolicy: file.hiring_policy,
565
- environmentId: file.environment,
566
- vcpus: file.vcpus,
567
- memoryMb: file.memory_mb
568
- };
569
- return { file, spec };
570
- }
571
- function stampId(path, id) {
572
- const doc = parseToml(readFileSync3(path, "utf8"));
573
- doc.id = id;
574
- delete doc.slug;
575
- writeFileSync2(path, stringifyToml(doc));
576
- }
577
- function scaffoldAgentFile(path, name) {
578
- if (existsSync(path)) throw new Error(`${path} already exists`);
579
- writeFileSync2(
580
- path,
581
- `# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
582
- name = "${name}"
583
- # type = "pi" # pi (default) | hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode
584
- # llm = "deepseek-flash" # must be compatible with type
585
- # reasoning_effort = "high" # per harness \u2014 ${reasoningEffortDocLine()}
586
- # capabilities = []
587
- # hiring_policy = "deny" # its sessions work alone; "allow" (default) lets them hire helpers
588
- # environment = "env_..." # the environment its sessions boot in when they name none
589
- # [[skills]] # a skill from \`sky skills upload <dir>\`
590
- # type = "skill"
591
- # skillId = "skill_..."
592
-
593
- prompt = """
594
- You are ${name}, a helpful long-lived agent.
595
- """
596
- `
597
- );
598
- }
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";
599
475
 
600
- // src/catalog.ts
601
- var bare = (handle) => handle.replace(/^@/, "");
602
- var optionalAuth = () => ({ auth: Boolean(apiToken()) });
603
- function catalogQuery(filter) {
604
- const params = new URLSearchParams();
605
- if (filter.text?.trim()) params.set("q", filter.text.trim());
606
- if (filter.category) params.set("category", filter.category);
607
- const query = params.toString();
608
- return query ? `?${query}` : "";
609
- }
610
- function listingBody(agentId, options) {
611
- return {
612
- agent_id: agentId,
613
- handle: bare(options.handle),
614
- tagline: options.tagline,
615
- ...options.description ? { description: options.description } : {},
616
- ...options.category?.length ? { categories: options.category } : {},
617
- ...options.does?.length ? { does: options.does } : {},
618
- ...options.doesNot?.length ? { does_not: options.doesNot } : {},
619
- ...options.example?.length ? { examples: options.example } : {}
620
- };
621
- }
622
- async function listCatalog(filter = {}) {
623
- return (await api("GET", `/catalog/agents${catalogQuery(filter)}`, void 0, optionalAuth())).agents;
624
- }
625
- async function showCatalogAgent(handle) {
626
- return (await api("GET", `/catalog/agents/${encodeURIComponent(bare(handle))}`, void 0, optionalAuth())).agent;
627
- }
628
- async function listCategories() {
629
- return (await api("GET", "/catalog/categories", void 0, { auth: false })).categories;
630
- }
631
- async function publishListing(agentId, options) {
632
- return (await api("POST", "/catalog/listings", listingBody(agentId, options))).agent;
633
- }
634
- async function removeListing(handle) {
635
- await api("DELETE", `/catalog/listings/${encodeURIComponent(bare(handle))}`);
636
- }
637
- async function setEngineStatus(engine, status, reason) {
638
- await api("PATCH", `/catalog/engines/${encodeURIComponent(engine)}`, { status, reason });
639
- const state = await api("GET", "/catalog/engines", void 0, { auth: false });
640
- if (state.delisted.includes(engine) !== (status === "delisted")) {
641
- throw new Error("The catalog does not show the requested state; check whether another operator changed it.");
642
- }
643
- }
644
- var uses = (agent2) => `${agent2.stats.uses} ${agent2.stats.uses === 1 ? "use" : "uses"}`;
645
- var mine = (agent2) => agent2.visibility === "workspace" ? " (yours)" : "";
646
- function renderCatalogAgent(agent2) {
647
- const section = (title, lines) => lines.length ? [`${title}:`, ...lines.map((line) => ` - ${line}`)] : [];
648
- return [
649
- `@${agent2.handle} ${agent2.name}${mine(agent2)}`,
650
- agent2.tagline,
651
- ...agent2.description && agent2.description !== agent2.tagline ? ["", agent2.description] : [],
652
- "",
653
- `Categories: ${agent2.categories.join(", ") || "none"}`,
654
- `Used: ${uses(agent2)}`,
655
- ...section("Does", agent2.does),
656
- ...section("Does not", agent2.does_not),
657
- ...section("Examples", agent2.examples),
658
- "",
659
- `Hire it: sky session create @${agent2.handle} --message "\u2026"`
660
- ].join("\n");
661
- }
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";
662
481
 
663
- // src/clone.ts
664
- import { spawn } from "child_process";
665
- import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
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";
666
485
  import { homedir } from "os";
667
- import { basename as basename3, join as join3 } from "path";
668
- import { createInterface } from "readline";
669
- var PROMPT_CAP = 1e5;
670
- var HARNESSES = ["claude_code", "codex", "openclaw"];
671
- function claudeConfigRoot() {
672
- return process.env.CLAUDE_CONFIG_DIR ?? join3(homedir(), ".claude");
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;
673
493
  }
674
- function readIfExists(path) {
494
+ var isFile = (path) => {
675
495
  try {
676
- return readFileSync4(path, "utf8");
496
+ return statSync3(path).isFile();
677
497
  } catch {
678
- return void 0;
498
+ return false;
679
499
  }
680
- }
681
- function readJson(path) {
682
- const raw = readIfExists(path);
683
- if (raw === void 0) return void 0;
500
+ };
501
+ var isDir = (path) => {
684
502
  try {
685
- return JSON.parse(raw);
503
+ return statSync3(path).isDirectory();
686
504
  } catch {
687
- return void 0;
505
+ return false;
688
506
  }
689
- }
690
- function claudeModel(configRoot) {
691
- const candidates = [
692
- join3(process.cwd(), ".claude", "settings.local.json"),
693
- join3(process.cwd(), ".claude", "settings.json"),
694
- join3(configRoot, "settings.json")
695
- ];
696
- for (const path of candidates) {
697
- const model = readJson(path)?.model;
698
- if (typeof model === "string" && model.trim()) return model.trim();
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 {
699
514
  }
700
- return void 0;
515
+ return [join3(home, "AGENTS.md"), join3(home, "MEMORY.md"), ...ruleFiles].filter(isFile);
701
516
  }
702
- function claudeMcpServers() {
703
- const merged = {};
704
- const userConfig = readJson(join3(homedir(), ".claude.json"));
705
- Object.assign(merged, userConfig?.mcpServers ?? {});
706
- Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
707
- const projectConfig = readJson(
708
- join3(process.cwd(), ".mcp.json")
709
- );
710
- Object.assign(merged, projectConfig?.mcpServers ?? {});
711
- return merged;
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);
712
522
  }
713
- function assemblePrompt(parts) {
714
- const joined = parts.map((p) => `# \u2500\u2500 ${p.label} \u2500\u2500
715
-
716
- ${p.content.trim()}`).join("\n\n").trim();
717
- if (joined.length <= PROMPT_CAP) return { prompt: joined, truncated: false };
718
- const notice = "\n\n[truncated by sky clone: local instructions exceeded the 100k prompt cap]";
719
- return { prompt: joined.slice(0, PROMPT_CAP - notice.length) + notice, truncated: true };
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
+ ];
720
533
  }
721
- function readClaudeCode() {
722
- const configRoot = claudeConfigRoot();
723
- const parts = [];
724
- const sources = [];
725
- const global = readIfExists(join3(configRoot, "CLAUDE.md"));
726
- if (global?.trim()) {
727
- parts.push({ label: "Global CLAUDE.md", content: global });
728
- sources.push("global CLAUDE.md");
729
- }
730
- for (const name of ["CLAUDE.md", "AGENTS.md"]) {
731
- const project2 = readIfExists(join3(process.cwd(), name));
732
- if (project2?.trim()) {
733
- parts.push({ label: `Project ${name}`, content: project2 });
734
- sources.push(`project ${name}`);
735
- break;
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;
736
542
  }
737
- }
738
- const { prompt, truncated } = assemblePrompt(parts);
739
- const allServers = claudeMcpServers();
740
- const { kept, dropped } = splitMcpServers(allServers);
741
- const secretCandidates = [];
742
- for (const d of dropped) {
743
- const def = allServers[d.name];
744
- if (!def?.env) continue;
745
- for (const key of d.envKeys) {
746
- const value = def.env[key];
747
- if (value !== void 0 && value.trim()) {
748
- secretCandidates.push({ serverName: d.name, envKey: key, value, url: d.url });
749
- }
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;
750
549
  }
751
- }
752
- return {
753
- harness: "claude_code",
754
- configRoot,
755
- prompt,
756
- promptSources: sources,
757
- truncated,
758
- model: claudeModel(configRoot),
759
- keptServers: kept,
760
- droppedServers: dropped,
761
- secretCandidates
550
+ return null;
762
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);
763
553
  }
764
- function detectHarness(explicit) {
765
- if (explicit) {
766
- if (!HARNESSES.includes(explicit)) {
767
- throw new Error(`unknown harness ${explicit} \u2014 expected ${HARNESSES.join(" | ")}`);
768
- }
769
- return explicit;
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);
770
560
  }
771
- if (existsSync2(claudeConfigRoot())) return "claude_code";
772
- if (existsSync2(join3(homedir(), ".codex"))) return "codex";
773
- if (existsSync2(join3(homedir(), ".openclaw"))) return "openclaw";
774
- throw new Error(
775
- "no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
776
- );
777
- }
778
- function readLocal(harness2) {
779
- if (harness2 === "claude_code") return readClaudeCode();
780
- throw new Error(
781
- `${harness2} cloning lands next \u2014 claude_code is the first supported harness`
782
- );
783
- }
784
- function linkPath(configRoot) {
785
- return join3(configRoot, "agentsky-link.json");
561
+ return null;
786
562
  }
787
- function readLink(configRoot) {
788
- const link2 = readJson(linkPath(configRoot));
789
- return link2?.agentId ? link2 : void 0;
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
+ }
583
+ }
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);
589
+ }
590
+ return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
790
591
  }
791
- function writeLink(configRoot, link2) {
792
- writeFileSync3(linkPath(configRoot), `${JSON.stringify(link2, null, 2)}
793
- `);
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
+ }
794
599
  }
795
- var DEFAULT_VAULT_NAME = "default";
796
- async function fetchDefaultVaultId() {
797
- const { vaults } = await api(
798
- "GET",
799
- "/vaults"
800
- );
801
- return vaults.find((v) => v.display_name === DEFAULT_VAULT_NAME)?.id ?? null;
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;
802
604
  }
803
- async function ensureDefaultVaultId() {
804
- const existing = await fetchDefaultVaultId();
805
- if (existing) return existing;
605
+ function safeUrl(raw) {
806
606
  try {
807
- const { vault: vault2 } = await api("POST", "/vaults", {
808
- display_name: DEFAULT_VAULT_NAME
809
- });
810
- return vault2.id;
811
- } catch (error) {
812
- if (error instanceof ApiError && error.status === 409) {
813
- const raced = await fetchDefaultVaultId();
814
- if (raced) return raced;
815
- }
816
- throw error;
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;
817
613
  }
818
614
  }
819
- async function confirmAndVaultSecrets(candidates, yes) {
820
- if (!candidates.length) return;
821
- if (yes) {
822
- console.log(
823
- ` ${candidates.length} secret${candidates.length === 1 ? "" : "s"} from dropped MCP servers not saved (--yes skips vault confirmation).`
824
- );
825
- return;
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 } : {} };
826
624
  }
827
- console.log("\nSecret-bearing MCP servers were left out of the clone payload.");
828
- console.log("You can save their keys to your workspace vault now (values are sent encrypted, never displayed).\n");
829
- let vaultId;
830
- let saved = 0;
831
- let skipped = 0;
832
- for (const c of candidates) {
833
- const chosen = await confirm(` Add ${c.envKey} (from ${c.serverName}) to your default vault?`);
834
- if (!chosen) {
835
- console.log(` \u2014 skipped ${c.envKey}`);
836
- skipped += 1;
837
- continue;
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()) {
630
+ const out = [];
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);
838
636
  }
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") {
839
647
  try {
840
- vaultId ??= await ensureDefaultVaultId();
841
- await api("POST", `/vaults/${encodeURIComponent(vaultId)}/secrets`, {
842
- display_name: `${c.envKey} (${c.serverName})`,
843
- auth: {
844
- type: "environment_variable",
845
- secret_name: c.envKey,
846
- value: c.value,
847
- allowed_hosts: c.url ? [c.url] : []
848
- }
849
- });
850
- console.log(` \u2713 saved ${c.envKey}`);
851
- saved += 1;
852
- } catch (error) {
853
- if (error instanceof ApiError && error.status === 409) {
854
- console.log(` \u2014 ${c.envKey} already in vault`);
855
- } else {
856
- const msg = error instanceof Error ? error.message : String(error);
857
- console.log(` \u2717 ${c.envKey} failed: ${msg}`);
858
- }
648
+ const config = parseToml(readFileSync3(join3(process.env.CODEX_HOME ?? join3(home, ".codex"), "config.toml"), "utf8"));
649
+ push(config.mcp_servers, "user");
650
+ } catch {
859
651
  }
860
652
  }
861
- if (saved || skipped) {
862
- console.log(
863
- `
864
- Vault: ${saved} saved, ${skipped} skipped. Attach the "default" vault to a session to use these keys.`
865
- );
866
- }
653
+ return out;
867
654
  }
868
- function confirm(question) {
869
- return new Promise((done) => {
870
- const rl = createInterface({ input: process.stdin, output: process.stdout });
871
- rl.question(`${question} [Y/n] `, (answer) => {
872
- rl.close();
873
- done(/^(y(es)?)?$/i.test(answer.trim()));
874
- });
875
- });
655
+ function remoteWithoutCredentials(remote) {
656
+ return remote.trim().replace(/\/\/[^@/]+@/, "//").replace(/^[^@/]+@([^:]+):/, "$1:");
876
657
  }
877
- function openBrowser(url) {
878
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
879
- try {
880
- spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
881
- } catch {
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;
882
678
  }
883
- }
884
- function kb(text) {
885
- return `${Math.max(1, Math.round(text.length / 1024))} KB`;
886
- }
887
- async function createClone(local, opts) {
888
- const keptNames = Object.keys(local.keptServers);
889
- const summary = [
890
- local.promptSources.length ? `${local.promptSources.join(" + ")} (${kb(local.prompt)})` : "no instructions found",
891
- `model ${local.model ?? "(harness default)"}`,
892
- `${keptNames.length} MCP server${keptNames.length === 1 ? "" : "s"}${keptNames.length ? ` (${keptNames.join(", ")})` : ""}`
893
- ].join(" \xB7 ");
894
- console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename3(process.cwd())})`);
895
- console.log(`Will clone: ${summary}`);
896
- if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
897
- if (local.droppedServers.length) {
898
- console.log(
899
- `Left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
900
- );
901
- }
902
- if (opts.dryRun) {
903
- console.log("Dry run \u2014 nothing sent.");
904
- return;
905
- }
906
- if (!opts.yes && !await confirm("Launch this as a new cloud agent?")) {
907
- console.log("Aborted \u2014 nothing sent.");
908
- return;
909
- }
910
- const { servers: mcpServers2, skipped } = mcpServersFromKept(local.keptServers);
911
- if (skipped.length) {
912
- console.log(` note: local (stdio) MCP servers have no cloud equivalent and were skipped: ${skipped.join(", ")}`);
913
- }
914
- const body = {
915
- agentType: local.harness,
916
- ...local.model ? { llm: local.model } : {},
917
- ...local.prompt ? { prompt: local.prompt } : {},
918
- ...mcpServers2.length ? { mcpServers: mcpServers2 } : {}
679
+ return {
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
919
694
  };
920
- let sentLlm = local.model ?? null;
921
- let created;
922
- try {
923
- ({ agent: created } = await api("POST", "/agents", body));
924
- } catch (error) {
925
- if (error instanceof ApiError && error.status === 422 && local.model) {
926
- const successor = supersededBy(local.model);
927
- console.log(
928
- successor ? ` note: ${local.model} is superseded \u2014 using ${successor}` : ` note: ${local.model} isn't available for ${local.harness} \u2014 using the harness default`
929
- );
930
- sentLlm = successor;
931
- ({ agent: created } = await api("POST", "/agents", {
932
- ...body,
933
- llm: successor ?? void 0
934
- }));
935
- } else {
936
- throw error;
937
- }
938
- }
939
- console.log("Starting its runtime\u2026");
940
- await api("POST", "/sessions", { agent: created.id });
941
- const now = (/* @__PURE__ */ new Date()).toISOString();
942
- writeLink(local.configRoot, {
943
- baseUrl: apiBase(),
944
- agentId: created.id,
945
- clonedAt: now,
946
- lastSyncedAt: now,
947
- llm: sentLlm,
948
- mcpServers: keptNames
949
- });
950
- const url = `${apiBase()}/agents/${created.id}`;
951
- console.log(`Cloned \u2192 ${created.id} ${url}`);
952
- console.log(
953
- `Linked this machine (${linkPath(local.configRoot)}). Push prompt changes anytime by running \`sky clone\` again.`
954
- );
955
- await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
956
- if (opts.open) openBrowser(url);
957
- }
958
- async function syncClone(local, link2, opts) {
959
- const since = link2.lastSyncedAt ?? link2.clonedAt;
960
- console.log(
961
- `Syncing ${link2.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
962
- );
963
- if (opts.dryRun) {
964
- console.log(
965
- ` would push prompt (${kb(local.prompt)}) to ${link2.agentId}. Dry run \u2014 nothing sent.`
966
- );
967
- return;
968
- }
969
- let agent2;
970
- try {
971
- ({ agent: agent2 } = await api(
972
- "GET",
973
- `/agents/${encodeURIComponent(link2.agentId)}`
974
- ));
975
- } catch (error) {
976
- if (error instanceof ApiError && error.status === 404) {
977
- throw new Error(
978
- `linked agent ${link2.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
979
- );
980
- }
981
- throw error;
982
- }
983
- if (agent2.agentType !== local.harness) {
984
- throw new Error(
985
- `linked agent ${link2.agentId} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
986
- );
987
- }
988
- const result = await api(
989
- "PUT",
990
- `/agents/${encodeURIComponent(link2.agentId)}/prompt`,
991
- { prompt: local.prompt }
992
- );
993
- console.log(
994
- result.unchanged ? " prompt unchanged" : ` prompt updated (v${result.version}${result.applied === "live" ? ", hot-reloaded" : ", applies on next boot"})`
995
- );
996
- const drift = [];
997
- const localModel = local.model ?? null;
998
- const linkedModel = link2.llm ?? agent2.llm;
999
- if (localModel && linkedModel && localModel !== linkedModel) {
1000
- drift.push(`model changed ${linkedModel} \u2192 ${localModel}`);
1001
- }
1002
- const before = new Set(link2.mcpServers ?? []);
1003
- const nowKept = Object.keys(local.keptServers);
1004
- const newServers = nowKept.filter((n) => !before.has(n));
1005
- const goneServers = [...before].filter((n) => !nowKept.includes(n));
1006
- if (newServers.length) drift.push(`${newServers.length} new MCP server${newServers.length === 1 ? "" : "s"} (${newServers.join(", ")})`);
1007
- if (goneServers.length) drift.push(`${goneServers.length} removed MCP server${goneServers.length === 1 ? "" : "s"} (${goneServers.join(", ")})`);
1008
- if (drift.length) {
1009
- console.log(` skipped (set at launch): ${drift.join("; ")}`);
1010
- console.log(" \u2192 a running agent can't change these; run `sky clone --new` to launch a fresh agent.");
1011
- }
1012
- if (local.droppedServers.length) {
1013
- console.log(
1014
- ` left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
1015
- );
1016
- }
1017
- writeLink(local.configRoot, { ...link2, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
1018
- console.log(`Synced ${link2.agentId}.`);
1019
- await confirmAndVaultSecrets(local.secretCandidates, opts.yes ?? false);
1020
- if (opts.open) openBrowser(`${apiBase()}/agents/${link2.agentId}`);
1021
- }
1022
- async function clone(opts) {
1023
- const harness2 = detectHarness(opts.harness);
1024
- const local = readLocal(harness2);
1025
- const link2 = opts.new ? void 0 : readLink(local.configRoot);
1026
- if (link2) await syncClone(local, link2, opts);
1027
- else await createClone(local, opts);
1028
- }
1029
-
1030
- // src/import-local.ts
1031
- import { closeSync as closeSync3, existsSync as existsSync5, openSync as openSync3, readdirSync as readdirSync4, readFileSync as readFileSync8, readSync as readSync3, statSync as statSync5 } from "fs";
1032
- import { homedir as homedir4 } from "os";
1033
- import { join as join7 } from "path";
1034
- import { createHash as createHash3 } from "crypto";
1035
-
1036
- // src/import-files.ts
1037
- import { isUtf8 } from "buffer";
1038
- import { existsSync as existsSync4, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
1039
- import { homedir as homedir3 } from "os";
1040
- 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";
1041
-
1042
- // src/import-workspace.ts
1043
- import { spawnSync } from "child_process";
1044
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
1045
- import { homedir as homedir2 } from "os";
1046
- import { basename as basename4, dirname as dirname3, join as join4, relative, sep } from "path";
1047
- import { parse as parseToml2 } from "smol-toml";
1048
- var MAX_TREE = 5e3;
1049
- var SKILL_LINE = /Base directory for this skill: (\/[^\n\r"'\\<>]+)/g;
1050
- function git(cwd, ...args) {
1051
- const run2 = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 1e4, maxBuffer: 32 * 1024 * 1024 });
1052
- return run2.status === 0 ? run2.stdout : null;
1053
- }
1054
- var isFile = (path) => {
1055
- try {
1056
- return statSync3(path).isFile();
1057
- } catch {
1058
- return false;
1059
- }
1060
- };
1061
- var isDir = (path) => {
1062
- try {
1063
- return statSync3(path).isDirectory();
1064
- } catch {
1065
- return false;
1066
- }
1067
- };
1068
- function sharedInstructionPaths(home = homedir2()) {
1069
- const rules = join4(home, ".agents", "rules");
1070
- let ruleFiles = [];
1071
- try {
1072
- ruleFiles = readdirSync2(rules).filter((name) => name.endsWith(".md")).sort().map((name) => join4(rules, name));
1073
- } catch {
1074
- }
1075
- return [join4(home, "AGENTS.md"), join4(home, "MEMORY.md"), ...ruleFiles].filter(isFile);
1076
- }
1077
- function strings(value, out, depth = 0) {
1078
- if (depth > 12) return;
1079
- if (typeof value === "string") out.push(value);
1080
- else if (Array.isArray(value)) for (const item of value) strings(item, out, depth + 1);
1081
- else if (value && typeof value === "object") for (const item of Object.values(value)) strings(item, out, depth + 1);
1082
- }
1083
- function skillRoots(cwd, home) {
1084
- const claude = process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude");
1085
- const codex = process.env.CODEX_HOME ?? join4(home, ".codex");
1086
- return [
1087
- ...cwd ? [join4(cwd, ".claude", "skills"), join4(cwd, ".agents", "skills")] : [],
1088
- join4(home, ".agents", "skills"),
1089
- join4(claude, "skills"),
1090
- join4(codex, "skills"),
1091
- join4(home, ".hermes", "skills")
1092
- ];
1093
695
  }
1094
- function pluginSkill(plugin, skill, home) {
1095
- const walk = (dir, depth, underPlugin) => {
1096
- if (depth > 7) return null;
696
+ function skillFiles(skill) {
697
+ const out = [];
698
+ const walk = (dir, depth) => {
699
+ if (depth > 8 || out.length > 400) return;
1097
700
  let entries;
1098
701
  try {
1099
702
  entries = readdirSync2(dir, { withFileTypes: true });
1100
703
  } catch {
1101
- return null;
704
+ return;
1102
705
  }
1103
706
  for (const entry2 of entries) {
1104
- if (!entry2.isDirectory() || entry2.name === "node_modules" || entry2.name === ".git") continue;
1105
- const path = join4(dir, entry2.name);
1106
- if (entry2.name === skill && basename4(dir) === "skills" && (underPlugin || path.includes(`${sep}${plugin}${sep}`)) && isFile(join4(path, "SKILL.md"))) return realpathSync(path);
1107
- const found = walk(path, depth + 1, underPlugin);
1108
- if (found) return found;
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("/")}` });
1109
711
  }
1110
- return null;
1111
712
  };
1112
- return walk(join4(process.env.CLAUDE_CONFIG_DIR ?? join4(home, ".claude"), "plugins"), 0, false) ?? walk(join4(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions", "skills-plugin"), 0, true);
1113
- }
1114
- function findSkill(name, cwd, home) {
1115
- const [scope, skill] = name.includes(":") ? name.split(":", 2) : [null, name];
1116
- if (scope && skill) return pluginSkill(scope, skill, home);
1117
- for (const root of skillRoots(cwd, home)) {
1118
- const direct = join4(root, name);
1119
- if (isFile(join4(direct, "SKILL.md"))) return realpathSync(direct);
1120
- }
1121
- return null;
1122
- }
1123
- function loadedSkills(records, touched, cwd, home = homedir2()) {
1124
- const dirs = /* @__PURE__ */ new Map();
1125
- const missing = /* @__PURE__ */ new Set();
1126
- const add = (dir, name = basename4(dir)) => {
1127
- if (!isFile(join4(dir, "SKILL.md"))) return false;
1128
- const real = realpathSync(dir);
1129
- if (![...dirs.values()].includes(real)) dirs.set(name, real);
1130
- return true;
1131
- };
1132
- const called = [];
1133
- for (const record of records) {
1134
- const texts = [];
1135
- strings(record, texts);
1136
- for (const text of texts) for (const match of text.matchAll(SKILL_LINE)) add(match[1].trim().replace(/[.,;:)]+$/, ""));
1137
- const content = record?.message?.content;
1138
- if (!Array.isArray(content)) continue;
1139
- for (const block of content) {
1140
- const b = block;
1141
- if (b?.type === "tool_use" && b.name === "Skill" && typeof b.input?.skill === "string") called.push(b.input.skill.replace(/^\//, ""));
1142
- }
1143
- }
1144
- for (const path of touched) if (basename4(path) === "SKILL.md") add(dirname3(path));
1145
- for (const name of called) {
1146
- if (!name || dirs.has(name) || [...dirs.keys()].some((key) => key.endsWith(`:${name}`) || name.endsWith(`:${key}`))) continue;
1147
- const dir = findSkill(name, cwd, home);
1148
- if (!dir || !add(dir, name)) missing.add(name);
1149
- }
1150
- return { found: [...dirs].map(([name, dir]) => ({ name: name.replace(/[^A-Za-z0-9._:-]/g, "-"), dir })), missing: [...missing] };
713
+ if (existsSync2(skill.dir)) walk(skill.dir, 0);
714
+ return out;
1151
715
  }
1152
- function readJson2(path) {
716
+ function codexThreadName(sessionId, home = homedir()) {
717
+ let name = null;
1153
718
  try {
1154
- const value = JSON.parse(readFileSync5(path, "utf8"));
1155
- return value && typeof value === "object" ? value : null;
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
+ }
1156
727
  } catch {
1157
- return null;
1158
728
  }
1159
- }
1160
- function names(value) {
1161
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1162
- const keys2 = Object.keys(value).sort();
1163
- return keys2.length ? keys2 : void 0;
1164
- }
1165
- function safeUrl(raw) {
1166
- try {
1167
- const url = new URL(raw);
1168
- if (!/^https?:$/.test(url.protocol)) return null;
1169
- const query = [.../* @__PURE__ */ new Set([...url.searchParams.keys()])].sort();
1170
- return { url: `${url.protocol}//${url.host}${url.pathname}`, ...query.length ? { query } : {} };
1171
- } catch {
1172
- return null;
1173
- }
1174
- }
1175
- function serverEntry(name, def, scope) {
1176
- if (!def || typeof def !== "object") return null;
1177
- const env = names(def.env);
1178
- const headers = names(def.headers ?? def.http_headers);
1179
- if (typeof def.url === "string") {
1180
- const url = safeUrl(def.url);
1181
- if (!url) return null;
1182
- const type = def.type === "sse" ? "sse" : "http";
1183
- return { name, scope, transport: type, ...url, ...headers ? { headers } : {}, ...env ? { env } : {} };
1184
- }
1185
- if (typeof def.command !== "string" || !def.command) return null;
1186
- const args = Array.isArray(def.args) ? def.args.filter((arg) => typeof arg === "string") : [];
1187
- return { name, scope, transport: "stdio", command: def.command, ...args.length ? { args } : {}, ...env ? { env } : {} };
1188
- }
1189
- function mcpServers(harness2, cwd, home = homedir2()) {
1190
- const out = [];
1191
- const push = (servers, scope) => {
1192
- if (!servers || typeof servers !== "object") return;
1193
- for (const [name, def] of Object.entries(servers)) {
1194
- const entry2 = serverEntry(name, def, scope);
1195
- if (entry2 && !out.some((existing) => existing.name === name)) out.push(entry2);
1196
- }
1197
- };
1198
- if (harness2 === "claude_code") {
1199
- const config = readJson2(join4(process.env.CLAUDE_CONFIG_DIR ? dirname3(process.env.CLAUDE_CONFIG_DIR) : home, ".claude.json"));
1200
- const projects = config?.projects && typeof config.projects === "object" ? config.projects : {};
1201
- const root = cwd ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || cwd : "";
1202
- if (cwd) push(projects[cwd]?.mcpServers ?? projects[root]?.mcpServers, "local");
1203
- if (root) push(readJson2(join4(root, ".mcp.json"))?.mcpServers, "project");
1204
- push(config?.mcpServers, "user");
1205
- }
1206
- if (harness2 === "codex") {
1207
- try {
1208
- const config = parseToml2(readFileSync5(join4(process.env.CODEX_HOME ?? join4(home, ".codex"), "config.toml"), "utf8"));
1209
- push(config.mcp_servers, "user");
1210
- } catch {
1211
- }
1212
- }
1213
- return out;
1214
- }
1215
- function remoteWithoutCredentials(remote) {
1216
- return remote.trim().replace(/\/\/[^@/]+@/, "//").replace(/^[^@/]+@([^:]+):/, "$1:");
1217
- }
1218
- function workspaceState(cwd, harness2, skills2, home = homedir2(), title = null) {
1219
- const present = !!cwd && isDir(cwd);
1220
- const root = present ? git(cwd, "rev-parse", "--show-toplevel")?.trim() || null : null;
1221
- let gitState = null;
1222
- let tree = [];
1223
- let patch = null;
1224
- if (root) {
1225
- const dirty = (git(root, "status", "--porcelain=v1", "--untracked-files=normal") ?? "").split("\n").filter(Boolean).map((line) => line.slice(3));
1226
- const ahead = git(root, "rev-list", "--count", "@{upstream}..HEAD");
1227
- const remote = git(root, "remote", "get-url", "origin");
1228
- const branch = git(root, "rev-parse", "--abbrev-ref", "HEAD")?.trim() || null;
1229
- gitState = {
1230
- remote: remote ? remoteWithoutCredentials(remote) : null,
1231
- branch: branch === "HEAD" ? null : branch,
1232
- commit: git(root, "rev-parse", "HEAD")?.trim() || null,
1233
- dirty: dirty.slice(0, 500),
1234
- ahead: ahead === null ? null : Number(ahead.trim()) || 0
1235
- };
1236
- tree = (git(cwd, "-c", "core.quotePath=false", "ls-files") ?? "").split("\n").filter(Boolean);
1237
- if (dirty.length) patch = git(cwd, "diff", "HEAD", "--no-color", "--no-ext-diff", "--relative", "--", ".") || null;
1238
- }
1239
- return {
1240
- manifest: {
1241
- format: "agentsky.import-workspace",
1242
- format_version: 1,
1243
- harness: harness2,
1244
- title: title?.trim().slice(0, 120) || null,
1245
- cwd: cwd ? cwd.replace(home, "~") : null,
1246
- project: cwd ? basename4(root ?? cwd) : null,
1247
- git: gitState,
1248
- tree: tree.slice(0, MAX_TREE),
1249
- tree_truncated: tree.length > MAX_TREE,
1250
- skills: [...skills2.found.map((skill) => ({ name: skill.name, found: true })), ...skills2.missing.map((name) => ({ name, found: false }))],
1251
- mcp_servers: mcpServers(harness2, cwd, home)
1252
- },
1253
- patch
1254
- };
1255
- }
1256
- function skillFiles(skill) {
1257
- const out = [];
1258
- const walk = (dir, depth) => {
1259
- if (depth > 8 || out.length > 400) return;
1260
- let entries;
1261
- try {
1262
- entries = readdirSync2(dir, { withFileTypes: true });
1263
- } catch {
1264
- return;
1265
- }
1266
- for (const entry2 of entries) {
1267
- if (entry2.name === "node_modules" || entry2.name === ".git" || entry2.name === "__pycache__" || entry2.name === ".venv") continue;
1268
- const path = join4(dir, entry2.name);
1269
- if (entry2.isDirectory()) walk(path, depth + 1);
1270
- else if (entry2.isFile()) out.push({ path, name: `skills/${skill.name}/${relative(skill.dir, path).split(sep).join("/")}` });
1271
- }
1272
- };
1273
- if (existsSync3(skill.dir)) walk(skill.dir, 0);
1274
- return out;
1275
- }
1276
- function codexThreadName(sessionId, home = homedir2()) {
1277
- let name = null;
1278
- try {
1279
- for (const line of readFileSync5(join4(process.env.CODEX_HOME ?? join4(home, ".codex"), "session_index.jsonl"), "utf8").split("\n")) {
1280
- if (!line.includes(sessionId)) continue;
1281
- try {
1282
- const entry2 = JSON.parse(line);
1283
- if (entry2.id === sessionId && typeof entry2.thread_name === "string" && entry2.thread_name.trim()) name = entry2.thread_name.trim();
1284
- } catch {
1285
- }
1286
- }
1287
- } catch {
1288
- }
1289
- return name;
729
+ return name;
1290
730
  }
1291
731
 
1292
732
  // src/import-files.ts
@@ -1338,7 +778,7 @@ function classify(path) {
1338
778
  if (DOCUMENTS[ext]) return { kind: "document", mediaType: DOCUMENTS[ext] };
1339
779
  if (IMAGES[ext]) return { kind: "image", mediaType: IMAGES[ext] };
1340
780
  if (TEXT[ext]) return { kind: "text", mediaType: TEXT[ext] };
1341
- if (PLAIN_TEXT.has(ext) || PLAIN_NAMES.has(basename5(path))) return { kind: "text", mediaType: "text/plain" };
781
+ if (PLAIN_TEXT.has(ext) || PLAIN_NAMES.has(basename4(path))) return { kind: "text", mediaType: "text/plain" };
1342
782
  return null;
1343
783
  }
1344
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;
@@ -1352,13 +792,13 @@ function shellText(input) {
1352
792
  const command = Array.isArray(i.command) ? i.command.filter((part) => typeof part === "string").join(" ") : str(i.command);
1353
793
  return [command, str(i.cmd), str(i.script)].filter(Boolean).join("\n");
1354
794
  }
1355
- function touchedPaths(records, cwd, home = homedir3()) {
795
+ function touchedPaths(records, cwd, home = homedir2()) {
1356
796
  const out = /* @__PURE__ */ new Set();
1357
797
  const add = (raw, base) => {
1358
798
  if (!raw || raw.length > 1e3) return;
1359
799
  const path = raw.replace(/^~(?=\/)/, home);
1360
- if (isAbsolute(path)) out.add(resolve4(path));
1361
- else if (base) out.add(resolve4(base, path));
800
+ if (isAbsolute(path)) out.add(resolve2(path));
801
+ else if (base) out.add(resolve2(base, path));
1362
802
  };
1363
803
  const call = (name, input) => {
1364
804
  const i = obj(input);
@@ -1395,22 +835,22 @@ function touchedPaths(records, cwd, home = homedir3()) {
1395
835
  return [...out];
1396
836
  }
1397
837
  function gitRoot(dir) {
1398
- for (let d = dir; ; d = dirname4(d)) {
1399
- if (existsSync4(join5(d, ".git"))) return d;
1400
- if (d === dirname4(d)) return null;
838
+ for (let d = dir; ; d = dirname3(d)) {
839
+ if (existsSync3(join4(d, ".git"))) return d;
840
+ if (d === dirname3(d)) return null;
1401
841
  }
1402
842
  }
1403
- function instructionPaths(cwd, harness2, home = homedir3()) {
843
+ function instructionPaths(cwd, harness2, home = homedir2()) {
1404
844
  const out = [];
1405
- if (cwd && existsSync4(cwd)) {
845
+ if (cwd && existsSync3(cwd)) {
1406
846
  const root = gitRoot(cwd) ?? cwd;
1407
- for (let d = resolve4(cwd); ; d = dirname4(d)) {
1408
- for (const name of INSTRUCTION_FILES) out.push(join5(d, name));
1409
- if (d === root || d === dirname4(d)) break;
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;
1410
850
  }
1411
851
  }
1412
- if (harness2 === "claude_code") out.push(join5(process.env.CLAUDE_CONFIG_DIR ?? join5(home, ".claude"), "CLAUDE.md"));
1413
- if (harness2 === "codex") out.push(join5(process.env.CODEX_HOME ?? join5(home, ".codex"), "AGENTS.md"));
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"));
1414
854
  out.push(...sharedInstructionPaths(home));
1415
855
  return out.filter((path) => {
1416
856
  try {
@@ -1420,8 +860,8 @@ function instructionPaths(cwd, harness2, home = homedir3()) {
1420
860
  }
1421
861
  });
1422
862
  }
1423
- function resourceName(path, cwd, home = homedir3()) {
1424
- const inside = cwd ? relative2(resolve4(cwd), path) : "";
863
+ function resourceName(path, cwd, home = homedir2()) {
864
+ const inside = cwd ? relative2(resolve2(cwd), path) : "";
1425
865
  if (cwd && inside && !inside.startsWith("..") && !isAbsolute(inside)) return `workspace/${inside.split(sep2).join("/")}`;
1426
866
  const underHome = relative2(home, path);
1427
867
  const shown = !underHome.startsWith("..") && !isAbsolute(underHome) ? `~/${underHome}` : path.replace(/^\/+/, "");
@@ -1432,8 +872,8 @@ function resourceLine(name, mediaType, data) {
1432
872
  return redactLine(JSON.stringify({ type: "import.resource", name, media_type: mediaType, data: data.toString("base64") }), { requireStructured: true }).line + "\n";
1433
873
  }
1434
874
  function collectSessionFiles(records, opts) {
1435
- const home = opts.home ?? homedir3();
1436
- const exclude = new Set((opts.exclude ?? []).map((path) => resolve4(path)));
875
+ const home = opts.home ?? homedir2();
876
+ const exclude = new Set((opts.exclude ?? []).map((path) => resolve2(path)));
1437
877
  const leftOut = [];
1438
878
  const candidates = [];
1439
879
  const seen = /* @__PURE__ */ new Set();
@@ -1498,7 +938,7 @@ function collectSessionFiles(records, opts) {
1498
938
  for (const file of candidates) {
1499
939
  let data;
1500
940
  try {
1501
- data = readFileSync6(file.path);
941
+ data = readFileSync4(file.path);
1502
942
  } catch {
1503
943
  leftOut.push({ path: file.path, reason: "missing" });
1504
944
  continue;
@@ -1535,9 +975,9 @@ function collectSessionFiles(records, opts) {
1535
975
  // src/import-environment.ts
1536
976
  import { spawnSync as spawnSync2 } from "child_process";
1537
977
  import { createHash as createHash2 } from "crypto";
1538
- import { closeSync as closeSync2, createWriteStream, lstatSync, mkdtempSync, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync7, readSync as readSync2, rmSync as rmSync2 } from "fs";
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";
1539
979
  import { tmpdir } from "os";
1540
- import { join as join6 } from "path";
980
+ import { join as join5 } from "path";
1541
981
  import { isUtf8 as isUtf82 } from "buffer";
1542
982
  import { createGzip } from "zlib";
1543
983
 
@@ -1723,7 +1163,7 @@ function listFiles(cwd) {
1723
1163
  for (const entry2 of entries) {
1724
1164
  if (SKIPPED.has(entry2.name)) continue;
1725
1165
  const rel = prefix ? `${prefix}/${entry2.name}` : entry2.name;
1726
- if (entry2.isDirectory()) walk(join6(dir, entry2.name), rel);
1166
+ if (entry2.isDirectory()) walk(join5(dir, entry2.name), rel);
1727
1167
  else out.push(rel);
1728
1168
  if (out.length > MAX_ENVIRONMENT_FILES) return;
1729
1169
  }
@@ -1733,15 +1173,15 @@ function listFiles(cwd) {
1733
1173
  }
1734
1174
  function textOf(path, size) {
1735
1175
  if (size > MAX_REDACTED_TEXT_BYTES) return null;
1736
- const data = readFileSync7(path);
1176
+ const data = readFileSync5(path);
1737
1177
  if (data.subarray(0, 8e3).includes(0) || !isUtf82(data)) return null;
1738
1178
  return data;
1739
1179
  }
1740
1180
  async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1741
1181
  const listed = listFiles(cwd);
1742
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.`);
1743
- const scratch = mkdtempSync(join6(tmpdir(), "sky-environment-"));
1744
- const path = join6(scratch, "environment.tar.gz");
1183
+ const scratch = mkdtempSync(join5(tmpdir(), "sky-environment-"));
1184
+ const path = join5(scratch, "environment.tar.gz");
1745
1185
  const leftOut = [];
1746
1186
  const written2 = /* @__PURE__ */ new Set();
1747
1187
  const hash = createHash2("sha256");
@@ -1769,7 +1209,7 @@ async function packEnvironment(cwd, resources, platform = IMPORT_PLATFORM) {
1769
1209
  });
1770
1210
  try {
1771
1211
  for (const rel of listed) {
1772
- const full = join6(cwd, ...rel.split("/"));
1212
+ const full = join5(cwd, ...rel.split("/"));
1773
1213
  if (SECRET_FILE2.test(rel)) {
1774
1214
  leftOut.push({ path: rel, reason: "credential file" });
1775
1215
  continue;
@@ -1923,13 +1363,13 @@ function sniffTitle(filePath) {
1923
1363
  }
1924
1364
  }
1925
1365
  function listClaudeCodeSessions(limit) {
1926
- const root = process.env.CLAUDE_CONFIG_DIR ?? join7(homedir4(), ".claude");
1927
- const projectsDir = join7(root, "projects");
1928
- if (!existsSync5(projectsDir)) return [];
1366
+ const root = process.env.CLAUDE_CONFIG_DIR ?? join6(homedir3(), ".claude");
1367
+ const projectsDir = join6(root, "projects");
1368
+ if (!existsSync4(projectsDir)) return [];
1929
1369
  const sessions = [];
1930
1370
  try {
1931
1371
  for (const encoded of readdirSync4(projectsDir)) {
1932
- const projectDir = join7(projectsDir, encoded);
1372
+ const projectDir = join6(projectsDir, encoded);
1933
1373
  let entries;
1934
1374
  try {
1935
1375
  entries = readdirSync4(projectDir);
@@ -1940,7 +1380,7 @@ function listClaudeCodeSessions(limit) {
1940
1380
  for (const entry2 of entries) {
1941
1381
  if (!entry2.endsWith(".jsonl")) continue;
1942
1382
  const sessionId = entry2.slice(0, -6);
1943
- const mainFile = join7(projectDir, entry2);
1383
+ const mainFile = join6(projectDir, entry2);
1944
1384
  let stat;
1945
1385
  try {
1946
1386
  stat = statSync5(mainFile);
@@ -1950,12 +1390,12 @@ function listClaudeCodeSessions(limit) {
1950
1390
  if (!stat.isFile()) continue;
1951
1391
  let discoveryError;
1952
1392
  const subagentFiles = [];
1953
- const subagentDir = join7(projectDir, sessionId, "subagents");
1954
- if (existsSync5(subagentDir)) {
1393
+ const subagentDir = join6(projectDir, sessionId, "subagents");
1394
+ if (existsSync4(subagentDir)) {
1955
1395
  try {
1956
1396
  const subFiles = readdirSync4(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
1957
1397
  for (const f of subFiles) {
1958
- subagentFiles.push(join7(subagentDir, f));
1398
+ subagentFiles.push(join6(subagentDir, f));
1959
1399
  }
1960
1400
  } catch {
1961
1401
  discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
@@ -1977,7 +1417,7 @@ function listClaudeCodeSessions(limit) {
1977
1417
  return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
1978
1418
  }
1979
1419
  function listCodexSessions(limit) {
1980
- const sessionsDir = join7(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join7(homedir4(), ".codex"), "sessions");
1420
+ const sessionsDir = join6(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join6(homedir3(), ".codex"), "sessions");
1981
1421
  const records = [];
1982
1422
  function walk(dir) {
1983
1423
  let entries;
@@ -1987,7 +1427,7 @@ function listCodexSessions(limit) {
1987
1427
  return;
1988
1428
  }
1989
1429
  for (const entry2 of entries) {
1990
- const full = join7(dir, entry2.name);
1430
+ const full = join6(dir, entry2.name);
1991
1431
  if (entry2.isDirectory()) {
1992
1432
  walk(full);
1993
1433
  continue;
@@ -2074,7 +1514,7 @@ async function mintTrace(base, harness2) {
2074
1514
  function loadSummaryFile(summaryPath) {
2075
1515
  let raw;
2076
1516
  try {
2077
- raw = readFileSync8(summaryPath, "utf8");
1517
+ raw = readFileSync6(summaryPath, "utf8");
2078
1518
  } catch {
2079
1519
  throw new Error(`Cannot read summary file: ${summaryPath}`);
2080
1520
  }
@@ -2117,7 +1557,7 @@ function prepareImport(session2, summaryPath, options = {}) {
2117
1557
  for (const path of session2.files) {
2118
1558
  const stat = statSync5(path);
2119
1559
  if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
2120
- const text = readFileSync8(path, "utf8");
1560
+ const text = readFileSync6(path, "utf8");
2121
1561
  for (const raw of text.split("\n")) {
2122
1562
  if (!raw.trim()) continue;
2123
1563
  let record;
@@ -2190,92 +1630,735 @@ function prepareImport(session2, summaryPath, options = {}) {
2190
1630
  };
2191
1631
  summaryJson = JSON.stringify(summary);
2192
1632
  }
2193
- 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";
2194
- const gaps = [];
2195
- if (encryptedReasoningCount > 0) gaps.push({ kind: "encrypted_reasoning", count: encryptedReasoningCount });
2196
- if (compactionCount > 0) gaps.push({ kind: "compaction", count: compactionCount });
2197
- if (unreadableCount > 0) gaps.push({ kind: "unreadable", count: unreadableCount });
2198
- const cwd = meta.cwd ?? (session2.harness === "claude_code" ? session2.workspace : "");
2199
- return { body, transcriptLines: lines.length, outputBytes, redactionCount, gaps, attachedFiles: files2.attached, leftOutFiles: files2.leftOut, setup: files2.setup, cwd, resourceLines: files2.lines };
2200
- }
2201
- async function importSession(opts) {
2202
- const base = (opts.agentUseUrl ?? agentUseBase()).replace(/\/$/, "");
2203
- const destination2 = new URL(base);
2204
- if (destination2.username || destination2.password || destination2.protocol !== "https:" && !(destination2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(destination2.hostname))) {
2205
- throw new Error("Import requires HTTPS, or a loopback development server.");
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 };
1640
+ }
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.");
1646
+ }
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));
1657
+ }
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}).`);
1669
+ }
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
1686
+ };
1687
+ }
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;
1699
+ }
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
+ }
1719
+ }
1720
+
1721
+ // src/context.ts
1722
+ async function contextRequest(path, universe2) {
1723
+ try {
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 });
1728
+ }
1729
+ }
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) };
1733
+ }
1734
+ function memoryBody(operation, version) {
1735
+ return { ...operation, expectedVersion: version };
1736
+ }
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());
1740
+ }
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)}`);
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");
1768
+ }
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;
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);
1873
+ }
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;
1878
+ }
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
+ );
1900
+ }
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
+ };
1912
+ }
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;
1932
+ }
1933
+
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 } : {}
1954
+ };
1955
+ }
1956
+ async function listCatalog(filter = {}) {
1957
+ return (await api("GET", `/catalog/agents${catalogQuery(filter)}`, void 0, optionalAuth())).agents;
1958
+ }
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.");
1976
+ }
1977
+ }
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");
1995
+ }
1996
+
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");
2007
+ }
2008
+ function readIfExists(path) {
2009
+ try {
2010
+ return readFileSync8(path, "utf8");
2011
+ } catch {
2012
+ return void 0;
2013
+ }
2014
+ }
2015
+ function readJson2(path) {
2016
+ const raw = readIfExists(path);
2017
+ if (raw === void 0) return void 0;
2018
+ try {
2019
+ return JSON.parse(raw);
2020
+ } catch {
2021
+ return void 0;
2022
+ }
2023
+ }
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();
2033
+ }
2034
+ return void 0;
2035
+ }
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;
2070
+ }
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 });
2083
+ }
2084
+ }
2085
+ }
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
+ );
2111
+ }
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
+ );
2117
+ }
2118
+ function linkPath(configRoot) {
2119
+ return join7(configRoot, "agentsky-link.json");
2120
+ }
2121
+ function readLink(configRoot) {
2122
+ const link2 = readJson2(linkPath(configRoot));
2123
+ return link2?.agentId ? link2 : void 0;
2124
+ }
2125
+ function writeLink(configRoot, link2) {
2126
+ writeFileSync3(linkPath(configRoot), `${JSON.stringify(link2, null, 2)}
2127
+ `);
2128
+ }
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;
2140
+ try {
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;
2151
+ }
2152
+ }
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] : []
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}`);
2192
+ }
2193
+ }
2194
+ }
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
+ });
2209
+ });
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 {
2216
+ }
2217
+ }
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
+ );
2235
+ }
2236
+ if (opts.dryRun) {
2237
+ console.log("Dry run \u2014 nothing sent.");
2238
+ return;
2239
+ }
2240
+ if (!opts.yes && !await confirm("Launch this as a new cloud agent?")) {
2241
+ console.log("Aborted \u2014 nothing sent.");
2242
+ return;
2243
+ }
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 } : {}
2253
+ };
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
2283
+ });
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);
2291
+ }
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;
2304
+ try {
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
+ );
2206
2321
  }
2207
- opts.onProgress?.("Reading transcript and redacting secrets locally\u2026");
2208
- const { body, transcriptLines, outputBytes, redactionCount, gaps, attachedFiles, leftOutFiles, setup, cwd, resourceLines } = prepareImport(opts.session, opts.summaryPath, { files: opts.files });
2209
- let traceId;
2210
- let key;
2211
- if (opts.importKey && opts.grantTraceId) {
2212
- traceId = opts.grantTraceId;
2213
- key = opts.importKey;
2214
- } else {
2215
- opts.onProgress?.("Minting import key\u2026");
2216
- ({ traceId, key } = await mintTrace(base, opts.session.harness));
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}`);
2217
2335
  }
2218
- opts.onProgress?.("Uploading the complete transcript\u2026");
2219
- const response = await fetch(`${base}/api/traces/ingest`, {
2220
- method: "POST",
2221
- redirect: "error",
2222
- signal: AbortSignal.timeout(12e4),
2223
- headers: { authorization: `Bearer ${key}`, "content-type": "application/x-ndjson", "x-source-harness": opts.session.harness, "user-agent": USER_AGENT },
2224
- body
2225
- });
2226
- if (!response.ok) {
2227
- const result2 = await response.json().catch(() => null);
2228
- throw new Error(typeof result2?.error === "string" ? result2.error : result2?.error?.message ?? `Upload failed (${response.status}).`);
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.");
2229
2345
  }
2230
- const result = await response.json();
2231
- const finalTraceId = result.traceId ?? traceId;
2232
- const environment = opts.files === false || opts.environment === false || !cwd || !existsSync5(cwd) ? null : await uploadEnvironment(base, key, finalTraceId, cwd, resourceLines, opts.onProgress).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
2233
- return {
2234
- traceId: finalTraceId,
2235
- url: `${base}/playground/new?import=${encodeURIComponent(finalTraceId)}`,
2236
- harness: opts.session.harness,
2237
- sessionId: opts.session.sessionId,
2238
- transcriptFiles: opts.session.files.length,
2239
- bytes: outputBytes,
2240
- redactionCount,
2241
- gaps,
2242
- attachedFiles,
2243
- leftOutFiles,
2244
- setup,
2245
- environment
2246
- };
2247
- }
2248
- async function grantCall(base, key, path, body) {
2249
- const response = await fetch(`${base}${path}`, {
2250
- method: "POST",
2251
- redirect: "error",
2252
- signal: AbortSignal.timeout(3e5),
2253
- headers: { authorization: `Bearer ${key}`, "content-type": "application/json", "user-agent": USER_AGENT },
2254
- body: JSON.stringify(body)
2255
- });
2256
- const answer = await response.json().catch(() => null);
2257
- if (!response.ok) throw new Error(typeof answer?.error === "string" ? answer.error : answer?.error?.message ?? `The environment upload failed (${response.status}).`);
2258
- return answer;
2259
- }
2260
- async function uploadEnvironment(base, key, traceId, cwd, resourceLines, onProgress) {
2261
- onProgress?.("Packing the working folder (secrets redacted locally)\u2026");
2262
- const packed = await packEnvironment(cwd, resourcesOf(resourceLines));
2263
- try {
2264
- const path = `/api/traces/${encodeURIComponent(traceId)}/environment`;
2265
- const { upload } = await grantCall(base, key, path, { name: packed.folder, size: packed.bytes, sha256: packed.sha256, files: packed.files });
2266
- let shown = -1;
2267
- await transfer(upload, packed.path, (sent, total) => {
2268
- const percent = Math.floor(sent / total * 100);
2269
- if (percent >= shown + 10 || sent === total) {
2270
- shown = percent;
2271
- onProgress?.(`Uploading the working folder\u2026 ${percent}% of ${(total / 1024 / 1024).toFixed(1)} MiB`);
2272
- }
2273
- });
2274
- await grantCall(base, key, `${path}/complete`, {});
2275
- return { folder: packed.folder, files: packed.files, bytes: packed.bytes, redacted: packed.redacted, leftOut: packed.leftOut.length };
2276
- } finally {
2277
- packed.dispose();
2346
+ if (local.droppedServers.length) {
2347
+ console.log(
2348
+ ` left out of clone (secret-bearing): ${local.droppedServers.map((d) => `${d.name} MCP`).join(", ")}`
2349
+ );
2278
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);
2279
2362
  }
2280
2363
 
2281
2364
  // src/channels.ts
@@ -2342,7 +2425,7 @@ import { hostname } from "os";
2342
2425
  import { spawn as spawn2 } from "child_process";
2343
2426
 
2344
2427
  // src/mcp-register.ts
2345
- import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
2428
+ import { existsSync as existsSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
2346
2429
  import { spawnSync as spawnSync3 } from "child_process";
2347
2430
  import { homedir as homedir6 } from "os";
2348
2431
  import { join as join10 } from "path";
@@ -2350,7 +2433,7 @@ import { parse as parseToml3 } from "smol-toml";
2350
2433
  import { isMap, parseDocument } from "yaml";
2351
2434
 
2352
2435
  // src/local-agents.ts
2353
- import { existsSync as existsSync6 } from "fs";
2436
+ import { existsSync as existsSync7 } from "fs";
2354
2437
  import { homedir as homedir5 } from "os";
2355
2438
  import { join as join8 } from "path";
2356
2439
  function localAgents(env = process.env, home = homedir5()) {
@@ -2364,7 +2447,7 @@ function localAgents(env = process.env, home = homedir5()) {
2364
2447
  ];
2365
2448
  }
2366
2449
  function detectLocalAgents(env = process.env, home = homedir5()) {
2367
- return localAgents(env, home).filter((agent2) => existsSync6(agent2.root));
2450
+ return localAgents(env, home).filter((agent2) => existsSync7(agent2.root));
2368
2451
  }
2369
2452
  function selectLocalAgents(ids, env = process.env, home = homedir5()) {
2370
2453
  if (!ids?.length) return detectLocalAgents(env, home);
@@ -2487,6 +2570,12 @@ Details: \`${REFERENCES_DIR}/workspaces.md\` in this skill's folder.
2487
2570
  cheaper or different agent to get around a refusal.
2488
2571
  - \`get_usage\` shows the balance; check it before starting several cloud sessions
2489
2572
  at once so the person is told once, up front, instead of after the first fails.
2573
+ - Anything else the person can do on agentsky.dev or in the Mac app (rename or
2574
+ archive a thread, edit a Library file or skill, change an agent's model, invite
2575
+ a member, \u2026) is an API operation: find it with \`search_api\`, read its
2576
+ parameters with \`describe_api\`, and run it with \`call_api\`. It acts as the
2577
+ person with their own key, so make only the change they asked for. Where MCP
2578
+ is missing, \`sky api search <words>\` and \`sky api <METHOD> <path>\` do the same.
2490
2579
 
2491
2580
  ## How to pick
2492
2581
 
@@ -2874,7 +2963,7 @@ function installSkill(agent2, opts = {}) {
2874
2963
  var MCP_SERVER_NAME = SKILL_NAME;
2875
2964
  function detectHermes(env = process.env, home = homedir6()) {
2876
2965
  const root = env.HERMES_HOME || join10(home, ".hermes");
2877
- return existsSync7(root) ? { id: "hermes", name: "Hermes", root } : null;
2966
+ return existsSync8(root) ? { id: "hermes", name: "Hermes", root } : null;
2878
2967
  }
2879
2968
  function detectMcpClients(env = process.env, home = homedir6()) {
2880
2969
  const hermes = detectHermes(env, home);
@@ -3034,7 +3123,7 @@ function refreshMcpRegistrations(opts, clients = detectMcpClients(), runner = sy
3034
3123
  }
3035
3124
 
3036
3125
  // src/shell-env.ts
3037
- import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
3126
+ import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
3038
3127
  import { homedir as homedir7 } from "os";
3039
3128
  import { basename as basename6, dirname as dirname5, join as join11 } from "path";
3040
3129
  var TOKEN_ENV = "AGENTSKY_API_KEY";
@@ -3050,13 +3139,13 @@ function shellStartupFiles(env = process.env, home = homedir7(), platform = proc
3050
3139
  const shell = basename6(env.SHELL ?? "");
3051
3140
  const files2 = [];
3052
3141
  const zshenv = join11(env.ZDOTDIR || home, ".zshenv");
3053
- if (shell === "zsh" || platform === "darwin" || existsSync8(zshenv)) files2.push({ path: zshenv, kind: "sh" });
3142
+ if (shell === "zsh" || platform === "darwin" || existsSync9(zshenv)) files2.push({ path: zshenv, kind: "sh" });
3054
3143
  const bashrc = join11(home, ".bashrc");
3055
- if (shell === "bash" || existsSync8(bashrc)) files2.push({ path: bashrc, kind: "sh" });
3144
+ if (shell === "bash" || existsSync9(bashrc)) files2.push({ path: bashrc, kind: "sh" });
3056
3145
  const bashProfile = join11(home, ".bash_profile");
3057
- if (existsSync8(bashProfile)) files2.push({ path: bashProfile, kind: "sh" });
3146
+ if (existsSync9(bashProfile)) files2.push({ path: bashProfile, kind: "sh" });
3058
3147
  const fish = join11(env.XDG_CONFIG_HOME || join11(home, ".config"), "fish");
3059
- if (shell === "fish" || existsSync8(fish)) files2.push({ path: join11(fish, "conf.d", "agentsky.fish"), kind: "fish" });
3148
+ if (shell === "fish" || existsSync9(fish)) files2.push({ path: join11(fish, "conf.d", "agentsky.fish"), kind: "fish" });
3060
3149
  return files2;
3061
3150
  }
3062
3151
  function writeShellEnv(token, opts = {}) {
@@ -3072,7 +3161,7 @@ function writeShellEnv(token, opts = {}) {
3072
3161
  writeFileSync6(fish, `${header2}${token ? `set -gx ${TOKEN_ENV} ${quoted(token)}` : `set -e ${TOKEN_ENV}`}
3073
3162
  `, { mode: 384 });
3074
3163
  for (const file of startup) {
3075
- const existing = existsSync8(file.path) ? readFileSync11(file.path, "utf8") : "";
3164
+ const existing = existsSync9(file.path) ? readFileSync11(file.path, "utf8") : "";
3076
3165
  if (existing.includes(HOOK_MARK)) continue;
3077
3166
  const line = file.kind === "fish" ? `${HOOK_MARK}
3078
3167
  test -f ${quoted(fish)}; and source ${quoted(fish)}
@@ -3443,12 +3532,42 @@ function clip(value, max = 120) {
3443
3532
  const text = typeof value === "string" ? value : JSON.stringify(value);
3444
3533
  return text.length > max ? `${text.slice(0, max)}\u2026` : text;
3445
3534
  }
3446
- function renderEvent(event, data) {
3535
+ function createEventRenderer(out = process.stdout) {
3536
+ const seen = /* @__PURE__ */ new Set();
3537
+ let openStream = null;
3538
+ const closeLine = () => {
3539
+ if (openStream === null) return;
3540
+ out.write("\n");
3541
+ openStream = null;
3542
+ };
3543
+ return (event, data) => {
3544
+ const id = typeof data.id === "string" ? data.id : void 0;
3545
+ if (id) {
3546
+ if (seen.has(id)) return;
3547
+ seen.add(id);
3548
+ }
3549
+ if (event !== "agent.message") {
3550
+ if (event && event !== "keepalive") closeLine();
3551
+ renderOther(event, data);
3552
+ return;
3553
+ }
3554
+ const parts = Array.isArray(data.parts) ? data.parts : [];
3555
+ const streamId = typeof parts[0]?.stream_id === "string" ? parts[0].stream_id : null;
3556
+ const text = String(data.text ?? "");
3557
+ if (streamId === null) {
3558
+ closeLine();
3559
+ out.write(`${text}
3560
+ `);
3561
+ } else {
3562
+ if (openStream !== streamId) closeLine();
3563
+ out.write(text);
3564
+ openStream = streamId;
3565
+ }
3566
+ };
3567
+ }
3568
+ function renderOther(event, data) {
3447
3569
  const part = data.part;
3448
3570
  switch (event) {
3449
- case "agent.message":
3450
- console.log(String(data.text ?? ""));
3451
- break;
3452
3571
  case "agent.reasoning":
3453
3572
  console.log(dim(`\xB7 ${clip(part?.text ?? "")}`));
3454
3573
  break;
@@ -3480,11 +3599,12 @@ function renderEvent(event, data) {
3480
3599
  }
3481
3600
 
3482
3601
  // src/session-read.ts
3483
- var TURN_TYPES = ["user.message", "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
3602
+ var TURN_STARTS = /* @__PURE__ */ new Set(["user.message", "goal.defined", "goal.iteration"]);
3603
+ var TURN_TYPES = [...TURN_STARTS, "agent.message", "session.status_idle", "session.status_terminated", "session.error"];
3484
3604
  function latestTurn(events) {
3485
3605
  let start = 0;
3486
3606
  for (let index = events.length - 1; index >= 0; index -= 1) {
3487
- if (events[index].type === "user.message") {
3607
+ if (TURN_STARTS.has(events[index].type)) {
3488
3608
  start = index + 1;
3489
3609
  break;
3490
3610
  }
@@ -3531,7 +3651,7 @@ import { createInterface as createInterface3 } from "readline";
3531
3651
 
3532
3652
  // src/setup-local.ts
3533
3653
  import { isUtf8 as isUtf83 } from "buffer";
3534
- import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
3654
+ import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
3535
3655
  import { homedir as homedir8 } from "os";
3536
3656
  import { dirname as dirname6, isAbsolute as isAbsolute2, join as join13, resolve as resolve6 } from "path";
3537
3657
  import { parse as parseToml4 } from "smol-toml";
@@ -3572,7 +3692,7 @@ function readLocalSkills(agents) {
3572
3692
  const skills2 = [];
3573
3693
  const ignored = [];
3574
3694
  for (const agent2 of agents) {
3575
- if (!existsSync9(agent2.skillsDir)) continue;
3695
+ if (!existsSync10(agent2.skillsDir)) continue;
3576
3696
  for (const entry2 of readdirSync6(agent2.skillsDir, { withFileTypes: true })) {
3577
3697
  const dir = join13(agent2.skillsDir, entry2.name);
3578
3698
  let isDir2 = entry2.isDirectory();
@@ -3583,7 +3703,7 @@ function readLocalSkills(agents) {
3583
3703
  isDir2 = false;
3584
3704
  }
3585
3705
  }
3586
- if (!isDir2 || !existsSync9(join13(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
3706
+ if (!isDir2 || !existsSync10(join13(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
3587
3707
  const name = frontmatterName(readFileSync12(join13(dir, "SKILL.md"), "utf8"));
3588
3708
  if (!name) {
3589
3709
  ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
@@ -3690,7 +3810,7 @@ function findLocalSecrets(agents, home = homedir8()) {
3690
3810
 
3691
3811
  // src/subscriptions.ts
3692
3812
  import { spawn as spawn3 } from "child_process";
3693
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
3813
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
3694
3814
  import { homedir as homedir9 } from "os";
3695
3815
  import { join as join14 } from "path";
3696
3816
  import { createInterface as createInterface2 } from "readline";
@@ -3789,7 +3909,7 @@ async function connectClaude() {
3789
3909
  }
3790
3910
  function codexSignIn(env = process.env, home = homedir9()) {
3791
3911
  const path = join14(env.CODEX_HOME || join14(home, ".codex"), "auth.json");
3792
- if (!existsSync10(path)) {
3912
+ if (!existsSync11(path)) {
3793
3913
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
3794
3914
  }
3795
3915
  const raw = readFileSync13(path, "utf8");
@@ -3802,7 +3922,7 @@ function codexSignIn(env = process.env, home = homedir9()) {
3802
3922
  }
3803
3923
  function cursorSignIn(env = process.env, home = homedir9()) {
3804
3924
  const path = join14(env.XDG_CONFIG_HOME || join14(home, ".config"), "cursor", "auth.json");
3805
- if (!existsSync10(path)) {
3925
+ if (!existsSync11(path)) {
3806
3926
  throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
3807
3927
  }
3808
3928
  const raw = readFileSync13(path, "utf8");
@@ -3924,7 +4044,9 @@ async function subscriptionUsage(name, account) {
3924
4044
  return usage;
3925
4045
  }
3926
4046
  function renderSubscriptionUsage(name, usage) {
3927
- const label = MODEL_SUBSCRIPTION_PROVIDER_NAME[managedProviderFor(name)];
4047
+ const provider = managedProviderFor(name);
4048
+ const label = MODEL_SUBSCRIPTION_PROVIDER_NAME[provider];
4049
+ if (usage.status === "unavailable" && (usage.reason === "unsupported" || provider === "anthropic" && usage.reason === "permission")) return [`${label}: usage isn't available for this account type`];
3928
4050
  if (usage.status === "unavailable") return [`${label}: usage unavailable (${usage.reason ?? "unknown"})`];
3929
4051
  const head = [`${label}${usage.plan ? ` ${usage.plan}` : ""}`, usage.allowed === false ? "limit reached" : null].filter(Boolean).join(" \xB7 ");
3930
4052
  const rows = usage.windows.map(
@@ -4399,12 +4521,25 @@ async function getSkillUsage(since) {
4399
4521
  const qs = since ? `?since=${encodeURIComponent(since)}` : "";
4400
4522
  return api("GET", `/skills/usage${qs}`);
4401
4523
  }
4524
+ function nextSkills(current, skillId, verb) {
4525
+ const has = current.some((ref) => ref.type === "skill" && ref.skillId === skillId);
4526
+ if (verb === "attach") return has ? null : [...current, { type: "skill", skillId, version: "latest" }];
4527
+ return has ? current.filter((ref) => !(ref.type === "skill" && ref.skillId === skillId)) : null;
4528
+ }
4529
+ async function setAgentSkill(agentId, skillId, verb) {
4530
+ const path = `/agents/${encodeURIComponent(agentId)}`;
4531
+ const { agent: agent2 } = await api("GET", path);
4532
+ const skills2 = nextSkills(agent2.skills ?? [], skillId, verb);
4533
+ if (!skills2) return "unchanged";
4534
+ await api("PATCH", path, { skills: skills2, expectedVersion: agent2.version });
4535
+ return "changed";
4536
+ }
4402
4537
 
4403
4538
  // src/task-export.ts
4404
4539
  import { createHash as createHash6 } from "crypto";
4405
4540
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
4406
4541
  import { dirname as dirname8, join as join17 } from "path";
4407
- import { stringify as stringifyToml2 } from "smol-toml";
4542
+ import { stringify as stringifyToml } from "smol-toml";
4408
4543
  var TEXT_MAX = 2e4;
4409
4544
  function scrub(text, names2) {
4410
4545
  let out = redactPlainText(text).text;
@@ -4582,7 +4717,7 @@ Write your deliverables under /workspace/outputs.
4582
4717
  `);
4583
4718
  out.set(
4584
4719
  "task.toml",
4585
- stringifyToml2({
4720
+ stringifyToml({
4586
4721
  metadata: {
4587
4722
  name: `agentsky-${idHash}`,
4588
4723
  tier: opts.tier,
@@ -4969,41 +5104,43 @@ async function listBrowserProfiles() {
4969
5104
  const { data } = await api("GET", "/browser_profiles");
4970
5105
  return data;
4971
5106
  }
4972
- async function getBrowserProfile(scope) {
4973
- const profiles = await listBrowserProfiles();
4974
- const profile = profiles.find((entry2) => entry2.scope === scope);
4975
- if (!profile) throw new ApiError(404, "not_found", "browser profile not found");
4976
- return profile;
4977
- }
4978
5107
  async function importBrowserProfileState(scope, state) {
4979
5108
  return api("POST", `/browser_profiles/${encodeURIComponent(scope)}/imports`, state);
4980
5109
  }
4981
- async function listBrowserProfileSites(scope) {
4982
- const profile = await getBrowserProfile(scope);
4983
- 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 ?? [];
4984
5113
  }
4985
- async function browserSiteReference(scope, site) {
4986
- const sites = await listBrowserProfileSites(scope);
5114
+ async function removeBrowserProfileSite(site) {
5115
+ const sites = await listBrowserProfileSites();
4987
5116
  const row = sites.find((entry2) => entry2.site === site) ?? sites.find((entry2) => entry2.site_id === site);
4988
- if (!row) throw new ApiError(404, "not_found", "site not found in this browser profile");
4989
- 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)}`);
4990
5119
  }
4991
- async function removeBrowserProfileSite(scope, site) {
4992
- 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;
4993
5126
  }
4994
- async function moveBrowserProfileSite(scope, site, to) {
4995
- const reference = await browserSiteReference(scope, site);
4996
- const [identifier, query] = reference.split("?");
4997
- 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)}`);
4998
5129
  }
4999
5130
  function renderBrowserProfileSite(site) {
5000
5131
  const who = site.signed_in_by?.email ?? site.signed_in_by?.name ?? "unknown";
5001
5132
  const used = site.last_used_at ? `used ${site.last_used_at.slice(0, 10)}` : "never used";
5002
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}` : ""}`;
5003
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
+ }
5004
5139
  function renderBrowserProfile(profile) {
5005
- 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}`];
5006
5142
  for (const site of profile.sites) lines.push(` ${renderBrowserProfileSite(site)}`);
5143
+ for (const share of profile.shares ?? []) lines.push(` ${renderBrowserProfileShare(share)}`);
5007
5144
  return lines.join("\n");
5008
5145
  }
5009
5146
 
@@ -5159,6 +5296,148 @@ function renderLibraryArtifact(artifact2) {
5159
5296
  return `${artifact2.id} ${artifact2.name} ${version}${artifact2.shared ? " shared" : ""}`;
5160
5297
  }
5161
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
+
5367
+ // src/api-command.ts
5368
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
5369
+ var API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
5370
+ function readData(data, stdin = () => readFileSync17(0, "utf8")) {
5371
+ if (data === void 0) return void 0;
5372
+ const raw = data === "-" ? stdin() : data.startsWith("@") ? readFileSync17(data.slice(1), "utf8") : data;
5373
+ try {
5374
+ JSON.parse(raw);
5375
+ } catch {
5376
+ throw new Error("--data must be JSON (inline, @file or - for stdin)");
5377
+ }
5378
+ return raw;
5379
+ }
5380
+ function requestPath(path, query = []) {
5381
+ if (!path.startsWith("/") || path.startsWith("//")) throw new Error("path must start with /, e.g. /threads");
5382
+ const clean = path.replace(/^\/v1(?=\/)/, "");
5383
+ const params = new URLSearchParams();
5384
+ for (const pair of query) {
5385
+ const at = pair.indexOf("=");
5386
+ if (at <= 0) throw new Error(`--query needs key=value, got "${pair}"`);
5387
+ params.append(pair.slice(0, at), pair.slice(at + 1));
5388
+ }
5389
+ const sep4 = clean.includes("?") ? "&" : "?";
5390
+ const encoded = params.toString();
5391
+ return encoded ? `${clean}${sep4}${encoded}` : clean;
5392
+ }
5393
+ function matchOperations(doc, words) {
5394
+ const needles = words.map((w) => w.toLowerCase());
5395
+ const out = [];
5396
+ for (const [path, item] of Object.entries(doc.paths ?? {})) {
5397
+ for (const [method, op] of Object.entries(item)) {
5398
+ if (!API_METHODS.includes(method.toUpperCase())) continue;
5399
+ const hay = `${method} ${path} ${op.summary ?? ""} ${op.operationId ?? ""}`.toLowerCase();
5400
+ if (needles.every((n) => hay.includes(n))) out.push({ method: method.toUpperCase(), path, summary: op.summary, operationId: op.operationId });
5401
+ }
5402
+ }
5403
+ return out.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
5404
+ }
5405
+ async function searchApi(words) {
5406
+ const res = await apiRaw("GET", "/openapi.json", { auth: false });
5407
+ if (!res.ok) throw new Error(`could not read the API reference (${res.status})`);
5408
+ const ops = matchOperations(await res.json(), words);
5409
+ if (!ops.length) {
5410
+ console.log("no operations match");
5411
+ return;
5412
+ }
5413
+ for (const op of ops) console.log(`${op.method.padEnd(6)} ${op.path}${op.summary ? ` \u2014 ${op.summary}` : ""}`);
5414
+ }
5415
+ async function callApi(method, path, opts) {
5416
+ const verb = method.toUpperCase();
5417
+ if (!API_METHODS.includes(verb)) throw new Error(`method must be one of ${API_METHODS.join(", ")} or "search"`);
5418
+ const body = readData(opts.data);
5419
+ if (body !== void 0 && verb === "GET") throw new Error("a GET takes no --data; use --query");
5420
+ const res = await apiRaw(verb, requestPath(path, opts.query), { body, idempotencyKey: opts.idempotencyKey, universe: opts.workspace });
5421
+ const type = res.headers.get("content-type") ?? "";
5422
+ if (opts.output) {
5423
+ writeFileSync9(opts.output, new Uint8Array(await res.arrayBuffer()));
5424
+ console.error(`${res.status} \u2192 ${opts.output}`);
5425
+ } else if (/json/.test(type)) {
5426
+ const text = await res.text();
5427
+ let printed = text;
5428
+ try {
5429
+ printed = JSON.stringify(JSON.parse(text), null, 2);
5430
+ } catch {
5431
+ }
5432
+ (res.ok ? console.log : console.error)(printed);
5433
+ } else if (/^text\/|xml|csv|markdown/.test(type) || !type) {
5434
+ (res.ok ? console.log : console.error)(await res.text());
5435
+ } else {
5436
+ console.error(`${res.status}: ${type} body (${res.headers.get("content-length") ?? "?"} bytes); pass --output <file> to save it`);
5437
+ }
5438
+ if (!res.ok && res.status !== 304) process.exitCode = 1;
5439
+ }
5440
+
5162
5441
  // src/billing-plans.ts
5163
5442
  var readCodingPlan = () => api("GET", "/billing/coding-plan");
5164
5443
  function joinCodingPlan(dollars2, requestId) {
@@ -5233,20 +5512,31 @@ function wholeNumber2(flag, value) {
5233
5512
  return Number(value.trim());
5234
5513
  }
5235
5514
 
5515
+ // src/workspace-flag.ts
5516
+ function workspaceFlagIsCommands(argv, commandPath) {
5517
+ let at = 0;
5518
+ let matched = 0;
5519
+ for (; at < argv.length && matched < commandPath.length; at += 1) {
5520
+ if (argv[at] === commandPath[matched]) matched += 1;
5521
+ }
5522
+ if (matched < commandPath.length) return false;
5523
+ return argv.slice(at).some((arg) => arg === "--workspace" || arg.startsWith("--workspace="));
5524
+ }
5525
+
5236
5526
  // src/harness.ts
5237
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
5238
- import { dirname as dirname9, join as join18, resolve as resolve8 } from "path";
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";
5239
5529
  var HARNESS_FILE = "harness.json";
5240
5530
  function harnessFilePath(dir) {
5241
- return join18(resolve8(dir), HARNESS_FILE);
5531
+ return join19(resolve8(dir), HARNESS_FILE);
5242
5532
  }
5243
5533
  function readHarnessFile(dir) {
5244
5534
  const path = harnessFilePath(dir);
5245
- if (!existsSync11(path)) {
5535
+ if (!existsSync12(path)) {
5246
5536
  throw new Error(`no ${HARNESS_FILE} found in ${resolve8(dir)} \u2014 run \`sky harness init\` first`);
5247
5537
  }
5248
5538
  try {
5249
- return JSON.parse(readFileSync16(path, "utf8"));
5539
+ return JSON.parse(readFileSync18(path, "utf8"));
5250
5540
  } catch (err) {
5251
5541
  throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
5252
5542
  }
@@ -5273,11 +5563,11 @@ var INIT_COMMENT = `# harness.json \u2014 workspace harness profile skeleton
5273
5563
  `;
5274
5564
  function harnessInit(dir) {
5275
5565
  const path = harnessFilePath(dir);
5276
- if (existsSync11(path)) {
5566
+ if (existsSync12(path)) {
5277
5567
  throw new Error(`${path} already exists \u2014 delete it to re-init`);
5278
5568
  }
5279
5569
  mkdirSync6(dirname9(path), { recursive: true });
5280
- writeFileSync9(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
5570
+ writeFileSync10(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
5281
5571
  `);
5282
5572
  console.log(`Created ${path}`);
5283
5573
  console.log(INIT_COMMENT);
@@ -5410,9 +5700,16 @@ function printModelBilling(session2) {
5410
5700
  }
5411
5701
  var program = new Command();
5412
5702
  program.name("sky").description("Launch and talk to long-lived agents \u2014 agentsky.dev").version(CLI_VERSION).option("--workspace <slug>", "workspace to act in (overrides sky universe switch and SKY_UNIVERSE; alias of --universe on sub-commands)");
5413
- program.hook("preAction", () => {
5703
+ program.hook("preAction", (_root, action) => {
5414
5704
  const workspace2 = program.opts().workspace;
5415
- if (workspace2) process.env.SKY_WORKSPACE = workspace2;
5705
+ if (!workspace2) return;
5706
+ const path = [];
5707
+ for (let command = action; command && command !== program; command = command.parent) path.unshift(command.name());
5708
+ if (action.options.some((option) => option.long === "--workspace") && workspaceFlagIsCommands(process.argv.slice(2), path)) {
5709
+ action.setOptionValue("workspace", workspace2);
5710
+ return;
5711
+ }
5712
+ process.env.SKY_WORKSPACE = workspace2;
5416
5713
  });
5417
5714
  var lastProgressDecile = -1;
5418
5715
  function uploadProgress(sent, total) {
@@ -5447,6 +5744,9 @@ auth.command("whoami").description("who the current token acts as").action(
5447
5744
  console.log(
5448
5745
  `${who.user.email ?? "unknown"} \xB7 universe ${who.universe.slug}${who.universe.isPersonal ? " (default)" : ""} \xB7 scopes ${who.scopes.join(",")}`
5449
5746
  );
5747
+ if (who.credential_universe) {
5748
+ console.log(` this key is fixed to workspace ${who.credential_universe}: it cannot act in other workspaces or create new ones (make an account-wide key with \`sky keys create\`, or \`sky auth login\`)`);
5749
+ }
5450
5750
  })
5451
5751
  );
5452
5752
  auth.command("connect <provider>").description("connect a model subscription: claude (setup-token) or chatgpt (Codex sign-in); --browser signs any plan in on its own page").option("-y, --yes", "skip the consent prompt").option("--browser", "sign in on the provider's page instead of using this machine's sign-in (claude, chatgpt, cursor, grok, muse)").option("--account <id>", "with --browser: re-sign an additional account, or `new` to add one").option("--label <label>", "with --browser: name the account").option("--no-open", "with --browser: print the sign-in link without opening it").action(
@@ -5583,6 +5883,12 @@ workspace.command("list").action(
5583
5883
  })
5584
5884
  );
5585
5885
  workspace.command("delete <slug>").description("permanently delete a workspace you own and everything in it (asks you to type the slug)").option("-y, --yes", "skip typing the slug").action(run(deleteWorkspaceCommand));
5886
+ async function renameWorkspaceCommand(slug, name) {
5887
+ const { universe: renamed } = await api("PATCH", `/universes/${encodeURIComponent(slug)}`, { name: name.join(" ") });
5888
+ console.log(`${renamed.slug} is now "${renamed.name}"`);
5889
+ }
5890
+ universe.command("rename <slug> <name...>").description("rename a workspace you own (its slug stays)").action(run(renameWorkspaceCommand));
5891
+ workspace.command("rename <slug> <name...>").description("rename a workspace you own (alias of universe rename)").action(run(renameWorkspaceCommand));
5586
5892
  var policy = program.command("policy").description("policy rules: tool calls, spend caps, call counts, capabilities");
5587
5893
  async function policyScope(opts) {
5588
5894
  return opts.session ? { session: await resolveSessionRef(opts.session) } : { slug: await workspaceSlug(opts.universe) };
@@ -5693,7 +5999,7 @@ agent.command("destroy <id>").description("delete the spec; requires all session
5693
5999
  console.log(`Destroyed ${id}`);
5694
6000
  })
5695
6001
  );
5696
- agent.command("message <id> [text]").description("send one message to the default session and stream the turn until idle").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
6002
+ agent.command("message <id> [text]").description("send one message to the default session and stream the turn until idle").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks); a file_\u2026 id attaches that stored file").option("--file-id <id...>", "attach files already uploaded (`sky files upload`), by id, without uploading again").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
5697
6003
  run(
5698
6004
  async (id, text, opts) => sendAndFollow(await resolveSessionRef(id), text ?? "", opts)
5699
6005
  )
@@ -5807,7 +6113,7 @@ session.command("delete <id>").description("permanently delete the session, its
5807
6113
  console.log(`Deleted ${id}`);
5808
6114
  })
5809
6115
  );
5810
- session.command("message <ref> [text]").description("send one message (session id, or agent id = its default session)").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
6116
+ session.command("message <ref> [text]").description("send one message (session id, or agent id = its default session)").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks); a file_\u2026 id attaches that stored file").option("--file-id <id...>", "attach files already uploaded (`sky files upload`), by id, without uploading again").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").option("--plan", "plan mode: the agent inspects and answers with a plan, then waits for you to choose to implement it").option("--build", "build mode for this turn, overriding a session left in plan mode").action(
5811
6117
  run(
5812
6118
  async (ref, text, opts) => sendAndFollow(await resolveSessionRef(ref), text ?? "", opts)
5813
6119
  )
@@ -6000,7 +6306,7 @@ sessionWorkspace.command("cat <ref> <path>").description("print a file (up to 1
6000
6306
  run(async (ref, path, opts) => {
6001
6307
  const bytes = await readFile(await resolveSessionRef(ref), path);
6002
6308
  if (opts.out) {
6003
- writeFileSync10(opts.out, bytes);
6309
+ writeFileSync11(opts.out, bytes);
6004
6310
  console.error(`saved ${bytes.length} bytes to ${opts.out}`);
6005
6311
  } else process.stdout.write(bytes);
6006
6312
  })
@@ -6017,7 +6323,7 @@ sessionWorkspace.command("patch <ref> [repo]").description("the work in a reposi
6017
6323
  console.error(`${meta.repo}: ${meta.commits.length} commit${meta.commits.length === 1 ? "" : "s"} since ${meta.base_branch || "the imported copy"} (${meta.base_commit.slice(0, 12)})`);
6018
6324
  if (!patch.length) console.error("nothing changed");
6019
6325
  else if (opts.out) {
6020
- writeFileSync10(opts.out, patch);
6326
+ writeFileSync11(opts.out, patch);
6021
6327
  console.error(`wrote ${opts.out} \u2014 apply with: git apply ${opts.out}`);
6022
6328
  } else process.stdout.write(patch);
6023
6329
  })
@@ -6071,9 +6377,10 @@ function interactionMode(opts) {
6071
6377
  }
6072
6378
  async function uploadAttachments(opts) {
6073
6379
  const files2 = [];
6074
- for (const path of [...opts.image ?? [], ...opts.file ?? []]) {
6075
- const file = await uploadLocalFile(path);
6076
- console.error(`uploaded ${file.filename} \u2192 ${file.id}`);
6380
+ const refs = [...opts.image ?? [], ...opts.file ?? []].map((ref) => ({ ref, byId: false }));
6381
+ for (const { ref, byId } of [...refs, ...(opts.fileId ?? []).map((ref2) => ({ ref: ref2, byId: true }))]) {
6382
+ const { file, uploaded } = await attachment(ref, { byId });
6383
+ console.error(uploaded ? `uploaded ${file.filename} \u2192 ${file.id}` : `attaching ${file.id} (${file.filename})`);
6077
6384
  files2.push(file);
6078
6385
  }
6079
6386
  return files2;
@@ -6090,9 +6397,10 @@ async function sendAndFollow(sessionId, text, opts) {
6090
6397
  const controller = new AbortController();
6091
6398
  let opened = null;
6092
6399
  if (!opts.detach) {
6400
+ const render = createEventRenderer();
6093
6401
  opened = (async () => {
6094
6402
  for await (const frame of streamEvents(sessionId, controller.signal)) {
6095
- renderEvent(frame.event, frame.data);
6403
+ render(frame.event, frame.data);
6096
6404
  if (turnOver(frame)) {
6097
6405
  controller.abort();
6098
6406
  break;
@@ -6113,10 +6421,11 @@ async function chatLoop(sessionId, opts = {}) {
6113
6421
  const controller = new AbortController();
6114
6422
  const rl = createInterface6({ input: process.stdin, output: process.stdout, prompt: "> " });
6115
6423
  let system = opts.system;
6424
+ const render = createEventRenderer();
6116
6425
  void (async () => {
6117
6426
  try {
6118
6427
  for await (const frame of streamEvents(sessionId, controller.signal)) {
6119
- renderEvent(frame.event, frame.data);
6428
+ render(frame.event, frame.data);
6120
6429
  if (frame.event === "session.status_terminated") rl.close();
6121
6430
  else if (frame.event === "session.status_idle") rl.prompt();
6122
6431
  }
@@ -6223,10 +6532,32 @@ secrets.command("unset <id> <key>").action(
6223
6532
  console.log(`unset ${key}`);
6224
6533
  })
6225
6534
  );
6226
- program.command("launch").description("scaffold ./agent.toml if absent, create the agent, print its address").option("-f, --file <path>", "agent file", "agent.toml").option("--type <type>", "override the file's harness: hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode").option("--llm <llm>", "override the file's model (must suit the harness)").action(
6535
+ async function launchFromFile(path, spec, opts) {
6536
+ const { vcpus, memoryMb, ...specBody } = spec;
6537
+ const body = {
6538
+ ...specBody,
6539
+ ...opts.type ? { agentType: opts.type } : {},
6540
+ ...opts.llm ? { llm: opts.llm } : {}
6541
+ };
6542
+ const { agent: created } = await api("POST", "/agents", body);
6543
+ const { session: session2 } = await api("POST", "/sessions", {
6544
+ agent: created.id,
6545
+ ...opts.start ? {} : { provision: "deferred" },
6546
+ ...vcpus !== void 0 ? { vcpus } : {},
6547
+ ...memoryMb !== void 0 ? { memoryMb } : {}
6548
+ });
6549
+ stampId(path, created.id);
6550
+ console.log(`Launched ${created.id} (${created.agentType} \xB7 ${created.llm})`);
6551
+ printModelBilling(session2);
6552
+ console.log(opts.start ? " its first session is starting now" : " its first session starts on the first message");
6553
+ if (!spec.llm && !opts.llm) console.log(` model: ${created.llm}, the ${created.agentType} default \u2014 set llm = "\u2026" in ${basename9(path)} and run \`sky deploy\` to change it`);
6554
+ console.log(` sky agent message ${created.id} "hello"`);
6555
+ console.log(` sky agent chat ${created.id}`);
6556
+ }
6557
+ program.command("launch").description("scaffold ./agent.toml if absent, create the agent and its first session (idle until the first message), print its address").option("-f, --file <path>", "agent file", "agent.toml").option("--type <type>", "override the file's harness: hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode").option("--llm <llm>", "override the file's model (must suit the harness)").option("--start", "boot the first session now instead of on the first message").action(
6227
6558
  run(async (opts) => {
6228
6559
  const path = resolve9(opts.file);
6229
- if (!existsSync12(path)) {
6560
+ if (!existsSync13(path)) {
6230
6561
  const name = basename9(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
6231
6562
  scaffoldAgentFile(path, name);
6232
6563
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
@@ -6236,55 +6567,27 @@ program.command("launch").description("scaffold ./agent.toml if absent, create t
6236
6567
  console.log(`${file.id} already launched \u2014 use \`sky deploy\` to re-apply.`);
6237
6568
  return;
6238
6569
  }
6239
- const { vcpus, memoryMb, ...specBody } = spec;
6240
- const body = {
6241
- ...specBody,
6242
- ...opts.type ? { agentType: opts.type } : {},
6243
- ...opts.llm ? { llm: opts.llm } : {}
6244
- };
6245
- const { agent: created } = await api("POST", "/agents", body);
6246
- console.log("Starting its runtime\u2026");
6247
- const { session: session2 } = await api("POST", "/sessions", {
6248
- agent: created.id,
6249
- ...vcpus !== void 0 ? { vcpus } : {},
6250
- ...memoryMb !== void 0 ? { memoryMb } : {}
6251
- });
6252
- stampId(path, created.id);
6253
- console.log(`Launched ${created.id} (${created.agentType} \xB7 ${created.llm})`);
6254
- printModelBilling(session2);
6255
- console.log(` sky agent message ${created.id} "hello"`);
6256
- console.log(` sky agent chat ${created.id}`);
6570
+ await launchFromFile(path, spec, opts);
6257
6571
  })
6258
6572
  );
6259
- program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
6573
+ program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent: an unchanged file changes nothing)").option("-f, --file <path>", "agent file", "agent.toml").action(
6260
6574
  run(async (opts) => {
6261
6575
  const path = resolve9(opts.file);
6262
6576
  const { file, spec } = readAgentFile(path);
6263
6577
  if (!file.id) {
6264
6578
  console.log("No agent id in the file yet \u2014 running launch instead.");
6265
- const { vcpus, memoryMb, ...specBody } = spec;
6266
- const { agent: created } = await api("POST", "/agents", specBody);
6267
- await api("POST", "/sessions", {
6268
- agent: created.id,
6269
- ...vcpus !== void 0 ? { vcpus } : {},
6270
- ...memoryMb !== void 0 ? { memoryMb } : {}
6271
- });
6272
- stampId(path, created.id);
6273
- console.log(`Launched ${created.id}`);
6579
+ await launchFromFile(path, spec, {});
6274
6580
  return;
6275
6581
  }
6582
+ const ref = encodeURIComponent(file.id);
6583
+ const { agent: before } = await api("GET", `/agents/${ref}`);
6584
+ let promptChanged = false;
6276
6585
  if (spec.prompt !== void 0) {
6277
- await api("PUT", `/agents/${encodeURIComponent(file.id)}/prompt`, {
6278
- prompt: spec.prompt
6279
- });
6586
+ const saved = await api("PUT", `/agents/${ref}/prompt`, { prompt: spec.prompt });
6587
+ promptChanged = saved.unchanged === false;
6280
6588
  }
6281
- await api("PATCH", `/agents/${encodeURIComponent(file.id)}`, {
6282
- displayName: spec.displayName,
6283
- ...spec.capabilities ? { capabilities: spec.capabilities } : {},
6284
- ...spec.reasoningEffort ? { reasoningEffort: spec.reasoningEffort } : {},
6285
- ...spec.skills ? { skills: spec.skills } : {}
6286
- });
6287
- console.log(`Deployed ${file.id} (prompt + settings re-applied)`);
6589
+ const { agent: after } = await api("PATCH", `/agents/${ref}`, deployPatch(spec));
6590
+ for (const line of deployReport(file.id, spec, before, after, promptChanged)) console.log(line);
6288
6591
  })
6289
6592
  );
6290
6593
  program.command("clone").description("clone your local agent (Claude Code, \u2026) into the cloud; re-run to sync its prompt").option("--harness <type>", "claude_code | codex | openclaw (default: detected)").option("--new", "always launch a fresh agent, even if this machine is linked").option("--dry-run", "print the plan and send nothing").option("-y, --yes", "skip the confirmation").option("--open", "open the agent page when done").action(
@@ -6525,14 +6828,14 @@ task.command("get <task> <path>").description("read one task file; prints it, or
6525
6828
  run(async (taskId, path, opts) => {
6526
6829
  const bytes = await readTaskFile(taskId, path);
6527
6830
  if (opts.out) {
6528
- writeFileSync10(opts.out, bytes);
6831
+ writeFileSync11(opts.out, bytes);
6529
6832
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6530
6833
  } else process.stdout.write(bytes);
6531
6834
  })
6532
6835
  );
6533
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(
6534
6837
  run(async (taskId, path, local) => {
6535
- const { file, conflict } = await writeTaskFile(taskId, path, readFileSync17(local), mediaTypeFor(local));
6838
+ const { file, conflict } = await writeTaskFile(taskId, path, readFileSync19(local), mediaTypeFor(local));
6536
6839
  console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
6537
6840
  })
6538
6841
  );
@@ -6605,7 +6908,7 @@ task.command("verdict <task> <did-the-job>").description("record whether the tas
6605
6908
  console.log(`Recorded verdict on task ${updated.id}: did_the_job=${value === "yes"}`);
6606
6909
  })
6607
6910
  );
6608
- var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent by id");
6911
+ var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent (sky skills attach, or [[skills]] in agent.toml)");
6609
6912
  skills.command("upload <dir>").description("upload a skill directory (SKILL.md at its root) as a new skill").option("--name <name>", "display name (default: the directory's name)").option("--json", "print the stored skill as JSON").action(
6610
6913
  run(async (dir, opts) => {
6611
6914
  const { skill } = await uploadSkill(dir, opts.name);
@@ -6613,7 +6916,7 @@ skills.command("upload <dir>").description("upload a skill directory (SKILL.md a
6613
6916
  console.log(JSON.stringify(skill));
6614
6917
  return;
6615
6918
  }
6616
- console.log(`Uploaded ${skill.id} (${skill.displayName ?? dir}). Attach it in agent.toml:`);
6919
+ console.log(`Uploaded ${skill.id} (${skill.displayName ?? dir}). Attach it: sky skills attach ${skill.id} --agent <id>, or in agent.toml:`);
6617
6920
  console.log(` [[skills]]
6618
6921
  type = "skill"
6619
6922
  skillId = "${skill.id}"`);
@@ -6626,6 +6929,18 @@ skills.command("list").description("the workspace's stored skills").action(
6626
6929
  for (const skill of list) console.log(`${skill.id} ${skill.displayName ?? ""}`);
6627
6930
  })
6628
6931
  );
6932
+ for (const [verb, blurb] of [
6933
+ ["attach", "add a stored skill to an agent; its new sessions load it"],
6934
+ ["detach", "remove a skill from an agent; its new sessions no longer load it"]
6935
+ ]) {
6936
+ skills.command(`${verb} <skill>`).description(blurb).requiredOption("--agent <id>", "the agent").action(
6937
+ run(async (skillId, opts) => {
6938
+ const outcome = await setAgentSkill(opts.agent, skillId, verb);
6939
+ if (outcome === "unchanged") console.log(`${skillId} is ${verb === "attach" ? "already" : "not"} attached to ${opts.agent}`);
6940
+ else console.log(`${verb === "attach" ? "Attached" : "Detached"} ${skillId} ${verb === "attach" ? "to" : "from"} ${opts.agent} \u2014 new sessions ${verb === "attach" ? "load it" : "no longer load it"}; running sessions keep the skills they started with`);
6941
+ })
6942
+ );
6943
+ }
6629
6944
  skills.command("usage").description("view and miss counts per skill name in this workspace").option("--since <iso>", `start date (ISO 8601); default is the last 30 days`).option("--json", "print JSON").action(
6630
6945
  run(async (opts) => {
6631
6946
  const summary = await getSkillUsage(opts.since);
@@ -6684,7 +6999,7 @@ library.command("list").description("your Library, newest first").option("--sear
6684
6999
  );
6685
7000
  library.command("upload <path>").description("save a file from this computer to your Library").option("--workspace <key>", "place it in this workspace").action(
6686
7001
  run(async (path, opts) => {
6687
- const bytes = readFileSync17(path);
7002
+ const bytes = readFileSync19(path);
6688
7003
  const { artifact: artifact2 } = await uploadToLibrary({ bytes, name: basename9(path), mediaType: "application/octet-stream" }, `sky-upload-${crypto.randomUUID()}`, opts.workspace);
6689
7004
  console.log(renderLibraryArtifact(artifact2));
6690
7005
  })
@@ -6715,7 +7030,7 @@ library.command("place <id> <workspace>").description('place your artifact in a
6715
7030
  library.command("download <id> <out>").description("save one version's bytes to a file").option("--version <n>", "a saved version (the current one when omitted)").action(
6716
7031
  run(async (id, out, opts) => {
6717
7032
  const { bytes } = await downloadLibraryArtifact(id, opts.version ? Number(opts.version) : void 0);
6718
- writeFileSync10(out, bytes);
7033
+ writeFileSync11(out, bytes);
6719
7034
  console.log(`wrote ${bytes.length} bytes to ${out}`);
6720
7035
  })
6721
7036
  );
@@ -6736,7 +7051,7 @@ artifact.command("get <id> <path>").description("read one file of an artifact; p
6736
7051
  run(async (id, path, opts) => {
6737
7052
  const bytes = await readArtifactFile(id, path);
6738
7053
  if (opts.out) {
6739
- writeFileSync10(opts.out, bytes);
7054
+ writeFileSync11(opts.out, bytes);
6740
7055
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
6741
7056
  } else process.stdout.write(bytes);
6742
7057
  })
@@ -6753,6 +7068,35 @@ artifact.command("rm <id>").description("delete an artifact").action(
6753
7068
  console.log(`Deleted ${id}`);
6754
7069
  })
6755
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
+ }));
6756
7100
  function routineLine(r) {
6757
7101
  const target = r.target.type === "session" ? `\u2192 ${r.target.session_id}` : `+ ${r.target.agent}`;
6758
7102
  const cron = r.schedule ? `${r.schedule.expression} ${r.schedule.timezone}` : "manual-only";
@@ -6835,6 +7179,12 @@ for (const [verb, blurb] of [
6835
7179
  })
6836
7180
  );
6837
7181
  }
7182
+ routine.command("rm <id>").description("delete a routine: it never runs again; the sessions it started are kept").action(
7183
+ run(async (id) => {
7184
+ await api("DELETE", `/routines/${encodeURIComponent(id)}`);
7185
+ console.log(`Deleted routine ${id}. It will not run again; sessions it started are kept.`);
7186
+ })
7187
+ );
6838
7188
  routine.command("runs [id]").description("every fire attempt, newest first \u2014 of one routine, or all of them").option("--failed", "only runs that failed to start").option("--limit <n>", "how many (default 20)").action(
6839
7189
  run(async (id, opts) => {
6840
7190
  const params = new URLSearchParams({ limit: opts.limit ?? "20" });
@@ -7052,7 +7402,7 @@ usageOptions(
7052
7402
  run(async (opts) => {
7053
7403
  const { bytes } = await apiBytes("GET", `/billing/usage/export${usageQuery(opts)}`);
7054
7404
  if (opts.out) {
7055
- writeFileSync10(opts.out, bytes);
7405
+ writeFileSync11(opts.out, bytes);
7056
7406
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
7057
7407
  } else {
7058
7408
  process.stdout.write(bytes);
@@ -7130,10 +7480,10 @@ memory.command("save <text...>").description("add an entry").action(run(async (t
7130
7480
  memory.command("edit <index> <text...>").description("replace entry <index>").action(run(async (index, text) => writeMemory({ op: "update", index: memoryIndex(index), text: text.join(" ") })));
7131
7481
  memory.command("rm <index>").description("remove entry <index>").action(run(async (index) => writeMemory({ op: "remove", index: memoryIndex(index) })));
7132
7482
  memory.command("clear").description("remove every entry").action(run(async () => writeMemory({ op: "clear" })));
7133
- var context = program.command("context").description("search your conversations, memory and the workspace trace library");
7134
- 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(
7135
7485
  run(async (words, opts) => {
7136
- const page = await api("GET", `/context/search${searchQuery(words.join(" "), opts)}`);
7486
+ const page = await searchContext(words.join(" "), opts);
7137
7487
  console.log(opts.json ? JSON.stringify(page, null, 2) : renderContextPage(page));
7138
7488
  })
7139
7489
  );
@@ -7141,7 +7491,7 @@ context.command("read <ref>").description("read a search hit in full: <ref> is t
7141
7491
  run(async (ref, opts) => {
7142
7492
  const params = new URLSearchParams({ ref });
7143
7493
  if (opts.offset) params.set("offset", opts.offset);
7144
- const read = await api("GET", `/context/read?${params}`);
7494
+ const read = await contextRequest(`/context/read?${params}`);
7145
7495
  console.log(read.text);
7146
7496
  if (read.nextOffset !== null) console.error(`more: --offset ${read.nextOffset}`);
7147
7497
  })
@@ -7451,7 +7801,7 @@ mac.command("take-back <mount>").description("end a folder, browser window, scre
7451
7801
  })
7452
7802
  );
7453
7803
  var browserProfile = program.command("browser-profile").description("saved-login profiles: the sites your agents stay signed in to");
7454
- 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(
7455
7805
  run(async (opts) => {
7456
7806
  const profiles = await listBrowserProfiles();
7457
7807
  if (opts.json) {
@@ -7462,9 +7812,9 @@ browserProfile.command("list").description("your workspace's browser profiles: t
7462
7812
  for (const p of profiles) console.log(renderBrowserProfile(p));
7463
7813
  })
7464
7814
  );
7465
- browserProfile.command("sites <scope>").description("list the sites in a profile: workspace or private").option("--json", "print as JSON").action(
7466
- run(async (scope, opts) => {
7467
- 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();
7468
7818
  if (opts.json) {
7469
7819
  console.log(JSON.stringify(sites, null, 2));
7470
7820
  return;
@@ -7474,8 +7824,8 @@ browserProfile.command("sites <scope>").description("list the sites in a profile
7474
7824
  for (const s of sites) console.log(renderBrowserProfileSite(s));
7475
7825
  })
7476
7826
  );
7477
- 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(
7478
- 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) => {
7479
7829
  let raw;
7480
7830
  if (opts.file) {
7481
7831
  const { readFileSync: rf } = await import("fs");
@@ -7488,21 +7838,26 @@ browserProfile.command("import <scope>").description("import saved browser state
7488
7838
  });
7489
7839
  }
7490
7840
  const state = JSON.parse(raw);
7491
- const result = await importBrowserProfileState(scope, { state });
7841
+ const result = await importBrowserProfileState("mine", { state });
7492
7842
  console.log(`Imported ${result.sites.length} site${result.sites.length === 1 ? "" : "s"}: ${result.sites.join(", ")}`);
7493
7843
  })
7494
7844
  );
7495
- browserProfile.command("remove-site <scope> <site>").description("remove a site from a profile (the agent will need to sign in again)").action(
7496
- run(async (scope, site) => {
7497
- await removeBrowserProfileSite(scope, site);
7498
- 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));
7499
7855
  })
7500
7856
  );
7501
- browserProfile.command("move-site <scope> <site> <to>").description("move a site between profiles: <scope> is the current one, <to> is workspace or private").action(
7502
- run(async (scope, site, to) => {
7503
- if (to !== "workspace" && to !== "private") throw new ApiError(400, "invalid_request", "to must be workspace or private");
7504
- const result = await moveBrowserProfileSite(scope, site, to);
7505
- 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}`);
7506
7861
  })
7507
7862
  );
7508
7863
  var notifications = program.command("notifications").description("email and browser push notification preferences for tasks");
@@ -7562,21 +7917,21 @@ async function helpFetch(path) {
7562
7917
  return await res.json();
7563
7918
  }
7564
7919
  var help = program.command("help").description("search the AgentSky help center");
7565
- 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(
7566
7921
  run(async (words, opts) => {
7567
7922
  const q = words.join(" ");
7568
7923
  const topK = Math.min(Math.max(parseInt(opts.top, 10) || 5, 1), 20);
7569
- 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) })}`);
7570
7925
  if (opts.json) {
7571
- console.log(JSON.stringify(results, null, 2));
7926
+ console.log(JSON.stringify(hits, null, 2));
7572
7927
  return;
7573
7928
  }
7574
- if (!results.length) {
7929
+ if (!hits.length) {
7575
7930
  console.log("no results");
7576
7931
  return;
7577
7932
  }
7578
- for (const r of results) console.log(`${r.slug.padEnd(40)} [${r.category}] ${r.title}
7579
- ${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)}`);
7580
7935
  })
7581
7936
  );
7582
7937
  help.command("get <slug>").description("read a help article by its slug").option("--json", "print as JSON").action(
@@ -7591,6 +7946,85 @@ help.command("get <slug>").description("read a help article by its slug").option
7591
7946
  console.log(article.body);
7592
7947
  })
7593
7948
  );
7949
+ program.command("api <method> [path...]").description("call any AgentSky API operation (GET|POST|PUT|PATCH|DELETE <path>), or `sky api search <words\u2026>` to find one").option("-d, --data <json>", "JSON body: inline, @file, or - for stdin").option("-q, --query <key=value>", "query parameter (repeatable)", (v, acc) => [...acc, v], []).option("--workspace <slug>", "send the request in this workspace (X-Universe)").option("--idempotency-key <key>", "retry-safe key for a POST").option("-o, --output <file>", "save the body to a file (for downloads)").action(
7950
+ run(async (method, rest, opts) => {
7951
+ if (method.toLowerCase() === "search") return searchApi(rest);
7952
+ if (rest.length !== 1) throw new Error("usage: sky api <METHOD> <path> (one path), or sky api search <words\u2026>");
7953
+ return callApi(method, rest[0], opts);
7954
+ })
7955
+ );
7956
+ agent.command("edit <id>").description("change an agent's name, engine, model or thinking level (as its Library folder does)").option("--name <name>", "new name").option("--engine <agentType>", "engine, e.g. CLAUDE_CODE, CODEX, HERMES").option("--model <llm>", "model id, e.g. anthropic/claude-sonnet-4-5").option("--thinking <effort>", 'reasoning effort, or "default" to clear the pin').action(
7957
+ run(async (id, opts) => {
7958
+ const body = {
7959
+ ...opts.name ? { displayName: opts.name } : {},
7960
+ ...opts.engine ? { agentType: opts.engine } : {},
7961
+ ...opts.model ? { llm: opts.model } : {},
7962
+ ...opts.thinking ? { reasoningEffort: opts.thinking === "default" ? null : opts.thinking } : {}
7963
+ };
7964
+ if (!Object.keys(body).length) throw new Error("pass at least one of --name, --engine, --model, --thinking");
7965
+ const { agent: a } = await api("PATCH", `/agents/${encodeURIComponent(id)}`, body);
7966
+ console.log(`${a.id} \u2014 ${a.displayName} \xB7 ${a.agentType} \xB7 ${a.llm}${a.reasoningEffort ? ` \xB7 ${a.reasoningEffort}` : ""}`);
7967
+ })
7968
+ );
7969
+ skills.command("revisions <skillId>").description("a skill's saved versions, newest first").option("--json", "print as JSON").action(
7970
+ run(async (skillId, opts) => {
7971
+ const page = await api("GET", `/skills/${encodeURIComponent(skillId)}/revisions`);
7972
+ const rows = page.revisions ?? [];
7973
+ if (opts.json) return void console.log(JSON.stringify(rows, null, 2));
7974
+ if (!rows.length) return void console.log("no versions");
7975
+ for (const r of rows) console.log(`${r.id} ${r.createdAt ?? r.created_at ?? ""}`);
7976
+ })
7977
+ );
7978
+ skills.command("cat <skillId> <revisionId> <path>").description("print one file (e.g. SKILL.md) of a skill version").action(
7979
+ run(async (skillId, revisionId, path) => {
7980
+ const res = await apiRaw("GET", `/skills/${encodeURIComponent(skillId)}/revisions/${encodeURIComponent(revisionId)}/content?path=${encodeURIComponent(path)}`);
7981
+ if (!res.ok) throw new ApiError(res.status, "http_error", await res.text() || `${res.status}`);
7982
+ process.stdout.write(await res.text());
7983
+ })
7984
+ );
7985
+ skills.command("rm <skillId>").description("delete a skill from the workspace").action(
7986
+ run(async (skillId) => {
7987
+ await api("DELETE", `/skills/${encodeURIComponent(skillId)}`);
7988
+ console.log(`Deleted ${skillId}`);
7989
+ })
7990
+ );
7991
+ var notes = program.command("notes").description("memory notes in the Library's memory stores: list, read, write (every save keeps a version)");
7992
+ notes.command("stores").description("the workspace's memory stores").action(
7993
+ run(async () => {
7994
+ const { memory_stores: stores } = await api("GET", "/memory_stores?limit=100");
7995
+ if (!stores.length) return void console.log("no memory stores");
7996
+ for (const st of stores) console.log(`${st.id} ${st.name}`);
7997
+ })
7998
+ );
7999
+ notes.command("list <storeId> [folder]").description("the notes in a store, optionally under a folder ending in /").action(
8000
+ run(async (storeId, folder) => {
8001
+ const q = folder ? `&path_prefix=${encodeURIComponent(folder)}` : "";
8002
+ const { memories } = await api("GET", `/memory_stores/${encodeURIComponent(storeId)}/memories?limit=100${q}`);
8003
+ if (!memories.length) return void console.log("no notes");
8004
+ for (const n of memories) console.log(`${n.id} ${n.path}`);
8005
+ })
8006
+ );
8007
+ notes.command("read <storeId> <noteId>").description("print one note").action(
8008
+ run(async (storeId, noteId) => {
8009
+ const { memory: memory2 } = await api("GET", `/memory_stores/${encodeURIComponent(storeId)}/memories/${encodeURIComponent(noteId)}`);
8010
+ process.stdout.write(memory2.content.endsWith("\n") ? memory2.content : `${memory2.content}
8011
+ `);
8012
+ })
8013
+ );
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(
8015
+ run(async (storeId, file, opts) => {
8016
+ if (!opts.path === !opts.note) throw new Error("pass exactly one of --path (new note) or --note (update)");
8017
+ const content = readFileSync19(file, "utf8");
8018
+ const target = `/memory_stores/${encodeURIComponent(storeId)}/memories${opts.note ? `/${encodeURIComponent(opts.note)}` : ""}`;
8019
+ const { memory: memory2 } = await api("POST", target, { content, ...opts.path ? { path: opts.path } : {} });
8020
+ console.log(`Saved ${memory2.path} (${memory2.id})`);
8021
+ })
8022
+ );
8023
+ workspace.command("context").description("what every session in the workspace loads at start: AGENTS.md, MEMORY.md, indexes, your member page").action(
8024
+ run(async () => {
8025
+ console.log(JSON.stringify(await api("GET", "/workspace-context"), null, 2));
8026
+ })
8027
+ );
7594
8028
  var vault = program.command("vault").description("vaults: credentials a session is granted when it starts (values write-only)");
7595
8029
  vault.command("list").description("the workspace's vaults").action(
7596
8030
  run(async () => {
@@ -7680,7 +8114,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
7680
8114
  }));
7681
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)");
7682
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) => {
7683
- const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-ZRU624EY.js");
8117
+ const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-WLTD4C22.js");
7684
8118
  if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
7685
8119
  const scopes = parseContentScopes(opts.capture);
7686
8120
  const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
@@ -7715,7 +8149,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
7715
8149
  }));
7716
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 () => {
7717
8151
  try {
7718
- const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-ZRU624EY.js");
8152
+ const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-WLTD4C22.js");
7719
8153
  const chunks = [];
7720
8154
  for await (const chunk of process.stdin) chunks.push(chunk);
7721
8155
  const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
@@ -7728,14 +8162,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
7728
8162
  }
7729
8163
  });
7730
8164
  traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
7731
- const { readState, flush, grantSender } = await import("./trace-ZRU624EY.js");
8165
+ const { readState, flush, grantSender } = await import("./trace-WLTD4C22.js");
7732
8166
  const state = readState();
7733
8167
  if (!state) throw new Error("no trace is running here");
7734
8168
  const outcome = await flush(grantSender(state), { attempts: 4 });
7735
8169
  console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
7736
8170
  }));
7737
8171
  traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
7738
- const { readState, spoolStatus } = await import("./trace-ZRU624EY.js");
8172
+ const { readState, spoolStatus } = await import("./trace-WLTD4C22.js");
7739
8173
  const state = readState();
7740
8174
  if (!state) {
7741
8175
  console.log("No trace is running here.");
@@ -7745,7 +8179,7 @@ traceCmd.command("status").description("the running trace and its spool").action
7745
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`);
7746
8180
  }));
7747
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) => {
7748
- const { readState } = await import("./trace-ZRU624EY.js");
8182
+ const { readState } = await import("./trace-WLTD4C22.js");
7749
8183
  const ref = id ?? readState()?.trace_id;
7750
8184
  if (!ref) throw new Error("name a trace, or start one here");
7751
8185
  const bundle = await api("GET", `/traces/${ref}`);
@@ -7772,7 +8206,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
7772
8206
  for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
7773
8207
  }));
7774
8208
  traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
7775
- const { readState, record, flush, grantSender, clearState } = await import("./trace-ZRU624EY.js");
8209
+ const { readState, record, flush, grantSender, clearState } = await import("./trace-WLTD4C22.js");
7776
8210
  const state = readState();
7777
8211
  if (!state) throw new Error("no trace is running here");
7778
8212
  if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
@@ -7782,7 +8216,7 @@ traceCmd.command("stop").description("end the root span, send what is left, and
7782
8216
  console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
7783
8217
  }));
7784
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) => {
7785
- const lib = await import("./trace-library-QOQUAL5H.js");
8219
+ const lib = await import("./trace-library-YMAVLH4Y.js");
7786
8220
  if (opts.hook) {
7787
8221
  try {
7788
8222
  let raw = payload ?? "";
@@ -7841,10 +8275,10 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
7841
8275
  console.log(`Added ${imported.sessions} ${imported.agent} session${imported.sessions === 1 ? "" : "s"} from ${input}.`);
7842
8276
  }
7843
8277
  if (opts.installSchedule) {
7844
- const { join: join19 } = await import("path");
8278
+ const { join: join20 } = await import("path");
7845
8279
  const { homedir: homedir10 } = await import("os");
7846
8280
  const { execFileSync } = await import("child_process");
7847
- const log = join19(homedir10(), ".config", "sky", "trace-sync.log");
8281
+ const log = join20(homedir10(), ".config", "sky", "trace-sync.log");
7848
8282
  const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
7849
8283
  try {
7850
8284
  execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
@@ -7854,11 +8288,11 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
7854
8288
  console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
7855
8289
  }
7856
8290
  if (opts.status) {
7857
- const { readFileSync: readFileSync18 } = await import("fs");
7858
- const { join: join19 } = await import("path");
8291
+ const { readFileSync: readFileSync20 } = await import("fs");
8292
+ const { join: join20 } = await import("path");
7859
8293
  let state = { sessions: {} };
7860
8294
  try {
7861
- state = JSON.parse(readFileSync18(join19(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
8295
+ state = JSON.parse(readFileSync20(join20(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
7862
8296
  } catch {
7863
8297
  }
7864
8298
  const counts = /* @__PURE__ */ new Map();
@@ -7890,25 +8324,14 @@ traceCmd.command("sync [payload]").description("upload this machine's agent sess
7890
8324
  if (report.failed) process.exitCode = 1;
7891
8325
  })();
7892
8326
  });
7893
- traceCmd.command("search <query...>").description("search every session in a workspace's trace library (literal substring)").option("--workspace <slug>", "the workspace to search (default: the current one)").option("--limit <n>", "hits per page", (v) => Number.parseInt(v, 10), 20).option("--cursor <cursor>", "the next page of an earlier search").option("--json", "print the page as JSON").action(run(async (words, opts) => {
7894
- const { libraryRequest } = await import("./trace-library-QOQUAL5H.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) => {
7895
8328
  const workspace2 = opts.workspace ?? currentUniverse();
7896
8329
  if (!workspace2) throw new Error("name a workspace with --workspace");
7897
- const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
7898
- const page = await libraryRequest("GET", `/context/search?${params}`, workspace2);
7899
- if (opts.json) {
7900
- console.log(JSON.stringify(page, null, 2));
7901
- return;
7902
- }
7903
- if (!page.hits.length) console.log(page.partial ? `No hits, but the search was cut short (${page.coverage}).` : "No hits.");
7904
- for (const { hit } of page.hits) {
7905
- console.log(`${hit.occurredAt?.slice(0, 16).replace("T", " ") ?? " "} ${hit.title}`);
7906
- console.log(` ${hit.snippet.replace(/\s+/g, " ")}`);
7907
- }
7908
- 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));
7909
8332
  }));
7910
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) => {
7911
- const { libraryRequest } = await import("./trace-library-QOQUAL5H.js");
8334
+ const { libraryRequest } = await import("./trace-library-YMAVLH4Y.js");
7912
8335
  const workspace2 = opts.workspace ?? currentUniverse();
7913
8336
  if (!workspace2) throw new Error("name a workspace with --workspace");
7914
8337
  const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
@@ -8214,7 +8637,7 @@ session.command("code-review <id>").description("run the code-review assistant o
8214
8637
  }
8215
8638
  })
8216
8639
  );
8217
- session.command("snapshot <id>").description("read or revoke the public share link for a session").requiredOption("--thread <threadId>", "the thread/comparison that owns this session").option("--revoke", "revoke the snapshot link instead of reading it").option("--json", "print as JSON").action(
8640
+ session.command("snapshot <id>").usage("<id> --thread <threadId> [--revoke] [--json]").description("read or revoke the public share link for a session; --thread is required").requiredOption("--thread <threadId>", "(required) the thread/comparison that owns this session").option("--revoke", "revoke the snapshot link instead of reading it").option("--json", "print as JSON").action(
8218
8641
  run(async (id, opts) => {
8219
8642
  const path = `/sessions/${encodeURIComponent(id)}/snapshot?thread=${encodeURIComponent(opts.thread)}`;
8220
8643
  const result = await api(opts.revoke ? "DELETE" : "GET", path);