threadnote 1.1.6 → 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.
@@ -4349,6 +4349,7 @@ function grepUrisFromJson(output2) {
4349
4349
  }
4350
4350
  }
4351
4351
  var RECALL_CATEGORY_ORDER = ["memories", "resources", "skills"];
4352
+ var RECALL_PROMOTED_EXACT_SCORE = 0;
4352
4353
  function recallSnippet(value) {
4353
4354
  if (typeof value !== "string") {
4354
4355
  return "";
@@ -4400,11 +4401,14 @@ function parseRecallHits(output2, options = {}) {
4400
4401
  return [];
4401
4402
  }
4402
4403
  }
4404
+ function stripAnchor(uri) {
4405
+ return uri.replace(/#.*$/, "");
4406
+ }
4403
4407
  function mergeRecallHits(passes) {
4404
4408
  const byDocument = /* @__PURE__ */ new Map();
4405
4409
  for (const pass of passes) {
4406
4410
  for (const hit of pass) {
4407
- const documentUri = hit.uri.replace(/#.*$/, "");
4411
+ const documentUri = stripAnchor(hit.uri);
4408
4412
  const existing = byDocument.get(documentUri);
4409
4413
  if (!existing || hit.score > existing.score) {
4410
4414
  byDocument.set(documentUri, { ...hit, uri: documentUri });
@@ -4412,23 +4416,120 @@ function mergeRecallHits(passes) {
4412
4416
  }
4413
4417
  }
4414
4418
  return [...byDocument.values()].sort(
4415
- (left, right) => RECALL_CATEGORY_ORDER.indexOf(left.category) - RECALL_CATEGORY_ORDER.indexOf(right.category) || right.score - left.score
4419
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || right.score - left.score
4416
4420
  );
4417
4421
  }
4418
- function formatRecallHits(hits, maxHits) {
4419
- if (hits.length === 0) {
4422
+ function recallCategoryRank(category) {
4423
+ const index = RECALL_CATEGORY_ORDER.indexOf(category);
4424
+ return index === -1 ? RECALL_CATEGORY_ORDER.length : index;
4425
+ }
4426
+ function dedupeByContent(hits) {
4427
+ const seen = /* @__PURE__ */ new Set();
4428
+ const kept = [];
4429
+ for (const hit of hits) {
4430
+ if (hit.category !== "memories" && hit.snippet.length > 0) {
4431
+ const key = `${hit.category}
4432
+ ${hit.snippet}`;
4433
+ if (seen.has(key)) {
4434
+ continue;
4435
+ }
4436
+ seen.add(key);
4437
+ }
4438
+ kept.push(hit);
4439
+ }
4440
+ return kept;
4441
+ }
4442
+ function categoryForUri(uri) {
4443
+ if (uri.includes("/memories/")) {
4444
+ return "memories";
4445
+ }
4446
+ if (uri.startsWith("viking://resources/agent-skills/")) {
4447
+ return "skills";
4448
+ }
4449
+ return "resources";
4450
+ }
4451
+ function contextTypeForCategory(category) {
4452
+ if (category === "memories") {
4453
+ return "memory";
4454
+ }
4455
+ return category === "skills" ? "skill" : "resource";
4456
+ }
4457
+ function applyExactMatchBoost(hits, exactMatches) {
4458
+ if (exactMatches.length === 0) {
4459
+ return hits;
4460
+ }
4461
+ const termsByUri = new Map(exactMatches.map((match) => [stripAnchor(match.uri), match.terms]));
4462
+ const annotated = hits.map((hit) => {
4463
+ const terms = termsByUri.get(stripAnchor(hit.uri));
4464
+ return terms ? { ...hit, exactTerms: terms } : hit;
4465
+ });
4466
+ const present = new Set(annotated.map((hit) => stripAnchor(hit.uri)));
4467
+ const promoted = [...termsByUri.keys()].filter((uri) => !present.has(uri)).map((uri) => {
4468
+ const category = categoryForUri(uri);
4469
+ return {
4470
+ category,
4471
+ contextType: contextTypeForCategory(category),
4472
+ exactTerms: termsByUri.get(uri) ?? [],
4473
+ score: RECALL_PROMOTED_EXACT_SCORE,
4474
+ snippet: "",
4475
+ uri
4476
+ };
4477
+ });
4478
+ return [...annotated, ...promoted].sort(
4479
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (right.exactTerms?.length ?? 0) - (left.exactTerms?.length ?? 0) || right.score - left.score
4480
+ );
4481
+ }
4482
+ function renderRecallHits(shown, overflow) {
4483
+ if (shown.length === 0) {
4420
4484
  return void 0;
4421
4485
  }
4422
- const shown = hits.slice(0, maxHits);
4423
4486
  const lines = shown.flatMap((hit, index) => {
4424
- const head = `${index + 1}. ${hit.contextType} \xB7 score ${hit.score.toFixed(2)} \xB7 ${hit.uri}`;
4487
+ const scorePart = hit.score > 0 ? `score ${hit.score.toFixed(2)}` : void 0;
4488
+ const exactPart = hit.exactTerms?.length ? `exact: ${hit.exactTerms.join(", ")}` : void 0;
4489
+ const head = `${index + 1}. ${[hit.contextType, scorePart, exactPart].filter(Boolean).join(" \xB7 ")} \xB7 ${hit.uri}`;
4425
4490
  return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
4426
4491
  });
4427
- if (hits.length > maxHits) {
4428
- lines.push(`(+${hits.length - maxHits} more \u2014 refine the query or read a URI above)`);
4492
+ if (overflow > 0) {
4493
+ lines.push(`(+${overflow} more \u2014 refine the query or read a URI above)`);
4429
4494
  }
4430
4495
  return lines.join("\n");
4431
4496
  }
4497
+ var RECALL_CATEGORY_RESERVE = 2;
4498
+ function selectShownHits(ranked, limit, reserve) {
4499
+ if (ranked.length <= limit) {
4500
+ return ranked;
4501
+ }
4502
+ const selected = /* @__PURE__ */ new Set();
4503
+ for (const category of RECALL_CATEGORY_ORDER) {
4504
+ let taken = 0;
4505
+ for (const hit of ranked) {
4506
+ if (selected.size >= limit || taken >= reserve) {
4507
+ break;
4508
+ }
4509
+ if (hit.category === category && !selected.has(hit.uri)) {
4510
+ selected.add(hit.uri);
4511
+ taken += 1;
4512
+ }
4513
+ }
4514
+ }
4515
+ for (const hit of ranked) {
4516
+ if (selected.size >= limit) {
4517
+ break;
4518
+ }
4519
+ selected.add(hit.uri);
4520
+ }
4521
+ return ranked.filter((hit) => selected.has(hit.uri));
4522
+ }
4523
+ function buildRecallSections(passes, exactMatches, limit) {
4524
+ const ranked = dedupeByContent(applyExactMatchBoost(mergeRecallHits(passes), exactMatches));
4525
+ const shown = selectShownHits(ranked, limit, RECALL_CATEGORY_RESERVE);
4526
+ const shownUris = new Set(shown.map((hit) => stripAnchor(hit.uri)));
4527
+ return {
4528
+ exactTail: formatExactMatchPointers(exactMatches.filter((match) => !shownUris.has(stripAnchor(match.uri)))),
4529
+ ranked,
4530
+ semanticSection: renderRecallHits(shown, ranked.length - shown.length)
4531
+ };
4532
+ }
4432
4533
  function exactMemoryScopeUris(params) {
4433
4534
  const { agentMemoriesUri, includeArchived, intents, projectName, projectResourceUri, userBase } = params;
4434
4535
  const durable = projectName ? `${userBase}/durable/projects/${projectName}` : `${userBase}/durable/projects`;
@@ -8497,6 +8598,7 @@ function formatPlanSection(title, lines) {
8497
8598
  var import_promises5 = require("node:fs/promises");
8498
8599
  var import_node_os4 = require("node:os");
8499
8600
  var import_node_path6 = require("node:path");
8601
+ var import_node_util = require("node:util");
8500
8602
  var TEAMS_FILE_VERSION = 1;
8501
8603
  var SHARED_SEGMENT = "shared";
8502
8604
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
@@ -8504,6 +8606,15 @@ var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
8504
8606
  var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
8505
8607
  var SHAREABLE_ROOT_FILES = ["README.md", "AGENTS.md", "CLAUDE.md", "SKILL.md", ".gitignore"];
8506
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}";
8507
8618
  var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
8508
8619
  var DEFAULT_GIT_REMOTE_NAME = "origin";
8509
8620
  var SCRUBBER_PATTERNS = [
@@ -8530,7 +8641,10 @@ var SCRUBBER_PATTERNS = [
8530
8641
  // so redaction collapses an entire path to a single placeholder rather than
8531
8642
  // leaving the subpath visible.
8532
8643
  { name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
8533
- { 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)>"'`,]+/ }
8534
8648
  ];
8535
8649
  function applyScrubber(content, { redact }) {
8536
8650
  let cleaned = content;
@@ -8938,12 +9052,47 @@ async function runShareSyncQuiet(config, state, options) {
8938
9052
  return void 0;
8939
9053
  }
8940
9054
  async function stageShareableChanges(dryRun, git, worktree) {
9055
+ await removeOrphanPackIndexes(dryRun, git, worktree);
8941
9056
  const pathspecs = await existingShareablePathspecs(git, worktree);
8942
9057
  if (pathspecs.length === 0) {
8943
9058
  return;
8944
9059
  }
8945
9060
  await maybeRun(dryRun, git, ["-C", worktree, "add", "-A", "--", ...pathspecs], { allowFailure: true });
8946
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
+ }
8947
9096
  async function existingShareablePathspecs(git, worktree) {
8948
9097
  const rootFiles = await Promise.all(
8949
9098
  SHAREABLE_ROOT_FILES.map(
@@ -8970,7 +9119,8 @@ async function publishShareGitChange(worktree, relativePath, commitMessage, opti
8970
9119
  const verb = options.verb ?? "add";
8971
9120
  const git = await requiredExecutable("git");
8972
9121
  const messages = [];
8973
- 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];
8974
9124
  const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
8975
9125
  if (stageResult) {
8976
9126
  messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
@@ -9089,13 +9239,23 @@ async function runSharePublishArtifact(config, sourcePath, options) {
9089
9239
  }
9090
9240
  async function shareAgentArtifact(config, sourcePath, options) {
9091
9241
  const team = await resolveTeam(config, options.team);
9092
- const dryRun = options.dryRun === true;
9093
- const preview = options.preview === true;
9094
9242
  const resolvedSourcePath = expandPath(sourcePath);
9095
9243
  if (!await isRegularFileNoSymlink(resolvedSourcePath)) {
9096
9244
  throw new Error(`Agent artifact source is not a regular file: ${resolvedSourcePath}`);
9097
9245
  }
9098
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;
9099
9259
  const rawContent = await (0, import_promises5.readFile)(resolvedSourcePath, "utf8");
9100
9260
  if (!rawContent.trim()) {
9101
9261
  throw new Error(`Refusing to share empty agent artifact: ${resolvedSourcePath}`);
@@ -9163,6 +9323,702 @@ async function shareAgentArtifact(config, sourcePath, options) {
9163
9323
  });
9164
9324
  return { artifact, gitMessages, messages, sourcePath: resolvedSourcePath, targetPath, targetUri };
9165
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
+ }
9166
10022
  async function runShareInstallArtifacts(config, options) {
9167
10023
  const result = await installSharedAgentArtifacts(config, options);
9168
10024
  if (result.syncedTeams.length > 0) {
@@ -9202,6 +10058,10 @@ async function installSharedAgentArtifacts(config, options) {
9202
10058
  }
9203
10059
  let installedCount = 0;
9204
10060
  for (const artifact of artifacts) {
10061
+ if (isBundleArtifact(artifact)) {
10062
+ installedCount += await installBundleArtifact(artifact, options, dryRun, messages);
10063
+ continue;
10064
+ }
9205
10065
  const label = sharedArtifactLabel(artifact.artifact);
9206
10066
  const state = await sharedArtifactInstallState(artifact);
9207
10067
  if (dryRun) {
@@ -9727,6 +10587,9 @@ function sharedArtifactFromRelativePath(relativePath) {
9727
10587
  if (parts.length === 4 && parts[1] === "commands" && parts[2] === "claude" && parts[3].endsWith(".md")) {
9728
10588
  return { agent: "claude", kind: "command", name: parts[3].slice(0, -".md".length) };
9729
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
+ }
9730
10593
  return void 0;
9731
10594
  }
9732
10595
  async function collectSharedArtifacts(worktree, team) {
@@ -9751,18 +10614,95 @@ async function collectSharedArtifacts(worktree, team) {
9751
10614
  if (artifact === void 0) {
9752
10615
  continue;
9753
10616
  }
9754
- out.push({
9755
- artifact,
9756
- installPath: sharedArtifactInstallPath(team, artifact),
9757
- sourcePath: full,
9758
- sourceRelativePath: relativePath,
9759
- team
9760
- });
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
+ }
9761
10636
  }
9762
10637
  }
9763
10638
  await visit(root);
9764
10639
  return out.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
9765
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
+ }
9766
10706
  function filterSharedArtifacts(artifacts, options) {
9767
10707
  const name = options.name === void 0 ? void 0 : uriSegment(options.name);
9768
10708
  return artifacts.filter((artifact) => {
@@ -9784,6 +10724,218 @@ async function maybeSyncSharedArtifacts(config, options) {
9784
10724
  }
9785
10725
  return syncSharedReposBeforeAgentRead(config);
9786
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
+ }
9787
10939
  async function sharedArtifactInstallState(artifact) {
9788
10940
  const sourceContent = await (0, import_promises5.readFile)(artifact.sourcePath, "utf8");
9789
10941
  const sourceSha = sha256(sourceContent);
@@ -9906,11 +11058,12 @@ function sharedArtifactLabel(artifact) {
9906
11058
  return `${artifact.kind} ${artifact.agent}/${artifact.name}`;
9907
11059
  }
9908
11060
  function sharedArtifactInstallPath(team, artifact) {
9909
- if (artifact.kind === "skill" && artifact.agent === "codex") {
9910
- 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);
9911
11064
  }
9912
- if (artifact.kind === "skill" && artifact.agent === "claude") {
9913
- 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");
9914
11067
  }
9915
11068
  return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".claude", "commands", "threadnote", team, `${artifact.name}.md`);
9916
11069
  }
@@ -10478,19 +11631,17 @@ async function runRecall(config, options) {
10478
11631
  passes.push(await recallSearchHits(config, ov, searchArgs(seededUri), { dryRun, includeArchived }));
10479
11632
  }
10480
11633
  const recallOutputs = [];
10481
- const semanticSection = formatRecallHits(mergeRecallHits(passes), nodeLimit ?? 12);
11634
+ const exactMatches = await collectExactMemoryMatches(config, ov, query, { dryRun, includeArchived, project });
11635
+ const { semanticSection, exactTail } = buildRecallSections(passes, exactMatches, nodeLimit ?? 12);
10482
11636
  if (semanticSection) {
10483
11637
  console.log(`
10484
11638
  ${semanticSection}`);
10485
11639
  recallOutputs.push(semanticSection);
10486
11640
  }
10487
- const exactOutput = await printExactMemoryMatches(config, ov, query, {
10488
- dryRun,
10489
- includeArchived,
10490
- project
10491
- });
10492
- if (exactOutput) {
10493
- recallOutputs.push(exactOutput);
11641
+ if (exactTail) {
11642
+ console.log(`
11643
+ ${exactTail}`);
11644
+ recallOutputs.push(exactTail);
10494
11645
  }
10495
11646
  await printRecallHygieneNudges(config, recallOutputs.join("\n"));
10496
11647
  }
@@ -10854,10 +12005,10 @@ function hasAgentSkillCatalogIntent(query) {
10854
12005
  }
10855
12006
  return /\b(find|list|show|search|recall|use|choose|select)\b.{0,48}\bskills?\b/.test(normalized) || /\bskills?\b.{0,48}\b(for|to|that|which|about)\b/.test(normalized);
10856
12007
  }
10857
- async function printExactMemoryMatches(config, ov, query, options) {
12008
+ async function collectExactMemoryMatches(config, ov, query, options) {
10858
12009
  const terms = exactRecallTerms(query);
10859
12010
  if (terms.length === 0) {
10860
- return void 0;
12011
+ return [];
10861
12012
  }
10862
12013
  const scopes = exactMemoryScopes(config, options.includeArchived, query, options.project);
10863
12014
  const grepArgs = (term, scope) => withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5", "--output", "json"]);
@@ -10865,19 +12016,12 @@ async function printExactMemoryMatches(config, ov, query, options) {
10865
12016
  const planned = terms.flatMap((term) => scopes.map((scope) => formatShellCommand(ov, grepArgs(term, scope))));
10866
12017
  console.log("\nExact memory/resource matches:");
10867
12018
  console.log(planned.join("\n"));
10868
- return planned.join("\n");
12019
+ return [];
10869
12020
  }
10870
- const matches = await collectExactMatches(terms, scopes, async (term, scope) => {
12021
+ return collectExactMatches(terms, scopes, async (term, scope) => {
10871
12022
  const result = await runCommand(ov, grepArgs(term, scope), { allowFailure: true });
10872
12023
  return result.exitCode === 0 ? result.stdout : void 0;
10873
12024
  });
10874
- const text = formatExactMatchPointers(matches);
10875
- if (!text) {
10876
- return void 0;
10877
- }
10878
- console.log(`
10879
- ${text}`);
10880
- return text;
10881
12025
  }
10882
12026
  async function storeMemory(config, options) {
10883
12027
  if (options.replaceUri) {
@@ -12292,7 +13436,7 @@ function parseGithubRelease(value) {
12292
13436
  }
12293
13437
  function titleFromReleaseName(name, version) {
12294
13438
  const trimmed = name.trim();
12295
- const withoutPrefix = trimmed.replace(new RegExp(`^v?${escapeRegExp2(version)}:\\s*`, "i"), "");
13439
+ const withoutPrefix = trimmed.replace(new RegExp(`^v?${escapeRegExp3(version)}:\\s*`, "i"), "");
12296
13440
  return withoutPrefix || trimmed || "Release notes";
12297
13441
  }
12298
13442
  function bodyLines(body) {
@@ -12310,7 +13454,7 @@ function bodyLines(body) {
12310
13454
  }
12311
13455
  return out;
12312
13456
  }
12313
- function escapeRegExp2(value) {
13457
+ function escapeRegExp3(value) {
12314
13458
  return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
12315
13459
  }
12316
13460
 
@@ -15343,12 +16487,18 @@ async function main() {
15343
16487
  ).action(async (uri, options) => {
15344
16488
  await runSharePublish(getRuntimeConfig(program2), uri, options);
15345
16489
  });
15346
- 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(
15347
16491
  "--redact",
15348
16492
  "Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
15349
16493
  ).action(async (path, options) => {
15350
16494
  await runSharePublishArtifact(getRuntimeConfig(program2), path, options);
15351
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
+ });
15352
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) => {
15353
16503
  await runShareInstallArtifacts(getRuntimeConfig(program2), options);
15354
16504
  });