threadnote 1.1.2 → 1.1.3

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.
@@ -35953,6 +35953,13 @@ async function isFile(path) {
35953
35953
  return false;
35954
35954
  }
35955
35955
  }
35956
+ async function isDirectory(path) {
35957
+ try {
35958
+ return (await (0, import_promises.stat)(path)).isDirectory();
35959
+ } catch (_err) {
35960
+ return false;
35961
+ }
35962
+ }
35956
35963
  async function readFileIfExists(path) {
35957
35964
  try {
35958
35965
  return await (0, import_promises.readFile)(path, "utf8");
@@ -35976,6 +35983,17 @@ function expandPath(path) {
35976
35983
  }
35977
35984
  return (0, import_node_path.isAbsolute)(path) ? path : (0, import_node_path.resolve)(getInvocationCwd(), path);
35978
35985
  }
35986
+ function portablePath(path) {
35987
+ const home = (0, import_node_os.homedir)();
35988
+ const resolvedPath = (0, import_node_path.resolve)(path);
35989
+ if (resolvedPath === home) {
35990
+ return "~";
35991
+ }
35992
+ if (resolvedPath.startsWith(`${home}${import_node_path.sep}`)) {
35993
+ return `~/${resolvedPath.slice(home.length + 1).split(import_node_path.sep).join("/")}`;
35994
+ }
35995
+ return resolvedPath;
35996
+ }
35979
35997
  function getInvocationCwd() {
35980
35998
  return process.env.THREADNOTE_CALLER_CWD ?? process.cwd();
35981
35999
  }
@@ -37082,6 +37100,9 @@ var import_node_path4 = require("node:path");
37082
37100
  var TEAMS_FILE_VERSION = 1;
37083
37101
  var SHARED_SEGMENT = "shared";
37084
37102
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
37103
+ var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
37104
+ var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
37105
+ var ARTIFACT_INSTALL_METADATA_VERSION = 1;
37085
37106
  var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
37086
37107
  var DEFAULT_GIT_REMOTE_NAME = "origin";
37087
37108
  var SCRUBBER_PATTERNS = [
@@ -37401,6 +37422,155 @@ async function runGitCommand(dryRun, git, args, failureLabel) {
37401
37422
  }
37402
37423
  return result;
37403
37424
  }
37425
+ async function shareAgentArtifact(config2, sourcePath, options) {
37426
+ const team = await resolveTeam(config2, options.team);
37427
+ const dryRun = options.dryRun === true;
37428
+ const preview = options.preview === true;
37429
+ const resolvedSourcePath = expandPath(sourcePath);
37430
+ if (!await isRegularFileNoSymlink(resolvedSourcePath)) {
37431
+ throw new Error(`Agent artifact source is not a regular file: ${resolvedSourcePath}`);
37432
+ }
37433
+ const artifact = inferShareArtifact(resolvedSourcePath, options);
37434
+ const rawContent = await (0, import_promises4.readFile)(resolvedSourcePath, "utf8");
37435
+ if (!rawContent.trim()) {
37436
+ throw new Error(`Refusing to share empty agent artifact: ${resolvedSourcePath}`);
37437
+ }
37438
+ const scrub = applyScrubber(rawContent, { redact: options.redact === true });
37439
+ const relativePath = sharedArtifactRelativePath(artifact);
37440
+ const targetPath = (0, import_node_path4.join)(team.config.worktree, ...relativePath.split("/"));
37441
+ const targetUri = workfileToVikingUri(config2, team.config, targetPath);
37442
+ const messages = [
37443
+ `${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} ${artifact.kind} ${artifact.agent}/${artifact.name}`,
37444
+ `Source: ${portablePath(resolvedSourcePath)}`,
37445
+ `Destination: ${targetUri}`
37446
+ ];
37447
+ if (preview) {
37448
+ if (scrub.blocker) {
37449
+ messages.push(`PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or pass --redact.`);
37450
+ return {
37451
+ artifact,
37452
+ gitMessages: [],
37453
+ messages,
37454
+ sourcePath: resolvedSourcePath,
37455
+ targetPath,
37456
+ targetUri
37457
+ };
37458
+ }
37459
+ for (const redaction of scrub.redactions) {
37460
+ messages.push(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
37461
+ }
37462
+ return {
37463
+ artifact,
37464
+ gitMessages: [],
37465
+ messages,
37466
+ previewContent: scrub.cleaned,
37467
+ sourcePath: resolvedSourcePath,
37468
+ targetPath,
37469
+ targetUri
37470
+ };
37471
+ }
37472
+ if (scrub.blocker) {
37473
+ throw new Error(
37474
+ `Refusing to share ${resolvedSourcePath}: possible ${scrub.blocker}. Strip the sensitive value or pass --redact for soft-leak patterns.`
37475
+ );
37476
+ }
37477
+ for (const redaction of scrub.redactions) {
37478
+ messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before sharing.`);
37479
+ }
37480
+ const content = scrub.cleaned;
37481
+ const existingContent = await readFileIfExists(targetPath) ?? void 0;
37482
+ if (existingContent !== void 0 && existingContent !== content && options.force !== true) {
37483
+ throw new Error(
37484
+ `Shared artifact already exists with different content: ${portablePath(targetPath)}. Pass --force to replace it.`
37485
+ );
37486
+ }
37487
+ if (dryRun) {
37488
+ messages.push(`Would write shared artifact: ${portablePath(targetPath)}`);
37489
+ }
37490
+ const ov = await openVikingCliForMode(dryRun);
37491
+ const ovHasResource = !dryRun && await vikingResourceExists(ov, config2, targetUri);
37492
+ await ensureSharedDirectoryChain(config2, ov, targetUri, dryRun, { quiet: true });
37493
+ await writeMemoryFile(config2, ov, targetUri, content, ovHasResource ? "replace" : "create", dryRun, { quiet: true });
37494
+ const message = options.message ?? `share: publish ${relativePath}`;
37495
+ const gitMessages = await publishShareGitChange(team.config.worktree, relativePath, message, {
37496
+ dryRun,
37497
+ push: options.push
37498
+ });
37499
+ return { artifact, gitMessages, messages, sourcePath: resolvedSourcePath, targetPath, targetUri };
37500
+ }
37501
+ async function listSharedAgentArtifacts(config2, options = {}) {
37502
+ const syncResult = await maybeSyncSharedArtifacts(config2, options);
37503
+ const team = await resolveTeam(config2, options.team);
37504
+ const artifacts = filterSharedArtifacts(await collectSharedArtifacts(team.config.worktree, team.name), options);
37505
+ const summaries = [];
37506
+ for (const artifact of artifacts) {
37507
+ summaries.push({
37508
+ ...artifact,
37509
+ installStatus: await sharedArtifactInstallStatus(artifact),
37510
+ metadataPath: sharedArtifactMetadataPath(artifact)
37511
+ });
37512
+ }
37513
+ return { artifacts: summaries, syncedTeams: syncResult.syncedTeams, team: team.name, warnings: syncResult.warnings };
37514
+ }
37515
+ async function installSharedAgentArtifacts(config2, options) {
37516
+ const syncResult = await maybeSyncSharedArtifacts(config2, options);
37517
+ const team = await resolveTeam(config2, options.team);
37518
+ const dryRun = options.dryRun === true || options.apply !== true;
37519
+ const allArtifacts = await collectSharedArtifacts(team.config.worktree, team.name);
37520
+ const artifacts = filterSharedArtifacts(allArtifacts, options);
37521
+ const messages = [];
37522
+ if (artifacts.length === 0) {
37523
+ const filters = sharedArtifactFilterLabel(options);
37524
+ if (filters) {
37525
+ throw new Error(`No shared agent artifacts found for team "${team.name}" matching ${filters}.`);
37526
+ }
37527
+ return {
37528
+ installedCount: 0,
37529
+ messages: [`No shared agent artifacts found for team "${team.name}".`],
37530
+ syncedTeams: syncResult.syncedTeams,
37531
+ team: team.name,
37532
+ warnings: syncResult.warnings
37533
+ };
37534
+ }
37535
+ if (options.name !== void 0 && artifacts.length > 1 && (options.agent === void 0 || options.kind === void 0)) {
37536
+ throw new Error(
37537
+ `Shared artifact "${options.name}" is ambiguous. Specify agent and kind. Matches: ${artifacts.map((artifact) => sharedArtifactLabel(artifact.artifact)).join(", ")}`
37538
+ );
37539
+ }
37540
+ let installedCount = 0;
37541
+ for (const artifact of artifacts) {
37542
+ const label = sharedArtifactLabel(artifact.artifact);
37543
+ const state = await sharedArtifactInstallState(artifact);
37544
+ if (dryRun) {
37545
+ const verb = sharedArtifactDryRunVerb(state.status, options.force === true);
37546
+ const suffix = sharedArtifactDryRunSuffix(state.status, options.force === true);
37547
+ messages.push(`${verb} ${label}: ${portablePath(artifact.installPath)}${suffix}`);
37548
+ continue;
37549
+ }
37550
+ if ((state.status === "local_modified" || state.status === "remote_changed_and_local_modified") && options.force !== true) {
37551
+ throw new Error(`Refusing to overwrite ${portablePath(artifact.installPath)}. Pass force=true or --force.`);
37552
+ }
37553
+ if (state.status === "current") {
37554
+ await writeSharedArtifactMetadata(artifact, state.sourceSha);
37555
+ messages.push(`Already installed ${label}: ${portablePath(artifact.installPath)}`);
37556
+ continue;
37557
+ }
37558
+ await ensureDirectory((0, import_node_path4.dirname)(artifact.installPath), false);
37559
+ await (0, import_promises4.writeFile)(artifact.installPath, state.sourceContent, { encoding: "utf8", mode: 384 });
37560
+ await writeSharedArtifactMetadata(artifact, state.sourceSha);
37561
+ installedCount += 1;
37562
+ messages.push(
37563
+ `${sharedArtifactInstallVerb(state.status, options.force === true)} ${label}: ${portablePath(artifact.installPath)}`
37564
+ );
37565
+ }
37566
+ return {
37567
+ installedCount,
37568
+ messages,
37569
+ syncedTeams: syncResult.syncedTeams,
37570
+ team: team.name,
37571
+ warnings: syncResult.warnings
37572
+ };
37573
+ }
37404
37574
  function normalizeTeamName(input) {
37405
37575
  const candidate = (input ?? "default").trim();
37406
37576
  if (!candidate) {
@@ -37535,6 +37705,254 @@ function vikingUriToWorktreeRelative(config2, uri, team) {
37535
37705
  }
37536
37706
  return uri.slice(prefix.length);
37537
37707
  }
37708
+ async function isRegularFileNoSymlink(path) {
37709
+ try {
37710
+ const stat2 = await (0, import_promises4.lstat)(path);
37711
+ return stat2.isFile();
37712
+ } catch (_err) {
37713
+ return false;
37714
+ }
37715
+ }
37716
+ function inferShareArtifact(path, options) {
37717
+ const normalizedPath = path.split(import_node_path4.sep).join("/");
37718
+ const fileName = (0, import_node_path4.basename)(path);
37719
+ const lowerFileName = fileName.toLowerCase();
37720
+ const lowerPath = normalizedPath.toLowerCase();
37721
+ const inferredKind = lowerFileName === "skill.md" ? "skill" : lowerPath.includes("/.claude/commands/") && lowerFileName.endsWith(".md") ? "command" : void 0;
37722
+ const inferredAgent = lowerPath.includes("/.codex/skills/") ? "codex" : lowerPath.includes("/.claude/skills/") || lowerPath.includes("/.claude/commands/") ? "claude" : void 0;
37723
+ const extensionIndex = fileName.lastIndexOf(".");
37724
+ const stem = extensionIndex > 0 ? fileName.slice(0, extensionIndex) : fileName;
37725
+ const inferredName = lowerFileName === "skill.md" ? (0, import_node_path4.basename)((0, import_node_path4.dirname)(path)) : stem;
37726
+ const kind = options.kind ?? inferredKind;
37727
+ const agent = options.agent ?? inferredAgent;
37728
+ const name = options.name ?? inferredName;
37729
+ if (kind !== "skill" && kind !== "command") {
37730
+ throw new Error("Could not infer artifact kind. Pass --kind skill or --kind command.");
37731
+ }
37732
+ if (agent !== "codex" && agent !== "claude") {
37733
+ throw new Error("Could not infer artifact agent. Pass --agent codex or --agent claude.");
37734
+ }
37735
+ if (kind === "skill" && lowerFileName !== "skill.md") {
37736
+ throw new Error("Skill artifacts must point at a SKILL.md file.");
37737
+ }
37738
+ if (kind === "command" && !lowerFileName.endsWith(".md")) {
37739
+ throw new Error("Command artifacts must be Markdown files.");
37740
+ }
37741
+ if (kind === "command" && agent !== "claude") {
37742
+ throw new Error("Only Claude command artifacts are supported.");
37743
+ }
37744
+ if (name.trim().length === 0) {
37745
+ throw new Error("Artifact name cannot be empty.");
37746
+ }
37747
+ return { agent, kind, name: uriSegment(name) };
37748
+ }
37749
+ function sharedArtifactRelativePath(artifact) {
37750
+ if (artifact.kind === "skill") {
37751
+ return `${SHAREABLE_ARTIFACT_DIR}/skills/${artifact.agent}/${artifact.name}/SKILL.md`;
37752
+ }
37753
+ return `${SHAREABLE_ARTIFACT_DIR}/commands/${artifact.agent}/${artifact.name}.md`;
37754
+ }
37755
+ function sharedArtifactFromRelativePath(relativePath) {
37756
+ const parts = relativePath.split("/");
37757
+ if (parts[0] !== SHAREABLE_ARTIFACT_DIR) {
37758
+ return void 0;
37759
+ }
37760
+ if (parts.length === 5 && parts[1] === "skills" && (parts[2] === "codex" || parts[2] === "claude") && parts[4] === "SKILL.md") {
37761
+ return { agent: parts[2], kind: "skill", name: parts[3] };
37762
+ }
37763
+ if (parts.length === 4 && parts[1] === "commands" && parts[2] === "claude" && parts[3].endsWith(".md")) {
37764
+ return { agent: "claude", kind: "command", name: parts[3].slice(0, -".md".length) };
37765
+ }
37766
+ return void 0;
37767
+ }
37768
+ async function collectSharedArtifacts(worktree, team) {
37769
+ const root = (0, import_node_path4.join)(worktree, SHAREABLE_ARTIFACT_DIR);
37770
+ if (!await isDirectory(root)) {
37771
+ return [];
37772
+ }
37773
+ const out = [];
37774
+ async function visit(path) {
37775
+ const entries = await (0, import_promises4.readdir)(path, { withFileTypes: true });
37776
+ for (const entry of entries) {
37777
+ const full = (0, import_node_path4.join)(path, entry.name);
37778
+ if (entry.isDirectory()) {
37779
+ await visit(full);
37780
+ continue;
37781
+ }
37782
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
37783
+ continue;
37784
+ }
37785
+ const relativePath = (0, import_node_path4.relative)(worktree, full).split(import_node_path4.sep).join("/");
37786
+ const artifact = sharedArtifactFromRelativePath(relativePath);
37787
+ if (artifact === void 0) {
37788
+ continue;
37789
+ }
37790
+ out.push({
37791
+ artifact,
37792
+ installPath: sharedArtifactInstallPath(team, artifact),
37793
+ sourcePath: full,
37794
+ sourceRelativePath: relativePath,
37795
+ team
37796
+ });
37797
+ }
37798
+ }
37799
+ await visit(root);
37800
+ return out.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
37801
+ }
37802
+ function filterSharedArtifacts(artifacts, options) {
37803
+ const name = options.name === void 0 ? void 0 : uriSegment(options.name);
37804
+ return artifacts.filter((artifact) => {
37805
+ if (options.agent !== void 0 && artifact.artifact.agent !== options.agent) {
37806
+ return false;
37807
+ }
37808
+ if (options.kind !== void 0 && artifact.artifact.kind !== options.kind) {
37809
+ return false;
37810
+ }
37811
+ if (name !== void 0 && artifact.artifact.name !== name) {
37812
+ return false;
37813
+ }
37814
+ return true;
37815
+ });
37816
+ }
37817
+ async function maybeSyncSharedArtifacts(config2, options) {
37818
+ if (options.sync === false) {
37819
+ return { syncedTeams: [], warnings: [] };
37820
+ }
37821
+ return syncSharedReposBeforeAgentRead(config2);
37822
+ }
37823
+ async function sharedArtifactInstallStatus(artifact) {
37824
+ return (await sharedArtifactInstallState(artifact)).status;
37825
+ }
37826
+ async function sharedArtifactInstallState(artifact) {
37827
+ const sourceContent = await (0, import_promises4.readFile)(artifact.sourcePath, "utf8");
37828
+ const sourceSha = sha256(sourceContent);
37829
+ const existingContent = await readFileIfExists(artifact.installPath) ?? void 0;
37830
+ if (existingContent === void 0) {
37831
+ return { sourceContent, sourceSha, status: "not_installed" };
37832
+ }
37833
+ const existingSha = sha256(existingContent);
37834
+ const metadata = await readSharedArtifactMetadata(artifact);
37835
+ if (existingSha === sourceSha) {
37836
+ return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "current" };
37837
+ }
37838
+ if (metadata === void 0) {
37839
+ return { existingContent, existingSha, sourceContent, sourceSha, status: "local_modified" };
37840
+ }
37841
+ const remoteChanged = metadata.sourceSha256 !== sourceSha;
37842
+ const localChanged = metadata.installedSha256 !== existingSha;
37843
+ if (remoteChanged && localChanged) {
37844
+ return {
37845
+ existingContent,
37846
+ existingSha,
37847
+ metadata,
37848
+ sourceContent,
37849
+ sourceSha,
37850
+ status: "remote_changed_and_local_modified"
37851
+ };
37852
+ }
37853
+ if (remoteChanged) {
37854
+ return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "update_available" };
37855
+ }
37856
+ return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "local_modified" };
37857
+ }
37858
+ async function readSharedArtifactMetadata(artifact) {
37859
+ const raw = await readFileIfExists(sharedArtifactMetadataPath(artifact));
37860
+ if (raw === void 0) {
37861
+ return void 0;
37862
+ }
37863
+ const parsed = parseJsonConfigObject(raw);
37864
+ if (parsed === void 0 || parsed.version !== ARTIFACT_INSTALL_METADATA_VERSION) {
37865
+ return void 0;
37866
+ }
37867
+ const artifactValue = parsed.artifact;
37868
+ if (typeof parsed.team !== "string" || typeof parsed.source !== "string" || typeof parsed.sourceSha256 !== "string" || typeof parsed.installedSha256 !== "string" || typeof parsed.installedAt !== "string" || typeof artifactValue !== "object" || artifactValue === null || Array.isArray(artifactValue)) {
37869
+ return void 0;
37870
+ }
37871
+ const metadataArtifact = artifactValue;
37872
+ if (metadataArtifact.agent !== artifact.artifact.agent || metadataArtifact.kind !== artifact.artifact.kind || metadataArtifact.name !== artifact.artifact.name) {
37873
+ return void 0;
37874
+ }
37875
+ return {
37876
+ artifact: artifact.artifact,
37877
+ installedAt: parsed.installedAt,
37878
+ installedSha256: parsed.installedSha256,
37879
+ source: parsed.source,
37880
+ sourceSha256: parsed.sourceSha256,
37881
+ team: parsed.team,
37882
+ version: ARTIFACT_INSTALL_METADATA_VERSION
37883
+ };
37884
+ }
37885
+ async function writeSharedArtifactMetadata(artifact, sourceSha) {
37886
+ const metadata = {
37887
+ artifact: artifact.artifact,
37888
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
37889
+ installedSha256: sourceSha,
37890
+ source: artifact.sourceRelativePath,
37891
+ sourceSha256: sourceSha,
37892
+ team: artifact.team,
37893
+ version: ARTIFACT_INSTALL_METADATA_VERSION
37894
+ };
37895
+ const metadataPath = sharedArtifactMetadataPath(artifact);
37896
+ await ensureDirectory((0, import_node_path4.dirname)(metadataPath), false);
37897
+ await (0, import_promises4.writeFile)(metadataPath, `${JSON.stringify(metadata, void 0, 2)}
37898
+ `, { encoding: "utf8", mode: 384 });
37899
+ }
37900
+ function sharedArtifactMetadataPath(artifact) {
37901
+ return `${artifact.installPath}.threadnote-install.json`;
37902
+ }
37903
+ function sharedArtifactDryRunVerb(status, force) {
37904
+ switch (status) {
37905
+ case "not_installed":
37906
+ return "Would install";
37907
+ case "current":
37908
+ return "Already installed";
37909
+ case "update_available":
37910
+ return "Would update";
37911
+ case "local_modified":
37912
+ case "remote_changed_and_local_modified":
37913
+ return force ? "Would replace" : "Would skip modified";
37914
+ }
37915
+ }
37916
+ function sharedArtifactDryRunSuffix(status, force) {
37917
+ if ((status === "local_modified" || status === "remote_changed_and_local_modified") && !force) {
37918
+ return " (pass --force to replace local changes)";
37919
+ }
37920
+ return "";
37921
+ }
37922
+ function sharedArtifactInstallVerb(status, force) {
37923
+ if (force && (status === "local_modified" || status === "remote_changed_and_local_modified")) {
37924
+ return "Replaced";
37925
+ }
37926
+ if (status === "update_available") {
37927
+ return "Updated";
37928
+ }
37929
+ return "Installed";
37930
+ }
37931
+ function sharedArtifactFilterLabel(options) {
37932
+ const filters = [];
37933
+ if (options.kind !== void 0) {
37934
+ filters.push(`kind=${options.kind}`);
37935
+ }
37936
+ if (options.agent !== void 0) {
37937
+ filters.push(`agent=${options.agent}`);
37938
+ }
37939
+ if (options.name !== void 0) {
37940
+ filters.push(`name=${uriSegment(options.name)}`);
37941
+ }
37942
+ return filters.join(", ");
37943
+ }
37944
+ function sharedArtifactLabel(artifact) {
37945
+ return `${artifact.kind} ${artifact.agent}/${artifact.name}`;
37946
+ }
37947
+ function sharedArtifactInstallPath(team, artifact) {
37948
+ if (artifact.kind === "skill" && artifact.agent === "codex") {
37949
+ return (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".codex", "skills", "threadnote", team, artifact.name, "SKILL.md");
37950
+ }
37951
+ if (artifact.kind === "skill" && artifact.agent === "claude") {
37952
+ return (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".claude", "skills", "threadnote", team, artifact.name, "SKILL.md");
37953
+ }
37954
+ return (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".claude", "commands", "threadnote", team, `${artifact.name}.md`);
37955
+ }
37538
37956
  function stripPersonalProvenance(content) {
37539
37957
  const lines = content.split("\n");
37540
37958
  let headerEnd = lines.length;
@@ -37571,9 +37989,9 @@ async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun, option
37571
37989
  continue;
37572
37990
  }
37573
37991
  if (options.quiet === true) {
37574
- await runCommand(ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
37992
+ await runCommand(ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared context."]));
37575
37993
  } else {
37576
- await maybeRun(false, ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
37994
+ await maybeRun(false, ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared context."]));
37577
37995
  }
37578
37996
  }
37579
37997
  }
@@ -37802,7 +38220,7 @@ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
37802
38220
  continue;
37803
38221
  }
37804
38222
  const firstSegment = change.relativePath.split("/")[0];
37805
- if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
38223
+ if (!SHAREABLE_TOP_LEVEL_DIRS.includes(firstSegment)) {
37806
38224
  continue;
37807
38225
  }
37808
38226
  const uri = workfileToVikingUri(config2, team, change.path);
@@ -37879,6 +38297,8 @@ async function main() {
37879
38297
  "When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
37880
38298
  "Use compact_context with dryRun=true for scoped memory hygiene when recall surfaces overlapping active memories.",
37881
38299
  "To share a durable memory with teammates, call `share_publish` with its viking:// URI. share_publish scrubs for secrets, writes and pushes the shared copy first, then removes the personal copy after the push succeeds. Do not publish handoffs, preferences, or anything carrying machine-local paths or in-flight task context.",
38300
+ "To share a local Codex/Claude skill or Claude command with teammates, call `share_skill` with the local file path. It publishes into the shared artifact catalog after the same scrubber checks.",
38301
+ "To use a team shared skill as a native local skill, call `list_shared_skills` first, then `install_shared_skill` for the selected name/agent/kind.",
37882
38302
  "Do not store secrets, customer data, raw production logs, or credentials."
37883
38303
  ].join("\n")
37884
38304
  }
@@ -38079,6 +38499,80 @@ function registerTools(server, config2) {
38079
38499
  return runSharePublishTool(config2, checkedUri.value, { message, preview, push, redact, team });
38080
38500
  }
38081
38501
  );
38502
+ server.registerTool(
38503
+ "share_skill",
38504
+ {
38505
+ annotations: { readOnlyHint: false, destructiveHint: true },
38506
+ description: "Publish a local Codex/Claude skill or Claude command markdown file into a team's shared artifact catalog. Path inference handles ~/.codex/skills/**/SKILL.md, ~/.claude/skills/**/SKILL.md, and ~/.claude/commands/**/*.md; pass agent/kind/name when sharing from another path. Default team is used unless team is provided. Pass preview=true to inspect bytes without writing or committing.",
38507
+ inputSchema: {
38508
+ agent: external_exports.enum(["codex", "claude"]).optional().describe("Agent owner when path inference is ambiguous"),
38509
+ force: external_exports.boolean().optional().describe("Replace an existing shared artifact with different content"),
38510
+ kind: external_exports.enum(["skill", "command"]).optional().describe("Artifact kind when path inference is ambiguous"),
38511
+ message: external_exports.string().optional().describe('Commit message override; defaults to "share: publish <path>"'),
38512
+ name: external_exports.string().optional().describe("Shared artifact name; defaults to skill directory or command file stem"),
38513
+ path: external_exports.string().optional().describe("Required local path to SKILL.md or a Claude command markdown file"),
38514
+ preview: external_exports.boolean().optional().describe("Return the bytes that would land in the shared git repo"),
38515
+ push: external_exports.boolean().optional().describe("Push to remote after committing; defaults to true"),
38516
+ redact: external_exports.boolean().optional().describe("Replace soft-leak matches (local paths) with placeholders and continue; credentials still block."),
38517
+ team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
38518
+ }
38519
+ },
38520
+ async ({ agent, force, kind, message, name, path, preview, push, redact, team }) => {
38521
+ const checkedPath = requiredText(path, "share_skill", "path", {
38522
+ path: "~/.codex/skills/example/SKILL.md"
38523
+ });
38524
+ if (!checkedPath.ok) {
38525
+ return checkedPath.error;
38526
+ }
38527
+ return runShareSkillTool(config2, checkedPath.value, {
38528
+ agent,
38529
+ force,
38530
+ kind,
38531
+ message,
38532
+ name,
38533
+ preview,
38534
+ push,
38535
+ redact,
38536
+ team
38537
+ });
38538
+ }
38539
+ );
38540
+ server.registerTool(
38541
+ "list_shared_skills",
38542
+ {
38543
+ annotations: { readOnlyHint: true, destructiveHint: false },
38544
+ description: "List shared Codex/Claude skills and Claude commands available in a configured Threadnote team repo, including whether each one is already installed locally.",
38545
+ inputSchema: {
38546
+ agent: external_exports.enum(["codex", "claude"]).optional().describe("Optional agent filter"),
38547
+ kind: external_exports.enum(["skill", "command"]).optional().describe("Optional kind filter"),
38548
+ name: external_exports.string().optional().describe("Optional shared artifact name filter"),
38549
+ team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
38550
+ }
38551
+ },
38552
+ async ({ agent, kind, name, team }) => runListSharedSkillsTool(config2, { agent, kind, name, team })
38553
+ );
38554
+ server.registerTool(
38555
+ "install_shared_skill",
38556
+ {
38557
+ annotations: { readOnlyHint: false, destructiveHint: true },
38558
+ description: "Install one shared Codex/Claude skill or Claude command from a configured Threadnote team repo into the local agent skill/command directory. Use list_shared_skills first to find names and disambiguate agent/kind.",
38559
+ inputSchema: {
38560
+ agent: external_exports.enum(["codex", "claude"]).optional().describe("Agent owner; required when name is ambiguous"),
38561
+ dryRun: external_exports.boolean().optional().describe("Preview install without writing local files"),
38562
+ force: external_exports.boolean().optional().describe("Replace an existing installed artifact with different content"),
38563
+ kind: external_exports.enum(["skill", "command"]).optional().describe("Artifact kind; required when name is ambiguous"),
38564
+ name: external_exports.string().optional().describe("Required shared artifact name to install"),
38565
+ team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
38566
+ }
38567
+ },
38568
+ async ({ agent, dryRun, force, kind, name, team }) => {
38569
+ const checkedName = requiredText(name, "install_shared_skill", "name", { name: "reviewer" });
38570
+ if (!checkedName.ok) {
38571
+ return checkedName.error;
38572
+ }
38573
+ return runInstallSharedSkillTool(config2, checkedName.value, { agent, dryRun, force, kind, team });
38574
+ }
38575
+ );
38082
38576
  }
38083
38577
  function registerOpenVikingParityTools(server, config2) {
38084
38578
  server.registerTool(
@@ -39439,6 +39933,75 @@ async function runSharePublishTool(config2, sourceUri, options) {
39439
39933
  return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
39440
39934
  }
39441
39935
  }
39936
+ async function runShareSkillTool(config2, sourcePath, options) {
39937
+ try {
39938
+ const result = await shareAgentArtifact(config2, sourcePath, options);
39939
+ const lines = [...result.messages, ...result.gitMessages];
39940
+ if (result.previewContent !== void 0) {
39941
+ lines.push("-----BEGIN PREVIEW-----");
39942
+ lines.push(result.previewContent);
39943
+ lines.push("-----END PREVIEW-----");
39944
+ }
39945
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
39946
+ } catch (err) {
39947
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
39948
+ }
39949
+ }
39950
+ async function runListSharedSkillsTool(config2, options) {
39951
+ try {
39952
+ const result = await listSharedAgentArtifacts(config2, options);
39953
+ if (result.artifacts.length === 0) {
39954
+ const lines2 = shareArtifactToolHeader(result.team, result.syncedTeams, result.warnings);
39955
+ lines2.push(`No shared skills or commands found for team "${result.team}".`);
39956
+ return { content: [{ type: "text", text: lines2.join("\n") }] };
39957
+ }
39958
+ const lines = shareArtifactToolHeader(result.team, result.syncedTeams, result.warnings);
39959
+ lines.push(`Shared skills and commands for team "${result.team}":`);
39960
+ for (const artifact of result.artifacts) {
39961
+ lines.push(
39962
+ `- ${artifact.artifact.kind} ${artifact.artifact.agent}/${artifact.artifact.name} (${artifact.installStatus})`
39963
+ );
39964
+ lines.push(
39965
+ ` install: install_shared_skill({"name":"${artifact.artifact.name}","agent":"${artifact.artifact.agent}","kind":"${artifact.artifact.kind}"})`
39966
+ );
39967
+ }
39968
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
39969
+ } catch (err) {
39970
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
39971
+ }
39972
+ }
39973
+ async function runInstallSharedSkillTool(config2, name, options) {
39974
+ try {
39975
+ const result = await installSharedAgentArtifacts(config2, {
39976
+ ...options,
39977
+ apply: options.dryRun !== true,
39978
+ name
39979
+ });
39980
+ return {
39981
+ content: [
39982
+ {
39983
+ type: "text",
39984
+ text: [...shareArtifactToolHeader(result.team, result.syncedTeams, result.warnings), ...result.messages].join(
39985
+ "\n"
39986
+ )
39987
+ }
39988
+ ],
39989
+ isError: false
39990
+ };
39991
+ } catch (err) {
39992
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
39993
+ }
39994
+ }
39995
+ function shareArtifactToolHeader(team, syncedTeams, warnings) {
39996
+ const lines = [`Team: ${team}`];
39997
+ if (syncedTeams.length > 0) {
39998
+ lines.push(`Synced shared teams: ${syncedTeams.join(", ")}`);
39999
+ }
40000
+ for (const warning2 of warnings) {
40001
+ lines.push(`Warning: ${warning2}`);
40002
+ }
40003
+ return lines;
40004
+ }
39442
40005
  function withIdentity2(config2, args) {
39443
40006
  return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
39444
40007
  }