threadnote 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8598,6 +8598,7 @@ function formatPlanSection(title, lines) {
8598
8598
  var import_promises5 = require("node:fs/promises");
8599
8599
  var import_node_os4 = require("node:os");
8600
8600
  var import_node_path6 = require("node:path");
8601
+ var import_node_util = require("node:util");
8601
8602
  var TEAMS_FILE_VERSION = 1;
8602
8603
  var SHARED_SEGMENT = "shared";
8603
8604
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
@@ -8605,6 +8606,15 @@ var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
8605
8606
  var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
8606
8607
  var SHAREABLE_ROOT_FILES = ["README.md", "AGENTS.md", "CLAUDE.md", "SKILL.md", ".gitignore"];
8607
8608
  var ARTIFACT_INSTALL_METADATA_VERSION = 1;
8609
+ var BUNDLE_MANIFEST_VERSION = 1;
8610
+ var BUNDLE_MANIFEST_FILE = ".threadnote-bundle.json";
8611
+ var BUNDLE_INSTALL_METADATA_FILE = ".threadnote-bundle-install.json";
8612
+ var OV_SUMMARY_FILES = [".abstract.md", ".overview.md"];
8613
+ var BUNDLE_IGNORE_DIR_NAMES = [".git", "node_modules", "reviews", "repos"];
8614
+ var PACK_INDEX_SUFFIX = ".pack.md";
8615
+ var PACK_MANIFEST_SUFFIX = ".pack.json";
8616
+ var PACK_FILES_DIR = "files";
8617
+ var PACK_ROOT_TOKEN = "${THREADNOTE_PACK_ROOT}";
8608
8618
  var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
8609
8619
  var DEFAULT_GIT_REMOTE_NAME = "origin";
8610
8620
  var SCRUBBER_PATTERNS = [
@@ -8631,7 +8641,10 @@ var SCRUBBER_PATTERNS = [
8631
8641
  // so redaction collapses an entire path to a single placeholder rather than
8632
8642
  // leaving the subpath visible.
8633
8643
  { name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
8634
- { name: "linux home path", placeholder: "<local-path>", regex: /\b\/home\/[^\s)>"'`,]+/ }
8644
+ // No leading \b: it only matches when a word char precedes the slash, which
8645
+ // misses the common cases (after =, :, whitespace, or line start). Mirror the
8646
+ // macOS pattern so /home/... is caught in every position.
8647
+ { name: "linux home path", placeholder: "<local-path>", regex: /\/home\/[^\s)>"'`,]+/ }
8635
8648
  ];
8636
8649
  function applyScrubber(content, { redact }) {
8637
8650
  let cleaned = content;
@@ -9039,12 +9052,47 @@ async function runShareSyncQuiet(config, state, options) {
9039
9052
  return void 0;
9040
9053
  }
9041
9054
  async function stageShareableChanges(dryRun, git, worktree) {
9055
+ await removeOrphanPackIndexes(dryRun, git, worktree);
9042
9056
  const pathspecs = await existingShareablePathspecs(git, worktree);
9043
9057
  if (pathspecs.length === 0) {
9044
9058
  return;
9045
9059
  }
9046
9060
  await maybeRun(dryRun, git, ["-C", worktree, "add", "-A", "--", ...pathspecs], { allowFailure: true });
9047
9061
  }
9062
+ async function removeOrphanPackIndexes(dryRun, git, worktree) {
9063
+ const packsRoot = (0, import_node_path6.join)(worktree, SHAREABLE_ARTIFACT_DIR, "packs");
9064
+ if (!await isDirectory(packsRoot)) {
9065
+ return;
9066
+ }
9067
+ for (const agentEntry of await (0, import_promises5.readdir)(packsRoot, { withFileTypes: true })) {
9068
+ if (!agentEntry.isDirectory()) {
9069
+ continue;
9070
+ }
9071
+ const agentDir = (0, import_node_path6.join)(packsRoot, agentEntry.name);
9072
+ for (const nameEntry of await (0, import_promises5.readdir)(agentDir, { withFileTypes: true })) {
9073
+ if (!nameEntry.isDirectory()) {
9074
+ continue;
9075
+ }
9076
+ const packDir = (0, import_node_path6.join)(agentDir, nameEntry.name);
9077
+ const indexPath = (0, import_node_path6.join)(packDir, `${nameEntry.name}${PACK_INDEX_SUFFIX}`);
9078
+ const manifestPath = (0, import_node_path6.join)(packDir, `${nameEntry.name}${PACK_MANIFEST_SUFFIX}`);
9079
+ if (!await isFile(indexPath) || await isFile(manifestPath)) {
9080
+ continue;
9081
+ }
9082
+ const indexRelative = (0, import_node_path6.relative)(worktree, indexPath).split(import_node_path6.sep).join("/");
9083
+ const tracked = await runCommand(git, ["-C", worktree, "ls-files", "--", indexRelative], { allowFailure: true });
9084
+ if (tracked.exitCode === 0 && tracked.stdout.trim().length > 0) {
9085
+ continue;
9086
+ }
9087
+ console.warn(
9088
+ `${dryRun ? "Would remove" : "Removing"} incomplete shared pack (missing ${nameEntry.name}${PACK_MANIFEST_SUFFIX}): ${indexRelative}`
9089
+ );
9090
+ if (!dryRun) {
9091
+ await (0, import_promises5.rm)(packDir, { force: true, recursive: true });
9092
+ }
9093
+ }
9094
+ }
9095
+ }
9048
9096
  async function existingShareablePathspecs(git, worktree) {
9049
9097
  const rootFiles = await Promise.all(
9050
9098
  SHAREABLE_ROOT_FILES.map(
@@ -9071,7 +9119,8 @@ async function publishShareGitChange(worktree, relativePath, commitMessage, opti
9071
9119
  const verb = options.verb ?? "add";
9072
9120
  const git = await requiredExecutable("git");
9073
9121
  const messages = [];
9074
- const stageArgs = verb === "rm" ? ["-C", worktree, "rm", relativePath] : ["-C", worktree, "add", "--", relativePath];
9122
+ const paths = typeof relativePath === "string" ? [relativePath] : [...relativePath];
9123
+ const stageArgs = verb === "rm" ? ["-C", worktree, "rm", ...paths] : ["-C", worktree, "add", "--", ...paths];
9075
9124
  const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
9076
9125
  if (stageResult) {
9077
9126
  messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
@@ -9190,13 +9239,23 @@ async function runSharePublishArtifact(config, sourcePath, options) {
9190
9239
  }
9191
9240
  async function shareAgentArtifact(config, sourcePath, options) {
9192
9241
  const team = await resolveTeam(config, options.team);
9193
- const dryRun = options.dryRun === true;
9194
- const preview = options.preview === true;
9195
9242
  const resolvedSourcePath = expandPath(sourcePath);
9196
9243
  if (!await isRegularFileNoSymlink(resolvedSourcePath)) {
9197
9244
  throw new Error(`Agent artifact source is not a regular file: ${resolvedSourcePath}`);
9198
9245
  }
9199
9246
  const artifact = inferShareArtifact(resolvedSourcePath, options);
9247
+ if (artifact.kind === "skill") {
9248
+ const skillDir = (0, import_node_path6.dirname)(resolvedSourcePath);
9249
+ const members = await collectBundleMemberFiles(skillDir);
9250
+ if (members.length > 1) {
9251
+ return shareBundleArtifact(config, team, artifact, skillDir, members, options);
9252
+ }
9253
+ }
9254
+ return shareSingleArtifact(config, team, resolvedSourcePath, artifact, options);
9255
+ }
9256
+ async function shareSingleArtifact(config, team, resolvedSourcePath, artifact, options) {
9257
+ const dryRun = options.dryRun === true;
9258
+ const preview = options.preview === true;
9200
9259
  const rawContent = await (0, import_promises5.readFile)(resolvedSourcePath, "utf8");
9201
9260
  if (!rawContent.trim()) {
9202
9261
  throw new Error(`Refusing to share empty agent artifact: ${resolvedSourcePath}`);
@@ -9264,6 +9323,702 @@ async function shareAgentArtifact(config, sourcePath, options) {
9264
9323
  });
9265
9324
  return { artifact, gitMessages, messages, sourcePath: resolvedSourcePath, targetPath, targetUri };
9266
9325
  }
9326
+ async function shareBundleArtifact(config, team, artifact, skillDir, members, options) {
9327
+ const dryRun = options.dryRun === true;
9328
+ const preview = options.preview === true;
9329
+ const skillRootRelative = `${SHAREABLE_ARTIFACT_DIR}/skills/${artifact.agent}/${artifact.name}`;
9330
+ const skillRootTargetDir = (0, import_node_path6.join)(team.config.worktree, ...skillRootRelative.split("/"));
9331
+ const skillMdTargetPath = (0, import_node_path6.join)(skillRootTargetDir, "SKILL.md");
9332
+ const skillMdTargetUri = workfileToVikingUri(config, team.config, skillMdTargetPath);
9333
+ const skillRootTargetUri = parentUri(skillMdTargetUri);
9334
+ const skillMdSourcePath = (0, import_node_path6.join)(skillDir, "SKILL.md");
9335
+ const prepared = await Promise.all(
9336
+ members.map((member) => prepareBundleMember(config, team, member, skillRootTargetDir, options))
9337
+ );
9338
+ const skillMd = prepared.find((entry) => entry.relativePath === "SKILL.md");
9339
+ if (skillMd === void 0) {
9340
+ throw new Error(`Skill bundle ${artifact.agent}/${artifact.name} is missing SKILL.md.`);
9341
+ }
9342
+ if (!skillMd.binary && typeof skillMd.content === "string" && !skillMd.content.trim()) {
9343
+ throw new Error(`Refusing to share empty agent artifact: ${skillMdSourcePath}`);
9344
+ }
9345
+ const messages = [
9346
+ `${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} skill ${artifact.agent}/${artifact.name} bundle (${prepared.length} files)`,
9347
+ `Source: ${portablePath(skillDir)}`,
9348
+ `Destination: ${skillRootTargetUri}/`
9349
+ ];
9350
+ const blockers = prepared.filter((entry) => entry.blocker !== void 0);
9351
+ if (preview) {
9352
+ for (const entry of prepared) {
9353
+ const flags = entry.binary ? ["binary"] : [];
9354
+ for (const redaction of entry.redactions) {
9355
+ flags.push(`redact ${redaction.count}\xD7 ${redaction.name}`);
9356
+ }
9357
+ const note = entry.blocker !== void 0 ? ` BLOCKED: ${entry.blocker}` : "";
9358
+ messages.push(` ${entry.relativePath}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}${note}`);
9359
+ }
9360
+ return {
9361
+ artifact,
9362
+ gitMessages: [],
9363
+ messages,
9364
+ previewContent: skillMd.binary ? void 0 : skillMd.content,
9365
+ sourcePath: skillMdSourcePath,
9366
+ targetPath: skillMdTargetPath,
9367
+ targetUri: skillMdTargetUri
9368
+ };
9369
+ }
9370
+ if (blockers.length > 0) {
9371
+ throw new Error(
9372
+ `Refusing to share skill ${artifact.agent}/${artifact.name}: ${blockers.map((entry) => `${entry.relativePath} (${entry.blocker})`).join("; ")}. Strip the value, pass --redact for local paths, or --allow-binary for binary files.`
9373
+ );
9374
+ }
9375
+ for (const entry of prepared) {
9376
+ for (const redaction of entry.redactions) {
9377
+ messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} in ${entry.relativePath} before sharing.`);
9378
+ }
9379
+ }
9380
+ for (const entry of prepared) {
9381
+ const existing = await readFileBytesIfExists(entry.targetPath);
9382
+ if (existing !== void 0 && sha256(existing) !== entry.sha256 && options.force !== true) {
9383
+ throw new Error(
9384
+ `Shared artifact already exists with different content: ${portablePath(entry.targetPath)}. Pass --force to replace it.`
9385
+ );
9386
+ }
9387
+ }
9388
+ if (dryRun) {
9389
+ messages.push(`Would write ${prepared.length} files under ${portablePath(skillRootTargetDir)}`);
9390
+ return {
9391
+ artifact,
9392
+ gitMessages: [],
9393
+ messages,
9394
+ sourcePath: skillMdSourcePath,
9395
+ targetPath: skillMdTargetPath,
9396
+ targetUri: skillMdTargetUri
9397
+ };
9398
+ }
9399
+ const ov = await openVikingCliForMode(dryRun);
9400
+ const markdownMembers = orderSkillMdFirst(prepared.filter((entry) => entry.relativePath.endsWith(".md")));
9401
+ const otherMembers = prepared.filter((entry) => !entry.relativePath.endsWith(".md"));
9402
+ for (const entry of markdownMembers) {
9403
+ const ovHasResource = await vikingResourceExists(ov, config, entry.targetUri);
9404
+ await ensureSharedDirectoryChain(config, ov, entry.targetUri, dryRun, { quiet: true });
9405
+ await writeMemoryFile(
9406
+ config,
9407
+ ov,
9408
+ entry.targetUri,
9409
+ entry.content,
9410
+ ovHasResource ? "replace" : "create",
9411
+ dryRun,
9412
+ { quiet: true }
9413
+ );
9414
+ }
9415
+ await ensureDirectory(skillRootTargetDir, false);
9416
+ for (const entry of otherMembers) {
9417
+ await ensureDirectory((0, import_node_path6.dirname)(entry.targetPath), false);
9418
+ await (0, import_promises5.writeFile)(entry.targetPath, entry.content, entry.binary ? { mode: 384 } : { encoding: "utf8", mode: 384 });
9419
+ }
9420
+ await (0, import_promises5.writeFile)((0, import_node_path6.join)(skillRootTargetDir, BUNDLE_MANIFEST_FILE), buildBundleManifest(artifact, prepared), {
9421
+ encoding: "utf8",
9422
+ mode: 384
9423
+ });
9424
+ const stagedPaths = [
9425
+ ...prepared.map((entry) => `${skillRootRelative}/${entry.relativePath}`),
9426
+ `${skillRootRelative}/${BUNDLE_MANIFEST_FILE}`
9427
+ ];
9428
+ const message = options.message ?? `share: publish skill ${artifact.agent}/${artifact.name} (${prepared.length} files)`;
9429
+ const gitMessages = await publishShareGitChange(team.config.worktree, stagedPaths, message, {
9430
+ dryRun,
9431
+ push: options.push
9432
+ });
9433
+ return {
9434
+ artifact,
9435
+ gitMessages,
9436
+ messages,
9437
+ sourcePath: skillMdSourcePath,
9438
+ targetPath: skillMdTargetPath,
9439
+ targetUri: skillMdTargetUri
9440
+ };
9441
+ }
9442
+ async function prepareBundleMember(config, team, member, skillRootTargetDir, options) {
9443
+ const buffer = await (0, import_promises5.readFile)(member.absolutePath);
9444
+ const targetPath = (0, import_node_path6.join)(skillRootTargetDir, ...member.relativePath.split("/"));
9445
+ const targetUri = workfileToVikingUri(config, team.config, targetPath);
9446
+ if (isProbablyBinary(buffer)) {
9447
+ const credential = detectBinaryCredential(buffer);
9448
+ const blocker = credential !== void 0 ? `possible ${credential} embedded in binary file` : options.allowBinary === true ? void 0 : "binary file (pass --allow-binary to include it unscanned)";
9449
+ return {
9450
+ binary: true,
9451
+ blocker,
9452
+ content: buffer,
9453
+ redactions: [],
9454
+ relativePath: member.relativePath,
9455
+ sha256: sha256(buffer),
9456
+ targetPath,
9457
+ targetUri
9458
+ };
9459
+ }
9460
+ const scrub = applyScrubber(buffer.toString("utf8"), { redact: options.redact === true });
9461
+ return {
9462
+ binary: false,
9463
+ blocker: scrub.blocker,
9464
+ content: scrub.cleaned,
9465
+ redactions: scrub.redactions,
9466
+ relativePath: member.relativePath,
9467
+ sha256: sha256(scrub.cleaned),
9468
+ targetPath,
9469
+ targetUri
9470
+ };
9471
+ }
9472
+ function orderSkillMdFirst(entries) {
9473
+ return [...entries].sort((a, b) => {
9474
+ if (a.relativePath === "SKILL.md") {
9475
+ return -1;
9476
+ }
9477
+ if (b.relativePath === "SKILL.md") {
9478
+ return 1;
9479
+ }
9480
+ return compareStrings(a.relativePath, b.relativePath);
9481
+ });
9482
+ }
9483
+ function buildBundleManifest(artifact, prepared) {
9484
+ const manifest = {
9485
+ artifact,
9486
+ members: prepared.map((entry) => ({ binary: entry.binary, path: entry.relativePath, sha256: entry.sha256 })).sort((a, b) => compareStrings(a.path, b.path)),
9487
+ version: BUNDLE_MANIFEST_VERSION
9488
+ };
9489
+ return `${JSON.stringify(manifest, void 0, 2)}
9490
+ `;
9491
+ }
9492
+ async function collectBundleMemberFiles(skillDir) {
9493
+ const out = [];
9494
+ async function visit(dir) {
9495
+ const entries = await (0, import_promises5.readdir)(dir, { withFileTypes: true });
9496
+ for (const entry of entries) {
9497
+ if (entry.isSymbolicLink()) {
9498
+ continue;
9499
+ }
9500
+ const full = (0, import_node_path6.join)(dir, entry.name);
9501
+ if (entry.isDirectory()) {
9502
+ if (!BUNDLE_IGNORE_DIR_NAMES.includes(entry.name)) {
9503
+ await visit(full);
9504
+ }
9505
+ continue;
9506
+ }
9507
+ if (!entry.isFile() || isIgnoredBundleFile(entry.name)) {
9508
+ continue;
9509
+ }
9510
+ out.push({ absolutePath: full, relativePath: (0, import_node_path6.relative)(skillDir, full).split(import_node_path6.sep).join("/") });
9511
+ }
9512
+ }
9513
+ await visit(skillDir);
9514
+ return out.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
9515
+ }
9516
+ function isIgnoredBundleFile(name) {
9517
+ if (name === ".DS_Store" || name === BUNDLE_MANIFEST_FILE || name === BUNDLE_INSTALL_METADATA_FILE) {
9518
+ return true;
9519
+ }
9520
+ if (OV_SUMMARY_FILES.includes(name)) {
9521
+ return true;
9522
+ }
9523
+ return name.endsWith(".log") || name.endsWith(".threadnote-install.json");
9524
+ }
9525
+ function isProbablyBinary(buffer) {
9526
+ if (buffer.includes(0)) {
9527
+ return true;
9528
+ }
9529
+ try {
9530
+ new import_node_util.TextDecoder("utf-8", { fatal: true }).decode(buffer);
9531
+ return false;
9532
+ } catch (_err) {
9533
+ return true;
9534
+ }
9535
+ }
9536
+ function detectBinaryCredential(buffer) {
9537
+ const latin1 = buffer.toString("latin1");
9538
+ for (const pattern of SCRUBBER_PATTERNS) {
9539
+ if (pattern.placeholder === void 0 && pattern.regex.test(latin1)) {
9540
+ return pattern.name;
9541
+ }
9542
+ }
9543
+ return void 0;
9544
+ }
9545
+ function detectBinaryLocalPath(buffer, rewriteRoots) {
9546
+ const latin1 = buffer.toString("latin1");
9547
+ for (const root of rewriteRoots) {
9548
+ if (root.length > 0 && latin1.includes(root)) {
9549
+ return "machine-local path";
9550
+ }
9551
+ }
9552
+ for (const pattern of SCRUBBER_PATTERNS) {
9553
+ if (pattern.placeholder !== void 0 && pattern.regex.test(latin1)) {
9554
+ return pattern.name;
9555
+ }
9556
+ }
9557
+ return void 0;
9558
+ }
9559
+ async function readFileBytesIfExists(path) {
9560
+ try {
9561
+ return await (0, import_promises5.readFile)(path);
9562
+ } catch (_err) {
9563
+ return void 0;
9564
+ }
9565
+ }
9566
+ function isBundleArtifact(artifact) {
9567
+ if (artifact.artifact.kind === "pack") {
9568
+ return true;
9569
+ }
9570
+ return artifact.members !== void 0 && artifact.members.length > 1;
9571
+ }
9572
+ function compareStrings(a, b) {
9573
+ return a < b ? -1 : a > b ? 1 : 0;
9574
+ }
9575
+ async function runSharePublishBundle(config, manifestPath, options) {
9576
+ const result = await shareBundlePack(config, manifestPath, options);
9577
+ printShareArtifactResult(result, options.preview === true);
9578
+ }
9579
+ function parsePackManifest(raw, manifestPath) {
9580
+ const parsed = parseJsonConfigObject(raw);
9581
+ if (parsed === void 0) {
9582
+ throw new Error(`Invalid pack manifest (not a JSON object): ${manifestPath}`);
9583
+ }
9584
+ const name = parsed.name;
9585
+ if (typeof name !== "string" || name.trim().length === 0) {
9586
+ throw new Error(`Pack manifest must set a non-empty "name": ${manifestPath}`);
9587
+ }
9588
+ const agent = parsed.agent;
9589
+ if (agent !== "codex" && agent !== "claude") {
9590
+ throw new Error(`Pack manifest "agent" must be "codex" or "claude": ${manifestPath}`);
9591
+ }
9592
+ const stringArray2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
9593
+ const skills = stringArray2(parsed.skills);
9594
+ if (skills.length === 0) {
9595
+ throw new Error(`Pack manifest must list at least one skill in "skills": ${manifestPath}`);
9596
+ }
9597
+ const depsValue = parsed.deps ?? {};
9598
+ const pathRewrites = Array.isArray(parsed.pathRewrites) ? parsed.pathRewrites.map((entry) => typeof entry === "string" ? entry : entry?.from).filter((item) => typeof item === "string").map((rewrite) => rewrite.replace(/\/+$/, "")) : [];
9599
+ for (const rewrite of pathRewrites) {
9600
+ if (!(0, import_node_path6.isAbsolute)(rewrite) || rewrite.split("/").filter(Boolean).length < 2) {
9601
+ throw new Error(
9602
+ `Pack manifest pathRewrites entry must be an absolute repo-root path (got "${rewrite}"): ${manifestPath}`
9603
+ );
9604
+ }
9605
+ }
9606
+ return {
9607
+ agent,
9608
+ deps: {
9609
+ cli: stringArray2(depsValue.cli),
9610
+ mcp: stringArray2(depsValue.mcp),
9611
+ os: stringArray2(depsValue.os),
9612
+ runtime: stringArray2(depsValue.runtime)
9613
+ },
9614
+ description: typeof parsed.description === "string" ? parsed.description : void 0,
9615
+ include: stringArray2(parsed.include),
9616
+ name,
9617
+ pathRewrites,
9618
+ skills
9619
+ };
9620
+ }
9621
+ async function collectPackMembers(manifestDir, manifest) {
9622
+ const members = /* @__PURE__ */ new Map();
9623
+ const addEntry = async (entry) => {
9624
+ const normalized = entry.split("/").filter(Boolean).join("/");
9625
+ if (normalized.split("/").includes("..")) {
9626
+ throw new Error(`Pack manifest entries must stay within the pack root (got "${entry}").`);
9627
+ }
9628
+ const absolute = (0, import_node_path6.join)(manifestDir, ...normalized.split("/"));
9629
+ if (absolute !== manifestDir && !absolute.startsWith(manifestDir + import_node_path6.sep)) {
9630
+ throw new Error(`Pack manifest entry escapes the pack root: ${entry}`);
9631
+ }
9632
+ if (await isDirectory(absolute)) {
9633
+ for (const member of await collectBundleMemberFiles(absolute)) {
9634
+ const relativePath = `${normalized}/${member.relativePath}`;
9635
+ members.set(relativePath, { absolutePath: member.absolutePath, relativePath });
9636
+ }
9637
+ return;
9638
+ }
9639
+ if (await isRegularFileNoSymlink(absolute)) {
9640
+ members.set(normalized, { absolutePath: absolute, relativePath: normalized });
9641
+ return;
9642
+ }
9643
+ throw new Error(`Pack manifest references a missing path: ${entry}`);
9644
+ };
9645
+ for (const skill of manifest.skills) {
9646
+ const skillRel = skill.replace(/\/SKILL\.md$/i, "");
9647
+ const skillDir = (0, import_node_path6.join)(manifestDir, ...skillRel.split("/"));
9648
+ if (!await isFile((0, import_node_path6.join)(skillDir, "SKILL.md"))) {
9649
+ throw new Error(`Pack skill "${skill}" must be a directory containing SKILL.md.`);
9650
+ }
9651
+ await addEntry(skillRel);
9652
+ }
9653
+ for (const include of manifest.include) {
9654
+ await addEntry(include);
9655
+ }
9656
+ return [...members.values()].sort((a, b) => compareStrings(a.relativePath, b.relativePath));
9657
+ }
9658
+ function escapeRegExp2(value) {
9659
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9660
+ }
9661
+ function tokenizePackPaths(text, rewriteRoots) {
9662
+ let out = text;
9663
+ for (const root of rewriteRoots) {
9664
+ if (root.length > 0) {
9665
+ out = out.replace(
9666
+ new RegExp(`(?<![A-Za-z0-9/._~-])${escapeRegExp2(root)}(?=[/\\\\]|["'\`\\s),>:\\]};=]|$)`, "g"),
9667
+ PACK_ROOT_TOKEN
9668
+ );
9669
+ }
9670
+ }
9671
+ return out;
9672
+ }
9673
+ function residualRewriteRoot(content, rewriteRoots) {
9674
+ return rewriteRoots.find((root) => root.length > 0 && content.includes(root));
9675
+ }
9676
+ var PORTABLE_PATH_PREFIXES = [
9677
+ "/usr/",
9678
+ "/bin/",
9679
+ "/sbin/",
9680
+ "/lib/",
9681
+ "/lib64/",
9682
+ "/etc/",
9683
+ "/opt/homebrew/",
9684
+ "/tmp/",
9685
+ "/var/",
9686
+ "/private/var/",
9687
+ "/dev/",
9688
+ "/proc/",
9689
+ "/run/",
9690
+ "/sys/",
9691
+ "/Library/",
9692
+ "/System/",
9693
+ "/Applications/"
9694
+ ];
9695
+ function unportableAbsolutePaths(content) {
9696
+ const scan = content.split(`${PACK_ROOT_TOKEN}/`).join("").split(PACK_ROOT_TOKEN).join("");
9697
+ const found = /* @__PURE__ */ new Set();
9698
+ for (const path of scan.match(/(?<![A-Za-z0-9._~$-])\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+/g) ?? []) {
9699
+ if (!PORTABLE_PATH_PREFIXES.some((prefix) => path.startsWith(prefix))) {
9700
+ found.add(path);
9701
+ }
9702
+ }
9703
+ for (const path of scan.match(/[A-Za-z]:\\[^\s"']+/g) ?? []) {
9704
+ found.add(path);
9705
+ }
9706
+ return [...found].sort((a, b) => compareStrings(a, b));
9707
+ }
9708
+ async function preparePackMember(config, team, member, filesTargetDir, rewriteRoots, options) {
9709
+ const buffer = await (0, import_promises5.readFile)(member.absolutePath);
9710
+ const targetPath = (0, import_node_path6.join)(filesTargetDir, ...member.relativePath.split("/"));
9711
+ const targetUri = workfileToVikingUri(config, team.config, targetPath);
9712
+ if (isProbablyBinary(buffer)) {
9713
+ const credential = detectBinaryCredential(buffer);
9714
+ const localPath = credential === void 0 ? detectBinaryLocalPath(buffer, rewriteRoots) : void 0;
9715
+ let blocker2;
9716
+ if (credential !== void 0) {
9717
+ blocker2 = `possible ${credential} embedded in binary file`;
9718
+ } else if (options.allowBinary !== true) {
9719
+ blocker2 = "binary file (pass --allow-binary to include it unscanned)";
9720
+ } else if (localPath !== void 0) {
9721
+ blocker2 = `possible ${localPath} embedded in binary file (cannot be rewritten)`;
9722
+ }
9723
+ return {
9724
+ binary: true,
9725
+ blocker: blocker2,
9726
+ content: buffer,
9727
+ redactions: [],
9728
+ relativePath: member.relativePath,
9729
+ sha256: sha256(buffer),
9730
+ targetPath,
9731
+ targetUri
9732
+ };
9733
+ }
9734
+ const text = buffer.toString("utf8");
9735
+ if (text.includes(PACK_ROOT_TOKEN)) {
9736
+ return {
9737
+ binary: false,
9738
+ blocker: `contains the reserved ${PACK_ROOT_TOKEN} token`,
9739
+ content: text,
9740
+ redactions: [],
9741
+ relativePath: member.relativePath,
9742
+ sha256: sha256(text),
9743
+ targetPath,
9744
+ targetUri
9745
+ };
9746
+ }
9747
+ const tokenized = tokenizePackPaths(text, rewriteRoots);
9748
+ const isMarkdown = member.relativePath.toLowerCase().endsWith(".md");
9749
+ const scrub = applyScrubber(tokenized, { redact: isMarkdown && options.redact === true });
9750
+ const residual = residualRewriteRoot(scrub.cleaned, rewriteRoots);
9751
+ const blocker = scrub.blocker ?? (residual !== void 0 ? `machine-local path "${residual}" not rewritten` : void 0);
9752
+ return {
9753
+ binary: false,
9754
+ blocker,
9755
+ content: scrub.cleaned,
9756
+ redactions: scrub.redactions,
9757
+ relativePath: member.relativePath,
9758
+ sha256: sha256(scrub.cleaned),
9759
+ targetPath,
9760
+ targetUri
9761
+ };
9762
+ }
9763
+ function buildPackIndex(artifact, manifest, skillNames, memberCount) {
9764
+ const lines = [
9765
+ "---",
9766
+ `name: ${artifact.name}`,
9767
+ `agent: ${artifact.agent}`,
9768
+ "kind: pack",
9769
+ `skills: [${skillNames.join(", ")}]`,
9770
+ "---",
9771
+ "",
9772
+ `# ${artifact.name} (skill pack)`,
9773
+ "",
9774
+ manifest.description ?? `A Threadnote skill pack bundling ${skillNames.length} skill(s) and their shared support files (${memberCount} files total).`,
9775
+ "",
9776
+ "## Skills",
9777
+ ...skillNames.map((skill) => `- ${skill}`),
9778
+ "",
9779
+ "## Requirements",
9780
+ "Threadnote installs files only. Ensure these exist on the target machine before running:"
9781
+ ];
9782
+ if (manifest.deps.runtime.length > 0) {
9783
+ lines.push(`- runtime: ${manifest.deps.runtime.join(", ")}`);
9784
+ }
9785
+ if (manifest.deps.cli.length > 0) {
9786
+ lines.push(`- CLI: ${manifest.deps.cli.join(", ")}`);
9787
+ }
9788
+ if (manifest.deps.os.length > 0) {
9789
+ lines.push(`- OS: ${manifest.deps.os.join(", ")}`);
9790
+ }
9791
+ if (manifest.deps.mcp.length > 0) {
9792
+ lines.push(`- MCP (configure separately): ${manifest.deps.mcp.join(", ")}`);
9793
+ }
9794
+ lines.push("", `Install: threadnote share install-artifacts --kind pack --name ${artifact.name} --apply`, "");
9795
+ return lines.join("\n");
9796
+ }
9797
+ function buildPackManifestJson(artifact, manifest, prepared) {
9798
+ const data = {
9799
+ artifact,
9800
+ deps: manifest.deps,
9801
+ members: prepared.map((entry) => ({ binary: entry.binary, path: entry.relativePath, sha256: entry.sha256 })).sort((a, b) => compareStrings(a.path, b.path)),
9802
+ version: BUNDLE_MANIFEST_VERSION
9803
+ };
9804
+ return `${JSON.stringify(data, void 0, 2)}
9805
+ `;
9806
+ }
9807
+ function packSkillName(skillEntry) {
9808
+ const trimmed = skillEntry.replace(/\/SKILL\.md$/i, "");
9809
+ return (0, import_node_path6.basename)(trimmed);
9810
+ }
9811
+ async function shareBundlePack(config, manifestPath, options) {
9812
+ const team = await resolveTeam(config, options.team);
9813
+ const dryRun = options.dryRun === true;
9814
+ const preview = options.preview === true;
9815
+ const resolvedManifest = expandPath(manifestPath);
9816
+ if (!await isRegularFileNoSymlink(resolvedManifest)) {
9817
+ throw new Error(`Pack manifest is not a regular file: ${resolvedManifest}`);
9818
+ }
9819
+ const manifest = parsePackManifest(await (0, import_promises5.readFile)(resolvedManifest, "utf8"), resolvedManifest);
9820
+ const manifestDir = (0, import_node_path6.dirname)(resolvedManifest);
9821
+ const artifact = { agent: manifest.agent, kind: "pack", name: uriSegment(manifest.name) };
9822
+ const skillNames = manifest.skills.map(packSkillName);
9823
+ const members = await collectPackMembers(manifestDir, manifest);
9824
+ const autoRoots = manifestDir.split("/").filter(Boolean).length >= 2 ? [manifestDir] : [];
9825
+ const rewriteRoots = [.../* @__PURE__ */ new Set([...autoRoots, ...manifest.pathRewrites])].sort((a, b) => b.length - a.length);
9826
+ const packRootRelative = `${SHAREABLE_ARTIFACT_DIR}/packs/${artifact.agent}/${artifact.name}`;
9827
+ const filesRelative = `${packRootRelative}/${PACK_FILES_DIR}`;
9828
+ const indexRelative = `${packRootRelative}/${artifact.name}${PACK_INDEX_SUFFIX}`;
9829
+ const manifestRelative = `${packRootRelative}/${artifact.name}${PACK_MANIFEST_SUFFIX}`;
9830
+ const filesTargetDir = (0, import_node_path6.join)(team.config.worktree, ...filesRelative.split("/"));
9831
+ const packRootTargetDir = (0, import_node_path6.join)(team.config.worktree, ...packRootRelative.split("/"));
9832
+ const indexTargetPath = (0, import_node_path6.join)(team.config.worktree, ...indexRelative.split("/"));
9833
+ const indexTargetUri = workfileToVikingUri(config, team.config, indexTargetPath);
9834
+ const prepared = await Promise.all(
9835
+ members.map((member) => preparePackMember(config, team, member, filesTargetDir, rewriteRoots, options))
9836
+ );
9837
+ const indexContent = tokenizePackPaths(buildPackIndex(artifact, manifest, skillNames, prepared.length), rewriteRoots);
9838
+ const indexScrub = applyScrubber(indexContent, { redact: options.redact === true });
9839
+ const messages = [
9840
+ `${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} pack ${artifact.agent}/${artifact.name} (${prepared.length} files, ${skillNames.length} skills)`,
9841
+ `Source: ${portablePath(manifestDir)}`,
9842
+ `Destination: ${indexTargetUri}`
9843
+ ];
9844
+ const blockers = prepared.filter((entry) => entry.blocker !== void 0);
9845
+ if (preview) {
9846
+ for (const entry of prepared) {
9847
+ const flags = entry.binary ? ["binary"] : [];
9848
+ for (const redaction of entry.redactions) {
9849
+ flags.push(`redact ${redaction.count}\xD7 ${redaction.name}`);
9850
+ }
9851
+ const note = entry.blocker !== void 0 ? ` BLOCKED: ${entry.blocker}` : "";
9852
+ messages.push(` ${entry.relativePath}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}${note}`);
9853
+ }
9854
+ return {
9855
+ artifact,
9856
+ gitMessages: [],
9857
+ messages,
9858
+ previewContent: indexScrub.cleaned,
9859
+ sourcePath: resolvedManifest,
9860
+ targetPath: indexTargetPath,
9861
+ targetUri: indexTargetUri
9862
+ };
9863
+ }
9864
+ const indexResidual = residualRewriteRoot(indexScrub.cleaned, rewriteRoots);
9865
+ if (indexScrub.blocker !== void 0 || indexResidual !== void 0) {
9866
+ throw new Error(
9867
+ `Refusing to share pack ${artifact.agent}/${artifact.name}: index ${indexScrub.blocker ?? `machine-local path "${indexResidual}" not rewritten`}.`
9868
+ );
9869
+ }
9870
+ if (blockers.length > 0) {
9871
+ throw new Error(
9872
+ `Refusing to share pack ${artifact.agent}/${artifact.name}: ${blockers.map((entry) => `${entry.relativePath} (${entry.blocker})`).join("; ")}. Strip the value, pass --redact for local paths, or --allow-binary for binary files.`
9873
+ );
9874
+ }
9875
+ const packJson = applyScrubber(tokenizePackPaths(buildPackManifestJson(artifact, manifest, prepared), rewriteRoots), {
9876
+ redact: options.redact === true
9877
+ });
9878
+ const packJsonResidual = residualRewriteRoot(packJson.cleaned, rewriteRoots);
9879
+ if (packJson.blocker !== void 0 || packJsonResidual !== void 0) {
9880
+ throw new Error(
9881
+ `Refusing to share pack ${artifact.agent}/${artifact.name}: manifest ${packJson.blocker ?? `machine-local path "${packJsonResidual}" not rewritten`}.`
9882
+ );
9883
+ }
9884
+ for (const entry of prepared) {
9885
+ for (const redaction of entry.redactions) {
9886
+ messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} in ${entry.relativePath} before sharing.`);
9887
+ }
9888
+ }
9889
+ const unportable = /* @__PURE__ */ new Set();
9890
+ for (const entry of prepared) {
9891
+ if (!entry.binary && typeof entry.content === "string") {
9892
+ for (const path of unportableAbsolutePaths(entry.content)) {
9893
+ unportable.add(`${entry.relativePath}: ${path}`);
9894
+ }
9895
+ }
9896
+ }
9897
+ for (const path of unportableAbsolutePaths(indexScrub.cleaned)) {
9898
+ unportable.add(`${artifact.name}${PACK_INDEX_SUFFIX}: ${path}`);
9899
+ }
9900
+ for (const path of unportableAbsolutePaths(packJson.cleaned)) {
9901
+ unportable.add(`${artifact.name}${PACK_MANIFEST_SUFFIX}: ${path}`);
9902
+ }
9903
+ if (unportable.size > 0) {
9904
+ messages.push(
9905
+ `Warning: possible machine-local absolute path(s) that will not resolve on a teammate's machine (declare in pathRewrites or strip if not portable): ${[...unportable].join("; ")}`
9906
+ );
9907
+ }
9908
+ for (const entry of prepared) {
9909
+ const existing = await readFileBytesIfExists(entry.targetPath);
9910
+ if (existing !== void 0 && sha256(existing) !== entry.sha256 && options.force !== true) {
9911
+ throw new Error(
9912
+ `Shared pack file already exists with different content: ${portablePath(entry.targetPath)}. Pass --force to replace it.`
9913
+ );
9914
+ }
9915
+ }
9916
+ if (dryRun) {
9917
+ messages.push(`Would write ${prepared.length} files under ${portablePath(packRootTargetDir)}`);
9918
+ return {
9919
+ artifact,
9920
+ gitMessages: [],
9921
+ messages,
9922
+ sourcePath: resolvedManifest,
9923
+ targetPath: indexTargetPath,
9924
+ targetUri: indexTargetUri
9925
+ };
9926
+ }
9927
+ const ov = await openVikingCliForMode(dryRun);
9928
+ const rollbacks = [];
9929
+ const manifestTargetPath = (0, import_node_path6.join)(team.config.worktree, ...manifestRelative.split("/"));
9930
+ try {
9931
+ const writeMarkdownMember = async (uri, content, worktreePath) => {
9932
+ const priorBytes = await readFileBytesIfExists(worktreePath);
9933
+ const hadResource = await vikingResourceExists(ov, config, uri);
9934
+ await ensureSharedDirectoryChain(config, ov, uri, dryRun, { quiet: true });
9935
+ await writeMemoryFile(config, ov, uri, content, hadResource ? "replace" : "create", dryRun, { quiet: true });
9936
+ rollbacks.push(async () => {
9937
+ if (priorBytes !== void 0) {
9938
+ await writeMemoryFile(config, ov, uri, priorBytes.toString("utf8"), "replace", false, { quiet: true });
9939
+ } else if (await vikingResourceExists(ov, config, uri)) {
9940
+ await removeMemoryUri(config, ov, uri, false, { quiet: true });
9941
+ }
9942
+ });
9943
+ };
9944
+ await writeMarkdownMember(indexTargetUri, indexScrub.cleaned, indexTargetPath);
9945
+ for (const entry of prepared.filter((member) => member.relativePath.endsWith(".md"))) {
9946
+ await writeMarkdownMember(entry.targetUri, entry.content, entry.targetPath);
9947
+ }
9948
+ await ensureDirectory(filesTargetDir, false);
9949
+ for (const entry of prepared.filter((member) => !member.relativePath.endsWith(".md"))) {
9950
+ const priorBytes = await readFileBytesIfExists(entry.targetPath);
9951
+ await ensureDirectory((0, import_node_path6.dirname)(entry.targetPath), false);
9952
+ await (0, import_promises5.writeFile)(entry.targetPath, entry.content, entry.binary ? { mode: 384 } : { encoding: "utf8", mode: 384 });
9953
+ rollbacks.push(async () => {
9954
+ if (priorBytes !== void 0) {
9955
+ await (0, import_promises5.writeFile)(entry.targetPath, priorBytes, { mode: 384 });
9956
+ } else {
9957
+ await (0, import_promises5.rm)(entry.targetPath, { force: true });
9958
+ }
9959
+ });
9960
+ }
9961
+ await ensureDirectory(packRootTargetDir, false);
9962
+ const priorManifest = await readFileBytesIfExists(manifestTargetPath);
9963
+ await (0, import_promises5.writeFile)(manifestTargetPath, packJson.cleaned, { encoding: "utf8", mode: 384 });
9964
+ rollbacks.push(async () => {
9965
+ if (priorManifest !== void 0) {
9966
+ await (0, import_promises5.writeFile)(manifestTargetPath, priorManifest, { mode: 384 });
9967
+ } else {
9968
+ await (0, import_promises5.rm)(manifestTargetPath, { force: true });
9969
+ }
9970
+ });
9971
+ const currentFiles = new Set(prepared.map((entry) => `${filesRelative}/${entry.relativePath}`));
9972
+ const git = await requiredExecutable("git");
9973
+ const tracked = await runCommand(git, ["-C", team.config.worktree, "ls-files", "--", filesRelative], {
9974
+ allowFailure: true
9975
+ });
9976
+ const stalePaths = tracked.exitCode === 0 ? tracked.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !currentFiles.has(line)) : [];
9977
+ for (const stale of stalePaths) {
9978
+ await runCommand(git, ["-C", team.config.worktree, "rm", "-f", "--ignore-unmatch", "--", stale], {
9979
+ allowFailure: true
9980
+ });
9981
+ if (stale.endsWith(".md")) {
9982
+ const staleUri = workfileToVikingUri(config, team.config, (0, import_node_path6.join)(team.config.worktree, ...stale.split("/")));
9983
+ try {
9984
+ if (await vikingResourceExists(ov, config, staleUri)) {
9985
+ await removeMemoryUri(config, ov, staleUri, dryRun, { quiet: true });
9986
+ }
9987
+ } catch (pruneErr) {
9988
+ messages.push(
9989
+ `Warning: could not remove stale OpenViking resource ${staleUri}: ${pruneErr instanceof Error ? pruneErr.message : String(pruneErr)}`
9990
+ );
9991
+ }
9992
+ }
9993
+ }
9994
+ } catch (publishErr) {
9995
+ for (const undo of rollbacks.reverse()) {
9996
+ try {
9997
+ await undo();
9998
+ } catch (_cleanupErr) {
9999
+ }
10000
+ }
10001
+ throw publishErr;
10002
+ }
10003
+ const stagedPaths = [
10004
+ indexRelative,
10005
+ manifestRelative,
10006
+ ...prepared.map((entry) => `${filesRelative}/${entry.relativePath}`)
10007
+ ];
10008
+ const message = options.message ?? `share: publish pack ${artifact.agent}/${artifact.name} (${prepared.length} files)`;
10009
+ const gitMessages = await publishShareGitChange(team.config.worktree, stagedPaths, message, {
10010
+ dryRun,
10011
+ push: options.push
10012
+ });
10013
+ return {
10014
+ artifact,
10015
+ gitMessages,
10016
+ messages,
10017
+ sourcePath: resolvedManifest,
10018
+ targetPath: indexTargetPath,
10019
+ targetUri: indexTargetUri
10020
+ };
10021
+ }
9267
10022
  async function runShareInstallArtifacts(config, options) {
9268
10023
  const result = await installSharedAgentArtifacts(config, options);
9269
10024
  if (result.syncedTeams.length > 0) {
@@ -9303,6 +10058,10 @@ async function installSharedAgentArtifacts(config, options) {
9303
10058
  }
9304
10059
  let installedCount = 0;
9305
10060
  for (const artifact of artifacts) {
10061
+ if (isBundleArtifact(artifact)) {
10062
+ installedCount += await installBundleArtifact(artifact, options, dryRun, messages);
10063
+ continue;
10064
+ }
9306
10065
  const label = sharedArtifactLabel(artifact.artifact);
9307
10066
  const state = await sharedArtifactInstallState(artifact);
9308
10067
  if (dryRun) {
@@ -9828,6 +10587,9 @@ function sharedArtifactFromRelativePath(relativePath) {
9828
10587
  if (parts.length === 4 && parts[1] === "commands" && parts[2] === "claude" && parts[3].endsWith(".md")) {
9829
10588
  return { agent: "claude", kind: "command", name: parts[3].slice(0, -".md".length) };
9830
10589
  }
10590
+ if (parts.length === 5 && parts[1] === "packs" && (parts[2] === "codex" || parts[2] === "claude") && parts[4] === `${parts[3]}${PACK_INDEX_SUFFIX}`) {
10591
+ return { agent: parts[2], kind: "pack", name: parts[3] };
10592
+ }
9831
10593
  return void 0;
9832
10594
  }
9833
10595
  async function collectSharedArtifacts(worktree, team) {
@@ -9852,18 +10614,95 @@ async function collectSharedArtifacts(worktree, team) {
9852
10614
  if (artifact === void 0) {
9853
10615
  continue;
9854
10616
  }
9855
- out.push({
9856
- artifact,
9857
- installPath: sharedArtifactInstallPath(team, artifact),
9858
- sourcePath: full,
9859
- sourceRelativePath: relativePath,
9860
- team
9861
- });
10617
+ const artifactDir = (0, import_node_path6.dirname)(full);
10618
+ if (artifact.kind === "pack" && !await isFile((0, import_node_path6.join)(artifactDir, `${artifact.name}${PACK_MANIFEST_SUFFIX}`))) {
10619
+ console.warn(
10620
+ `Skipping incomplete shared pack (missing ${artifact.name}${PACK_MANIFEST_SUFFIX}): ${relativePath}`
10621
+ );
10622
+ continue;
10623
+ }
10624
+ try {
10625
+ out.push({
10626
+ artifact,
10627
+ installPath: sharedArtifactInstallPath(team, artifact),
10628
+ members: await collectArtifactMembers(artifact, artifactDir),
10629
+ sourcePath: full,
10630
+ sourceRelativePath: relativePath,
10631
+ team
10632
+ });
10633
+ } catch (err) {
10634
+ console.warn(`Skipping shared artifact ${relativePath}: ${err instanceof Error ? err.message : String(err)}`);
10635
+ }
9862
10636
  }
9863
10637
  }
9864
10638
  await visit(root);
9865
10639
  return out.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
9866
10640
  }
10641
+ async function collectArtifactMembers(artifact, artifactDir) {
10642
+ if (artifact.kind === "skill") {
10643
+ return collectSharedBundleMembers(artifactDir);
10644
+ }
10645
+ if (artifact.kind === "pack") {
10646
+ return collectSharedPackMembers(artifact, artifactDir);
10647
+ }
10648
+ return void 0;
10649
+ }
10650
+ function isContainedMemberPath(baseDir, relativePath) {
10651
+ if ((0, import_node_path6.isAbsolute)(relativePath) || relativePath.split("/").includes("..")) {
10652
+ return false;
10653
+ }
10654
+ const resolved = (0, import_node_path6.join)(baseDir, ...relativePath.split("/"));
10655
+ return resolved === baseDir || resolved.startsWith(baseDir + import_node_path6.sep);
10656
+ }
10657
+ async function collectSharedPackMembers(artifact, packDir) {
10658
+ const filesDir = (0, import_node_path6.join)(packDir, PACK_FILES_DIR);
10659
+ if (!await isDirectory(filesDir)) {
10660
+ return [];
10661
+ }
10662
+ const manifestRaw = await readFileIfExists((0, import_node_path6.join)(packDir, `${artifact.name}${PACK_MANIFEST_SUFFIX}`));
10663
+ if (manifestRaw !== void 0) {
10664
+ const rawMembers = parseJsonConfigObject(manifestRaw)?.members;
10665
+ if (Array.isArray(rawMembers)) {
10666
+ const fromManifest = [];
10667
+ for (const entry of rawMembers) {
10668
+ const path = entry?.path;
10669
+ if (typeof path === "string" && path.length > 0) {
10670
+ if (!isContainedMemberPath(filesDir, path)) {
10671
+ throw new Error(`Refusing pack member with an unsafe path that escapes the pack root: ${path}`);
10672
+ }
10673
+ fromManifest.push({ absolutePath: (0, import_node_path6.join)(filesDir, ...path.split("/")), relativePath: path });
10674
+ }
10675
+ }
10676
+ if (fromManifest.length > 0) {
10677
+ return fromManifest.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
10678
+ }
10679
+ }
10680
+ }
10681
+ return collectBundleMemberFiles(filesDir);
10682
+ }
10683
+ async function collectSharedBundleMembers(skillDir) {
10684
+ const manifestRaw = await readFileIfExists((0, import_node_path6.join)(skillDir, BUNDLE_MANIFEST_FILE));
10685
+ if (manifestRaw !== void 0) {
10686
+ const parsed = parseJsonConfigObject(manifestRaw);
10687
+ const rawMembers = parsed?.members;
10688
+ if (Array.isArray(rawMembers)) {
10689
+ const fromManifest = [];
10690
+ for (const entry of rawMembers) {
10691
+ const path = entry?.path;
10692
+ if (typeof path === "string" && path.length > 0) {
10693
+ if (!isContainedMemberPath(skillDir, path)) {
10694
+ throw new Error(`Refusing skill member with an unsafe path that escapes the skill root: ${path}`);
10695
+ }
10696
+ fromManifest.push({ absolutePath: (0, import_node_path6.join)(skillDir, ...path.split("/")), relativePath: path });
10697
+ }
10698
+ }
10699
+ if (fromManifest.length > 0) {
10700
+ return fromManifest.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
10701
+ }
10702
+ }
10703
+ }
10704
+ return collectBundleMemberFiles(skillDir);
10705
+ }
9867
10706
  function filterSharedArtifacts(artifacts, options) {
9868
10707
  const name = options.name === void 0 ? void 0 : uriSegment(options.name);
9869
10708
  return artifacts.filter((artifact) => {
@@ -9885,6 +10724,218 @@ async function maybeSyncSharedArtifacts(config, options) {
9885
10724
  }
9886
10725
  return syncSharedReposBeforeAgentRead(config);
9887
10726
  }
10727
+ function bundleInstallRoot(artifact) {
10728
+ return artifact.artifact.kind === "pack" ? artifact.installPath : (0, import_node_path6.dirname)(artifact.installPath);
10729
+ }
10730
+ function bundleInstallMetadataPath(artifact) {
10731
+ return (0, import_node_path6.join)(bundleInstallRoot(artifact), BUNDLE_INSTALL_METADATA_FILE);
10732
+ }
10733
+ async function readBundleInstallMetadata(artifact) {
10734
+ const raw = await readFileIfExists(bundleInstallMetadataPath(artifact));
10735
+ if (raw === void 0) {
10736
+ return void 0;
10737
+ }
10738
+ const parsed = parseJsonConfigObject(raw);
10739
+ if (parsed === void 0 || parsed.version !== ARTIFACT_INSTALL_METADATA_VERSION || !Array.isArray(parsed.members)) {
10740
+ return void 0;
10741
+ }
10742
+ const recordedArtifact = parsed.artifact;
10743
+ if (recordedArtifact?.agent !== artifact.artifact.agent || recordedArtifact?.kind !== artifact.artifact.kind || recordedArtifact?.name !== artifact.artifact.name || parsed.team !== artifact.team) {
10744
+ return void 0;
10745
+ }
10746
+ const map2 = /* @__PURE__ */ new Map();
10747
+ for (const entry of parsed.members) {
10748
+ const path = entry?.path;
10749
+ const sourceSha256 = entry?.sourceSha256;
10750
+ const installedSha256 = entry?.installedSha256;
10751
+ if (typeof path === "string" && typeof sourceSha256 === "string" && typeof installedSha256 === "string") {
10752
+ map2.set(path, { installedSha256, sourceSha256 });
10753
+ }
10754
+ }
10755
+ return map2;
10756
+ }
10757
+ async function sharedBundleInstallStatus(artifact) {
10758
+ const members = artifact.members ?? [];
10759
+ const installRoot = bundleInstallRoot(artifact);
10760
+ const metadata = await readBundleInstallMetadata(artifact);
10761
+ const expanded = await prepareInstallMembers(members, installRoot, artifact.artifact.kind === "pack");
10762
+ const expectedByPath = new Map(expanded.map((entry) => [entry.relativePath, entry]));
10763
+ let installedCount = 0;
10764
+ let localChanged = false;
10765
+ let remoteChanged = false;
10766
+ const memberPaths = /* @__PURE__ */ new Set();
10767
+ for (const member of members) {
10768
+ memberPaths.add(member.relativePath);
10769
+ const expected = expectedByPath.get(member.relativePath);
10770
+ if (expected === void 0) {
10771
+ remoteChanged = true;
10772
+ continue;
10773
+ }
10774
+ const installedBytes = await readFileBytesIfExists((0, import_node_path6.join)(installRoot, ...member.relativePath.split("/")));
10775
+ if (installedBytes === void 0) {
10776
+ remoteChanged = true;
10777
+ continue;
10778
+ }
10779
+ installedCount += 1;
10780
+ const installedSha = sha256(installedBytes);
10781
+ const recorded = metadata?.get(member.relativePath);
10782
+ if (recorded === void 0) {
10783
+ if (installedSha !== expected?.installedSha256) {
10784
+ localChanged = true;
10785
+ }
10786
+ continue;
10787
+ }
10788
+ if (installedSha !== recorded.installedSha256) {
10789
+ localChanged = true;
10790
+ }
10791
+ if (expected?.sourceSha256 !== recorded.sourceSha256) {
10792
+ remoteChanged = true;
10793
+ }
10794
+ }
10795
+ if (metadata !== void 0) {
10796
+ for (const [recordedPath, recorded] of metadata) {
10797
+ if (memberPaths.has(recordedPath)) {
10798
+ continue;
10799
+ }
10800
+ remoteChanged = true;
10801
+ const installedBytes = await readFileBytesIfExists((0, import_node_path6.join)(installRoot, ...recordedPath.split("/")));
10802
+ if (installedBytes !== void 0 && sha256(installedBytes) !== recorded.installedSha256) {
10803
+ localChanged = true;
10804
+ }
10805
+ }
10806
+ }
10807
+ if (installedCount === 0 && metadata === void 0) {
10808
+ return "not_installed";
10809
+ }
10810
+ if (localChanged && remoteChanged) {
10811
+ return "remote_changed_and_local_modified";
10812
+ }
10813
+ if (remoteChanged) {
10814
+ return "update_available";
10815
+ }
10816
+ if (localChanged) {
10817
+ return "local_modified";
10818
+ }
10819
+ return "current";
10820
+ }
10821
+ function expandPackRoot(text, installRoot) {
10822
+ return text.split(PACK_ROOT_TOKEN).join(installRoot);
10823
+ }
10824
+ async function prepareInstallMembers(members, installRoot, expandTokens) {
10825
+ const prepared = await Promise.all(
10826
+ members.map(async (member) => {
10827
+ const sourceBytes = await readFileBytesIfExists(member.absolutePath);
10828
+ if (sourceBytes === void 0) {
10829
+ return void 0;
10830
+ }
10831
+ const installedBytes = expandTokens && !isProbablyBinary(sourceBytes) ? Buffer.from(expandPackRoot(sourceBytes.toString("utf8"), installRoot), "utf8") : sourceBytes;
10832
+ return {
10833
+ installedBytes,
10834
+ installedSha256: sha256(installedBytes),
10835
+ relativePath: member.relativePath,
10836
+ sourceSha256: sha256(sourceBytes)
10837
+ };
10838
+ })
10839
+ );
10840
+ return prepared.filter((member) => member !== void 0);
10841
+ }
10842
+ function serializeInstallMetadata(artifact, prepared) {
10843
+ const metadata = {
10844
+ artifact: artifact.artifact,
10845
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
10846
+ members: prepared.map((entry) => ({
10847
+ installedSha256: entry.installedSha256,
10848
+ path: entry.relativePath,
10849
+ sourceSha256: entry.sourceSha256
10850
+ })).sort((a, b) => compareStrings(a.path, b.path)),
10851
+ team: artifact.team,
10852
+ version: ARTIFACT_INSTALL_METADATA_VERSION
10853
+ };
10854
+ return `${JSON.stringify(metadata, void 0, 2)}
10855
+ `;
10856
+ }
10857
+ async function installBundleArtifact(artifact, options, dryRun, messages) {
10858
+ const members = artifact.members ?? [];
10859
+ const installRoot = bundleInstallRoot(artifact);
10860
+ const kindLabel = artifact.artifact.kind === "pack" ? "pack" : "bundle";
10861
+ const label = `${sharedArtifactLabel(artifact.artifact)} ${kindLabel} (${members.length} files)`;
10862
+ const status = await sharedBundleInstallStatus(artifact);
10863
+ if (dryRun) {
10864
+ const verb = sharedArtifactDryRunVerb(status, options.force === true);
10865
+ const suffix = sharedArtifactDryRunSuffix(status, options.force === true);
10866
+ messages.push(`${verb} ${label}: ${portablePath(installRoot)}${suffix}`);
10867
+ return 0;
10868
+ }
10869
+ if ((status === "local_modified" || status === "remote_changed_and_local_modified") && options.force !== true) {
10870
+ throw new Error(`Refusing to overwrite ${portablePath(installRoot)}. Pass force=true or --force.`);
10871
+ }
10872
+ const prepared = await prepareInstallMembers(members, installRoot, artifact.artifact.kind === "pack");
10873
+ if (prepared.length < members.length && options.force !== true) {
10874
+ throw new Error(
10875
+ `Refusing to install ${portablePath(installRoot)}: ${members.length - prepared.length} declared member(s) are unreadable in the shared pack (the shared worktree may be mid-sync). Retry after sync, or pass force=true / --force.`
10876
+ );
10877
+ }
10878
+ if (status === "current") {
10879
+ await (0, import_promises5.writeFile)(bundleInstallMetadataPath(artifact), serializeInstallMetadata(artifact, prepared), { mode: 384 });
10880
+ messages.push(`Already installed ${label}: ${portablePath(installRoot)}`);
10881
+ await surfacePackRequirements(artifact, messages);
10882
+ return 0;
10883
+ }
10884
+ const stagingRoot = `${installRoot}.threadnote-staging`;
10885
+ await (0, import_promises5.rm)(stagingRoot, { force: true, recursive: true });
10886
+ for (const entry of prepared) {
10887
+ const dest = (0, import_node_path6.join)(stagingRoot, ...entry.relativePath.split("/"));
10888
+ await ensureDirectory((0, import_node_path6.dirname)(dest), false);
10889
+ await (0, import_promises5.writeFile)(dest, entry.installedBytes, { mode: 384 });
10890
+ }
10891
+ await (0, import_promises5.writeFile)((0, import_node_path6.join)(stagingRoot, BUNDLE_INSTALL_METADATA_FILE), serializeInstallMetadata(artifact, prepared), {
10892
+ mode: 384
10893
+ });
10894
+ await ensureDirectory((0, import_node_path6.dirname)(installRoot), false);
10895
+ const backupRoot = `${installRoot}.threadnote-old`;
10896
+ await (0, import_promises5.rm)(backupRoot, { force: true, recursive: true });
10897
+ const hadPriorInstall = await exists(installRoot);
10898
+ if (hadPriorInstall) {
10899
+ await (0, import_promises5.rename)(installRoot, backupRoot);
10900
+ }
10901
+ try {
10902
+ await (0, import_promises5.rename)(stagingRoot, installRoot);
10903
+ } catch (swapErr) {
10904
+ if (hadPriorInstall) {
10905
+ await (0, import_promises5.rename)(backupRoot, installRoot);
10906
+ }
10907
+ throw swapErr;
10908
+ }
10909
+ await (0, import_promises5.rm)(backupRoot, { force: true, recursive: true });
10910
+ messages.push(`${sharedArtifactInstallVerb(status, options.force === true)} ${label}: ${portablePath(installRoot)}`);
10911
+ await surfacePackRequirements(artifact, messages);
10912
+ return 1;
10913
+ }
10914
+ async function surfacePackRequirements(artifact, messages) {
10915
+ if (artifact.artifact.kind !== "pack") {
10916
+ return;
10917
+ }
10918
+ const raw = await readFileIfExists(
10919
+ (0, import_node_path6.join)((0, import_node_path6.dirname)(artifact.sourcePath), `${artifact.artifact.name}${PACK_MANIFEST_SUFFIX}`)
10920
+ );
10921
+ if (raw === void 0) {
10922
+ return;
10923
+ }
10924
+ const deps = parseJsonConfigObject(raw)?.deps;
10925
+ if (deps === void 0 || typeof deps !== "object") {
10926
+ return;
10927
+ }
10928
+ const stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
10929
+ const depsRecord = deps;
10930
+ const tooling = [...stringList(depsRecord.runtime), ...stringList(depsRecord.cli), ...stringList(depsRecord.os)];
10931
+ const mcp = stringList(depsRecord.mcp);
10932
+ if (tooling.length > 0) {
10933
+ messages.push(`This pack will NOT run until these exist (Threadnote installs files only): ${tooling.join(", ")}.`);
10934
+ }
10935
+ if (mcp.length > 0) {
10936
+ messages.push(`Configure these MCP server(s) separately: ${mcp.join(", ")}.`);
10937
+ }
10938
+ }
9888
10939
  async function sharedArtifactInstallState(artifact) {
9889
10940
  const sourceContent = await (0, import_promises5.readFile)(artifact.sourcePath, "utf8");
9890
10941
  const sourceSha = sha256(sourceContent);
@@ -10007,11 +11058,12 @@ function sharedArtifactLabel(artifact) {
10007
11058
  return `${artifact.kind} ${artifact.agent}/${artifact.name}`;
10008
11059
  }
10009
11060
  function sharedArtifactInstallPath(team, artifact) {
10010
- if (artifact.kind === "skill" && artifact.agent === "codex") {
10011
- return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".codex", "skills", "threadnote", team, artifact.name, "SKILL.md");
11061
+ const agentDir = artifact.agent === "codex" ? ".codex" : ".claude";
11062
+ if (artifact.kind === "pack") {
11063
+ return (0, import_node_path6.join)((0, import_node_os4.homedir)(), agentDir, "skills", "threadnote-packs", team, artifact.name);
10012
11064
  }
10013
- if (artifact.kind === "skill" && artifact.agent === "claude") {
10014
- return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".claude", "skills", "threadnote", team, artifact.name, "SKILL.md");
11065
+ if (artifact.kind === "skill") {
11066
+ return (0, import_node_path6.join)((0, import_node_os4.homedir)(), agentDir, "skills", "threadnote", team, artifact.name, "SKILL.md");
10015
11067
  }
10016
11068
  return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".claude", "commands", "threadnote", team, `${artifact.name}.md`);
10017
11069
  }
@@ -12384,7 +13436,7 @@ function parseGithubRelease(value) {
12384
13436
  }
12385
13437
  function titleFromReleaseName(name, version) {
12386
13438
  const trimmed = name.trim();
12387
- const withoutPrefix = trimmed.replace(new RegExp(`^v?${escapeRegExp2(version)}:\\s*`, "i"), "");
13439
+ const withoutPrefix = trimmed.replace(new RegExp(`^v?${escapeRegExp3(version)}:\\s*`, "i"), "");
12388
13440
  return withoutPrefix || trimmed || "Release notes";
12389
13441
  }
12390
13442
  function bodyLines(body) {
@@ -12402,7 +13454,7 @@ function bodyLines(body) {
12402
13454
  }
12403
13455
  return out;
12404
13456
  }
12405
- function escapeRegExp2(value) {
13457
+ function escapeRegExp3(value) {
12406
13458
  return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
12407
13459
  }
12408
13460
 
@@ -15435,12 +16487,18 @@ async function main() {
15435
16487
  ).action(async (uri, options) => {
15436
16488
  await runSharePublish(getRuntimeConfig(program2), uri, options);
15437
16489
  });
15438
- share.command("publish-artifact").description("Publish a local Codex/Claude skill or Claude command into the shared team repo").argument("<path>", "Path to SKILL.md or a Claude command markdown file").option("--team <name>", "Team name; defaults to the configured default team").option("--agent <agent>", "Agent owner when path inference is ambiguous: codex or claude").option("--kind <kind>", "Artifact kind when path inference is ambiguous: skill or command").option("--name <name>", "Shared artifact name; defaults to skill directory or command file stem").option("--message <text>", "Commit message override").option("--force", "Replace an existing shared artifact with different content").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option("--preview", "Print the exact artifact bytes that would land in the shared git repo").option(
16490
+ share.command("publish-artifact").description("Publish a local Codex/Claude skill or Claude command into the shared team repo").argument("<path>", "Path to SKILL.md or a Claude command markdown file").option("--team <name>", "Team name; defaults to the configured default team").option("--agent <agent>", "Agent owner when path inference is ambiguous: codex or claude").option("--kind <kind>", "Artifact kind when path inference is ambiguous: skill or command").option("--name <name>", "Shared artifact name; defaults to skill directory or command file stem").option("--message <text>", "Commit message override").option("--force", "Replace an existing shared artifact with different content").option("--allow-binary", "Include binary skill files (unscannable by the scrubber); blocked by default").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option("--preview", "Print what would land in the shared git repo without writing or committing").option(
15439
16491
  "--redact",
15440
16492
  "Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
15441
16493
  ).action(async (path, options) => {
15442
16494
  await runSharePublishArtifact(getRuntimeConfig(program2), path, options);
15443
16495
  });
16496
+ share.command("publish-bundle").description("Publish a multi-skill constellation (pack) declared by a threadnote-bundle.json manifest").argument("<manifest>", "Path to a threadnote-bundle.json manifest").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--force", "Replace existing shared pack files with different content").option("--allow-binary", "Include binary files (unscannable by the scrubber); blocked by default").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option("--preview", "Print what would land in the shared git repo without writing or committing").option(
16497
+ "--redact",
16498
+ "Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
16499
+ ).action(async (manifest, options) => {
16500
+ await runSharePublishBundle(getRuntimeConfig(program2), manifest, options);
16501
+ });
15444
16502
  share.command("install-artifacts").description("Install shared Codex/Claude skills and Claude commands from a team repo").option("--team <name>", "Team name; defaults to the configured default team").option("--agent <agent>", "Install only artifacts for this agent: codex or claude").option("--kind <kind>", "Install only artifacts of this kind: skill or command").option("--name <name>", "Install only this shared artifact name").option("--apply", "Actually write files into local agent skill/command directories").option("--force", "Replace existing installed artifacts with different content").option("--no-sync", "Skip pulling shared team updates before listing/installing artifacts").option("--dry-run", "Preview install actions without writing files").action(async (options) => {
15445
16503
  await runShareInstallArtifacts(getRuntimeConfig(program2), options);
15446
16504
  });