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.
@@ -8457,6 +8457,9 @@ var import_node_path6 = require("node:path");
8457
8457
  var TEAMS_FILE_VERSION = 1;
8458
8458
  var SHARED_SEGMENT = "shared";
8459
8459
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
8460
+ var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
8461
+ var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
8462
+ var ARTIFACT_INSTALL_METADATA_VERSION = 1;
8460
8463
  var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
8461
8464
  var DEFAULT_GIT_REMOTE_NAME = "origin";
8462
8465
  var SCRUBBER_PATTERNS = [
@@ -8548,7 +8551,7 @@ async function runShareInit(config, remoteUrl, options) {
8548
8551
  if (!dryRun) {
8549
8552
  await ensureSharedGitignore(worktree, git, options.push !== false);
8550
8553
  const ingested = await ingestWorktreeFiles(config, newConfig, "create");
8551
- console.log(`Ingested ${ingested} shared memory file(s) into OpenViking.`);
8554
+ console.log(`Ingested ${ingested} shared file(s) into OpenViking.`);
8552
8555
  }
8553
8556
  }
8554
8557
  var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
@@ -8875,7 +8878,7 @@ async function runShareSyncQuiet(config, state, options) {
8875
8878
  return void 0;
8876
8879
  }
8877
8880
  async function stageShareableChanges(dryRun, git, worktree) {
8878
- const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_MEMORY_KIND_DIRS.map((dir) => `:(top)${dir}`)];
8881
+ const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_TOP_LEVEL_DIRS.map((dir) => `:(top)${dir}`)];
8879
8882
  await maybeRun(dryRun, git, ["-C", worktree, "add", "--", ...pathspecs], { allowFailure: true });
8880
8883
  }
8881
8884
  async function publishShareGitChange(worktree, relativePath, commitMessage, options = {}) {
@@ -8997,6 +9000,157 @@ ${err instanceof Error ? err.message : String(err)}`,
8997
9000
  }
8998
9001
  console.log(`Published ${sourceUri} -> ${targetUri}`);
8999
9002
  }
9003
+ async function runSharePublishArtifact(config, sourcePath, options) {
9004
+ const result = await shareAgentArtifact(config, sourcePath, options);
9005
+ printShareArtifactResult(result, options.preview === true);
9006
+ }
9007
+ async function shareAgentArtifact(config, sourcePath, options) {
9008
+ const team = await resolveTeam(config, options.team);
9009
+ const dryRun = options.dryRun === true;
9010
+ const preview = options.preview === true;
9011
+ const resolvedSourcePath = expandPath(sourcePath);
9012
+ if (!await isRegularFileNoSymlink(resolvedSourcePath)) {
9013
+ throw new Error(`Agent artifact source is not a regular file: ${resolvedSourcePath}`);
9014
+ }
9015
+ const artifact = inferShareArtifact(resolvedSourcePath, options);
9016
+ const rawContent = await (0, import_promises5.readFile)(resolvedSourcePath, "utf8");
9017
+ if (!rawContent.trim()) {
9018
+ throw new Error(`Refusing to share empty agent artifact: ${resolvedSourcePath}`);
9019
+ }
9020
+ const scrub = applyScrubber(rawContent, { redact: options.redact === true });
9021
+ const relativePath = sharedArtifactRelativePath(artifact);
9022
+ const targetPath = (0, import_node_path6.join)(team.config.worktree, ...relativePath.split("/"));
9023
+ const targetUri = workfileToVikingUri(config, team.config, targetPath);
9024
+ const messages = [
9025
+ `${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} ${artifact.kind} ${artifact.agent}/${artifact.name}`,
9026
+ `Source: ${portablePath(resolvedSourcePath)}`,
9027
+ `Destination: ${targetUri}`
9028
+ ];
9029
+ if (preview) {
9030
+ if (scrub.blocker) {
9031
+ messages.push(`PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or pass --redact.`);
9032
+ return {
9033
+ artifact,
9034
+ gitMessages: [],
9035
+ messages,
9036
+ sourcePath: resolvedSourcePath,
9037
+ targetPath,
9038
+ targetUri
9039
+ };
9040
+ }
9041
+ for (const redaction of scrub.redactions) {
9042
+ messages.push(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
9043
+ }
9044
+ return {
9045
+ artifact,
9046
+ gitMessages: [],
9047
+ messages,
9048
+ previewContent: scrub.cleaned,
9049
+ sourcePath: resolvedSourcePath,
9050
+ targetPath,
9051
+ targetUri
9052
+ };
9053
+ }
9054
+ if (scrub.blocker) {
9055
+ throw new Error(
9056
+ `Refusing to share ${resolvedSourcePath}: possible ${scrub.blocker}. Strip the sensitive value or pass --redact for soft-leak patterns.`
9057
+ );
9058
+ }
9059
+ for (const redaction of scrub.redactions) {
9060
+ messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before sharing.`);
9061
+ }
9062
+ const content = scrub.cleaned;
9063
+ const existingContent = await readFileIfExists(targetPath) ?? void 0;
9064
+ if (existingContent !== void 0 && existingContent !== content && options.force !== true) {
9065
+ throw new Error(
9066
+ `Shared artifact already exists with different content: ${portablePath(targetPath)}. Pass --force to replace it.`
9067
+ );
9068
+ }
9069
+ if (dryRun) {
9070
+ messages.push(`Would write shared artifact: ${portablePath(targetPath)}`);
9071
+ }
9072
+ const ov = await openVikingCliForMode(dryRun);
9073
+ const ovHasResource = !dryRun && await vikingResourceExists(ov, config, targetUri);
9074
+ await ensureSharedDirectoryChain(config, ov, targetUri, dryRun, { quiet: true });
9075
+ await writeMemoryFile(config, ov, targetUri, content, ovHasResource ? "replace" : "create", dryRun, { quiet: true });
9076
+ const message = options.message ?? `share: publish ${relativePath}`;
9077
+ const gitMessages = await publishShareGitChange(team.config.worktree, relativePath, message, {
9078
+ dryRun,
9079
+ push: options.push
9080
+ });
9081
+ return { artifact, gitMessages, messages, sourcePath: resolvedSourcePath, targetPath, targetUri };
9082
+ }
9083
+ async function runShareInstallArtifacts(config, options) {
9084
+ const result = await installSharedAgentArtifacts(config, options);
9085
+ if (result.syncedTeams.length > 0) {
9086
+ console.log(`Synced shared teams: ${result.syncedTeams.join(", ")}`);
9087
+ }
9088
+ for (const warning2 of result.warnings) {
9089
+ console.warn(`Warning: ${warning2}`);
9090
+ }
9091
+ for (const message of result.messages) {
9092
+ console.log(message);
9093
+ }
9094
+ }
9095
+ async function installSharedAgentArtifacts(config, options) {
9096
+ const syncResult = await maybeSyncSharedArtifacts(config, options);
9097
+ const team = await resolveTeam(config, options.team);
9098
+ const dryRun = options.dryRun === true || options.apply !== true;
9099
+ const allArtifacts = await collectSharedArtifacts(team.config.worktree, team.name);
9100
+ const artifacts = filterSharedArtifacts(allArtifacts, options);
9101
+ const messages = [];
9102
+ if (artifacts.length === 0) {
9103
+ const filters = sharedArtifactFilterLabel(options);
9104
+ if (filters) {
9105
+ throw new Error(`No shared agent artifacts found for team "${team.name}" matching ${filters}.`);
9106
+ }
9107
+ return {
9108
+ installedCount: 0,
9109
+ messages: [`No shared agent artifacts found for team "${team.name}".`],
9110
+ syncedTeams: syncResult.syncedTeams,
9111
+ team: team.name,
9112
+ warnings: syncResult.warnings
9113
+ };
9114
+ }
9115
+ if (options.name !== void 0 && artifacts.length > 1 && (options.agent === void 0 || options.kind === void 0)) {
9116
+ throw new Error(
9117
+ `Shared artifact "${options.name}" is ambiguous. Specify agent and kind. Matches: ${artifacts.map((artifact) => sharedArtifactLabel(artifact.artifact)).join(", ")}`
9118
+ );
9119
+ }
9120
+ let installedCount = 0;
9121
+ for (const artifact of artifacts) {
9122
+ const label = sharedArtifactLabel(artifact.artifact);
9123
+ const state = await sharedArtifactInstallState(artifact);
9124
+ if (dryRun) {
9125
+ const verb = sharedArtifactDryRunVerb(state.status, options.force === true);
9126
+ const suffix = sharedArtifactDryRunSuffix(state.status, options.force === true);
9127
+ messages.push(`${verb} ${label}: ${portablePath(artifact.installPath)}${suffix}`);
9128
+ continue;
9129
+ }
9130
+ if ((state.status === "local_modified" || state.status === "remote_changed_and_local_modified") && options.force !== true) {
9131
+ throw new Error(`Refusing to overwrite ${portablePath(artifact.installPath)}. Pass force=true or --force.`);
9132
+ }
9133
+ if (state.status === "current") {
9134
+ await writeSharedArtifactMetadata(artifact, state.sourceSha);
9135
+ messages.push(`Already installed ${label}: ${portablePath(artifact.installPath)}`);
9136
+ continue;
9137
+ }
9138
+ await ensureDirectory((0, import_node_path6.dirname)(artifact.installPath), false);
9139
+ await (0, import_promises5.writeFile)(artifact.installPath, state.sourceContent, { encoding: "utf8", mode: 384 });
9140
+ await writeSharedArtifactMetadata(artifact, state.sourceSha);
9141
+ installedCount += 1;
9142
+ messages.push(
9143
+ `${sharedArtifactInstallVerb(state.status, options.force === true)} ${label}: ${portablePath(artifact.installPath)}`
9144
+ );
9145
+ }
9146
+ return {
9147
+ installedCount,
9148
+ messages,
9149
+ syncedTeams: syncResult.syncedTeams,
9150
+ team: team.name,
9151
+ warnings: syncResult.warnings
9152
+ };
9153
+ }
9000
9154
  async function runShareUnpublish(config, sourceUri, options) {
9001
9155
  assertVikingUri(sourceUri);
9002
9156
  const team = await resolveTeam(config, options.team);
@@ -9089,7 +9243,7 @@ async function runShareRename(config, options) {
9089
9243
  console.log(`Would rename worktree: ${portablePath(oldTeam.config.worktree)} -> ${portablePath(newWorktree)}`);
9090
9244
  console.log(`Would rename gitdir: ${portablePath(oldTeam.config.gitdir)} -> ${portablePath(newGitdir)}`);
9091
9245
  console.log(`Would update git core.worktree for team "${newName}".`);
9092
- console.log(`Would reindex shared memories under team "${newName}" and remove old shared URI tree.`);
9246
+ console.log(`Would reindex shared context under team "${newName}" and remove old shared URI tree.`);
9093
9247
  console.log(`Would write teams file: ${teamsFilePath(config)}`);
9094
9248
  return;
9095
9249
  }
@@ -9107,7 +9261,7 @@ async function runShareRename(config, options) {
9107
9261
  false
9108
9262
  );
9109
9263
  console.log(`Renamed shared team "${oldTeam.name}" -> "${newName}".`);
9110
- console.log(`Reindexed ${ingested} shared memory file(s).`);
9264
+ console.log(`Reindexed ${ingested} shared file(s).`);
9111
9265
  }
9112
9266
  async function runShareSetUrl(config, remoteUrl, options) {
9113
9267
  if (!remoteUrl.trim()) {
@@ -9350,7 +9504,7 @@ async function walkMemoryFiles(root) {
9350
9504
  }
9351
9505
  const full = (0, import_node_path6.join)(path, entry.name);
9352
9506
  if (entry.isDirectory()) {
9353
- if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
9507
+ if (depth === 0 && !SHAREABLE_TOP_LEVEL_DIRS.includes(entry.name)) {
9354
9508
  continue;
9355
9509
  }
9356
9510
  await visit(full, depth + 1);
@@ -9432,6 +9586,264 @@ function vikingUriToWorktreeRelative(config, uri, team) {
9432
9586
  }
9433
9587
  return uri.slice(prefix.length);
9434
9588
  }
9589
+ async function isRegularFileNoSymlink(path) {
9590
+ try {
9591
+ const stat5 = await (0, import_promises5.lstat)(path);
9592
+ return stat5.isFile();
9593
+ } catch (_err) {
9594
+ return false;
9595
+ }
9596
+ }
9597
+ function inferShareArtifact(path, options) {
9598
+ const normalizedPath = path.split(import_node_path6.sep).join("/");
9599
+ const fileName = (0, import_node_path6.basename)(path);
9600
+ const lowerFileName = fileName.toLowerCase();
9601
+ const lowerPath = normalizedPath.toLowerCase();
9602
+ const inferredKind = lowerFileName === "skill.md" ? "skill" : lowerPath.includes("/.claude/commands/") && lowerFileName.endsWith(".md") ? "command" : void 0;
9603
+ const inferredAgent = lowerPath.includes("/.codex/skills/") ? "codex" : lowerPath.includes("/.claude/skills/") || lowerPath.includes("/.claude/commands/") ? "claude" : void 0;
9604
+ const extensionIndex = fileName.lastIndexOf(".");
9605
+ const stem = extensionIndex > 0 ? fileName.slice(0, extensionIndex) : fileName;
9606
+ const inferredName = lowerFileName === "skill.md" ? (0, import_node_path6.basename)((0, import_node_path6.dirname)(path)) : stem;
9607
+ const kind = options.kind ?? inferredKind;
9608
+ const agent = options.agent ?? inferredAgent;
9609
+ const name = options.name ?? inferredName;
9610
+ if (kind !== "skill" && kind !== "command") {
9611
+ throw new Error("Could not infer artifact kind. Pass --kind skill or --kind command.");
9612
+ }
9613
+ if (agent !== "codex" && agent !== "claude") {
9614
+ throw new Error("Could not infer artifact agent. Pass --agent codex or --agent claude.");
9615
+ }
9616
+ if (kind === "skill" && lowerFileName !== "skill.md") {
9617
+ throw new Error("Skill artifacts must point at a SKILL.md file.");
9618
+ }
9619
+ if (kind === "command" && !lowerFileName.endsWith(".md")) {
9620
+ throw new Error("Command artifacts must be Markdown files.");
9621
+ }
9622
+ if (kind === "command" && agent !== "claude") {
9623
+ throw new Error("Only Claude command artifacts are supported.");
9624
+ }
9625
+ if (name.trim().length === 0) {
9626
+ throw new Error("Artifact name cannot be empty.");
9627
+ }
9628
+ return { agent, kind, name: uriSegment(name) };
9629
+ }
9630
+ function sharedArtifactRelativePath(artifact) {
9631
+ if (artifact.kind === "skill") {
9632
+ return `${SHAREABLE_ARTIFACT_DIR}/skills/${artifact.agent}/${artifact.name}/SKILL.md`;
9633
+ }
9634
+ return `${SHAREABLE_ARTIFACT_DIR}/commands/${artifact.agent}/${artifact.name}.md`;
9635
+ }
9636
+ function sharedArtifactFromRelativePath(relativePath) {
9637
+ const parts = relativePath.split("/");
9638
+ if (parts[0] !== SHAREABLE_ARTIFACT_DIR) {
9639
+ return void 0;
9640
+ }
9641
+ if (parts.length === 5 && parts[1] === "skills" && (parts[2] === "codex" || parts[2] === "claude") && parts[4] === "SKILL.md") {
9642
+ return { agent: parts[2], kind: "skill", name: parts[3] };
9643
+ }
9644
+ if (parts.length === 4 && parts[1] === "commands" && parts[2] === "claude" && parts[3].endsWith(".md")) {
9645
+ return { agent: "claude", kind: "command", name: parts[3].slice(0, -".md".length) };
9646
+ }
9647
+ return void 0;
9648
+ }
9649
+ async function collectSharedArtifacts(worktree, team) {
9650
+ const root = (0, import_node_path6.join)(worktree, SHAREABLE_ARTIFACT_DIR);
9651
+ if (!await isDirectory(root)) {
9652
+ return [];
9653
+ }
9654
+ const out = [];
9655
+ async function visit(path) {
9656
+ const entries = await (0, import_promises5.readdir)(path, { withFileTypes: true });
9657
+ for (const entry of entries) {
9658
+ const full = (0, import_node_path6.join)(path, entry.name);
9659
+ if (entry.isDirectory()) {
9660
+ await visit(full);
9661
+ continue;
9662
+ }
9663
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
9664
+ continue;
9665
+ }
9666
+ const relativePath = (0, import_node_path6.relative)(worktree, full).split(import_node_path6.sep).join("/");
9667
+ const artifact = sharedArtifactFromRelativePath(relativePath);
9668
+ if (artifact === void 0) {
9669
+ continue;
9670
+ }
9671
+ out.push({
9672
+ artifact,
9673
+ installPath: sharedArtifactInstallPath(team, artifact),
9674
+ sourcePath: full,
9675
+ sourceRelativePath: relativePath,
9676
+ team
9677
+ });
9678
+ }
9679
+ }
9680
+ await visit(root);
9681
+ return out.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
9682
+ }
9683
+ function filterSharedArtifacts(artifacts, options) {
9684
+ const name = options.name === void 0 ? void 0 : uriSegment(options.name);
9685
+ return artifacts.filter((artifact) => {
9686
+ if (options.agent !== void 0 && artifact.artifact.agent !== options.agent) {
9687
+ return false;
9688
+ }
9689
+ if (options.kind !== void 0 && artifact.artifact.kind !== options.kind) {
9690
+ return false;
9691
+ }
9692
+ if (name !== void 0 && artifact.artifact.name !== name) {
9693
+ return false;
9694
+ }
9695
+ return true;
9696
+ });
9697
+ }
9698
+ async function maybeSyncSharedArtifacts(config, options) {
9699
+ if (options.sync === false) {
9700
+ return { syncedTeams: [], warnings: [] };
9701
+ }
9702
+ return syncSharedReposBeforeAgentRead(config);
9703
+ }
9704
+ async function sharedArtifactInstallState(artifact) {
9705
+ const sourceContent = await (0, import_promises5.readFile)(artifact.sourcePath, "utf8");
9706
+ const sourceSha = sha256(sourceContent);
9707
+ const existingContent = await readFileIfExists(artifact.installPath) ?? void 0;
9708
+ if (existingContent === void 0) {
9709
+ return { sourceContent, sourceSha, status: "not_installed" };
9710
+ }
9711
+ const existingSha = sha256(existingContent);
9712
+ const metadata = await readSharedArtifactMetadata(artifact);
9713
+ if (existingSha === sourceSha) {
9714
+ return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "current" };
9715
+ }
9716
+ if (metadata === void 0) {
9717
+ return { existingContent, existingSha, sourceContent, sourceSha, status: "local_modified" };
9718
+ }
9719
+ const remoteChanged = metadata.sourceSha256 !== sourceSha;
9720
+ const localChanged = metadata.installedSha256 !== existingSha;
9721
+ if (remoteChanged && localChanged) {
9722
+ return {
9723
+ existingContent,
9724
+ existingSha,
9725
+ metadata,
9726
+ sourceContent,
9727
+ sourceSha,
9728
+ status: "remote_changed_and_local_modified"
9729
+ };
9730
+ }
9731
+ if (remoteChanged) {
9732
+ return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "update_available" };
9733
+ }
9734
+ return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "local_modified" };
9735
+ }
9736
+ async function readSharedArtifactMetadata(artifact) {
9737
+ const raw = await readFileIfExists(sharedArtifactMetadataPath(artifact));
9738
+ if (raw === void 0) {
9739
+ return void 0;
9740
+ }
9741
+ const parsed = parseJsonConfigObject(raw);
9742
+ if (parsed === void 0 || parsed.version !== ARTIFACT_INSTALL_METADATA_VERSION) {
9743
+ return void 0;
9744
+ }
9745
+ const artifactValue = parsed.artifact;
9746
+ 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)) {
9747
+ return void 0;
9748
+ }
9749
+ const metadataArtifact = artifactValue;
9750
+ if (metadataArtifact.agent !== artifact.artifact.agent || metadataArtifact.kind !== artifact.artifact.kind || metadataArtifact.name !== artifact.artifact.name) {
9751
+ return void 0;
9752
+ }
9753
+ return {
9754
+ artifact: artifact.artifact,
9755
+ installedAt: parsed.installedAt,
9756
+ installedSha256: parsed.installedSha256,
9757
+ source: parsed.source,
9758
+ sourceSha256: parsed.sourceSha256,
9759
+ team: parsed.team,
9760
+ version: ARTIFACT_INSTALL_METADATA_VERSION
9761
+ };
9762
+ }
9763
+ async function writeSharedArtifactMetadata(artifact, sourceSha) {
9764
+ const metadata = {
9765
+ artifact: artifact.artifact,
9766
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
9767
+ installedSha256: sourceSha,
9768
+ source: artifact.sourceRelativePath,
9769
+ sourceSha256: sourceSha,
9770
+ team: artifact.team,
9771
+ version: ARTIFACT_INSTALL_METADATA_VERSION
9772
+ };
9773
+ const metadataPath = sharedArtifactMetadataPath(artifact);
9774
+ await ensureDirectory((0, import_node_path6.dirname)(metadataPath), false);
9775
+ await (0, import_promises5.writeFile)(metadataPath, `${JSON.stringify(metadata, void 0, 2)}
9776
+ `, { encoding: "utf8", mode: 384 });
9777
+ }
9778
+ function sharedArtifactMetadataPath(artifact) {
9779
+ return `${artifact.installPath}.threadnote-install.json`;
9780
+ }
9781
+ function sharedArtifactDryRunVerb(status, force) {
9782
+ switch (status) {
9783
+ case "not_installed":
9784
+ return "Would install";
9785
+ case "current":
9786
+ return "Already installed";
9787
+ case "update_available":
9788
+ return "Would update";
9789
+ case "local_modified":
9790
+ case "remote_changed_and_local_modified":
9791
+ return force ? "Would replace" : "Would skip modified";
9792
+ }
9793
+ }
9794
+ function sharedArtifactDryRunSuffix(status, force) {
9795
+ if ((status === "local_modified" || status === "remote_changed_and_local_modified") && !force) {
9796
+ return " (pass --force to replace local changes)";
9797
+ }
9798
+ return "";
9799
+ }
9800
+ function sharedArtifactInstallVerb(status, force) {
9801
+ if (force && (status === "local_modified" || status === "remote_changed_and_local_modified")) {
9802
+ return "Replaced";
9803
+ }
9804
+ if (status === "update_available") {
9805
+ return "Updated";
9806
+ }
9807
+ return "Installed";
9808
+ }
9809
+ function sharedArtifactFilterLabel(options) {
9810
+ const filters = [];
9811
+ if (options.kind !== void 0) {
9812
+ filters.push(`kind=${options.kind}`);
9813
+ }
9814
+ if (options.agent !== void 0) {
9815
+ filters.push(`agent=${options.agent}`);
9816
+ }
9817
+ if (options.name !== void 0) {
9818
+ filters.push(`name=${uriSegment(options.name)}`);
9819
+ }
9820
+ return filters.join(", ");
9821
+ }
9822
+ function sharedArtifactLabel(artifact) {
9823
+ return `${artifact.kind} ${artifact.agent}/${artifact.name}`;
9824
+ }
9825
+ function sharedArtifactInstallPath(team, artifact) {
9826
+ if (artifact.kind === "skill" && artifact.agent === "codex") {
9827
+ return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".codex", "skills", "threadnote", team, artifact.name, "SKILL.md");
9828
+ }
9829
+ if (artifact.kind === "skill" && artifact.agent === "claude") {
9830
+ return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".claude", "skills", "threadnote", team, artifact.name, "SKILL.md");
9831
+ }
9832
+ return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".claude", "commands", "threadnote", team, `${artifact.name}.md`);
9833
+ }
9834
+ function printShareArtifactResult(result, preview) {
9835
+ for (const message of result.messages) {
9836
+ console.log(message);
9837
+ }
9838
+ for (const gitMessage of result.gitMessages) {
9839
+ console.log(gitMessage);
9840
+ }
9841
+ if (preview && result.previewContent !== void 0) {
9842
+ console.log("-----BEGIN PREVIEW-----");
9843
+ console.log(result.previewContent);
9844
+ console.log("-----END PREVIEW-----");
9845
+ }
9846
+ }
9435
9847
  function stripPersonalProvenance(content) {
9436
9848
  const lines = content.split("\n");
9437
9849
  let headerEnd = lines.length;
@@ -9480,9 +9892,9 @@ async function ensureSharedDirectoryChain(config, ov, memoryUri, dryRun, options
9480
9892
  continue;
9481
9893
  }
9482
9894
  if (options.quiet === true) {
9483
- await runCommand(ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared memories."]));
9895
+ await runCommand(ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared context."]));
9484
9896
  } else {
9485
- await maybeRun(false, ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared memories."]));
9897
+ await maybeRun(false, ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared context."]));
9486
9898
  }
9487
9899
  }
9488
9900
  }
@@ -9711,7 +10123,7 @@ async function applyChangesToOpenViking(config, team, changes, options = {}) {
9711
10123
  continue;
9712
10124
  }
9713
10125
  const firstSegment = change.relativePath.split("/")[0];
9714
- if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
10126
+ if (!SHAREABLE_TOP_LEVEL_DIRS.includes(firstSegment)) {
9715
10127
  continue;
9716
10128
  }
9717
10129
  const uri = workfileToVikingUri(config, team, change.path);
@@ -14821,6 +15233,15 @@ async function main() {
14821
15233
  ).action(async (uri, options) => {
14822
15234
  await runSharePublish(getRuntimeConfig(program2), uri, options);
14823
15235
  });
15236
+ 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(
15237
+ "--redact",
15238
+ "Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
15239
+ ).action(async (path, options) => {
15240
+ await runSharePublishArtifact(getRuntimeConfig(program2), path, options);
15241
+ });
15242
+ 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) => {
15243
+ await runShareInstallArtifacts(getRuntimeConfig(program2), options);
15244
+ });
14824
15245
  share.command("unpublish").description("Pull a shared memory back into the personal namespace, commit removal and push").argument("<viking-uri>", "viking:// memory URI inside a team shared subtree").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").action(async (uri, options) => {
14825
15246
  await runShareUnpublish(getRuntimeConfig(program2), uri, options);
14826
15247
  });
@@ -119,11 +119,12 @@ compact_context({"project":"my-repo","topic":"active-bug","dryRun":true})
119
119
 
120
120
  Never compact secrets, credentials, customer data, raw production logs, or checked-in canonical docs into memory.
121
121
 
122
- ## Sharing memories with teammates
122
+ ## Sharing context with teammates
123
123
 
124
- Threadnote can publish a curated subset of durable memories into a team git repo so other engineers' agents can pull them.
125
- The mechanism lives under the `viking://user/<you>/memories/shared/<team>/...` subtree; only memories that are explicitly
126
- published leave the machine. Personal handoffs, preferences, and unpublished durable notes always stay local.
124
+ Threadnote can publish a curated subset of durable memories and agent artifacts (Codex/Claude skills and Claude
125
+ commands) into a team git repo so other engineers' agents can pull them. The mechanism lives under the
126
+ `viking://user/<you>/memories/shared/<team>/...` subtree; only content that is explicitly published leaves the machine.
127
+ Personal handoffs, preferences, and unpublished durable notes always stay local.
127
128
 
128
129
  Publish a durable memory when its content is useful to other engineers working on the same project (intended behavior,
129
130
  design decisions, API contracts, gotchas) and is safe to share. Do NOT publish:
@@ -136,6 +137,16 @@ The MCP tool `share_publish` runs the same scrubber as the CLI and refuses to pu
136
137
  patterns (PEM private keys, `sk-...`, `gh[pousr]_...`, `Bearer ...`, `AKIA...`, `xox[abprs]-...`). It writes and pushes
137
138
  the shared copy first, then removes the personal copy after the push succeeds.
138
139
 
140
+ The MCP tool `share_skill` shares a local Codex/Claude `SKILL.md` file or Claude command markdown file into the team's
141
+ `agent-artifacts/` catalog after the same scrubber checks. Shared artifacts are recallable after sync, but local
142
+ installation remains opt-in with `threadnote share install-artifacts --apply`.
143
+
144
+ When the user asks whether shared skills exist, call `list_shared_skills`. When the user asks to install one, call
145
+ `install_shared_skill` with the selected `name`; include `agent` and `kind` from the list result when available.
146
+ `list_shared_skills` syncs shared repos before listing and reports `not_installed`, `current`, `update_available`,
147
+ `local_modified`, or `remote_changed_and_local_modified`. Install/update selected shared skills only when the user asks;
148
+ use `force:true` only when the user explicitly accepts overwriting local modifications.
149
+
139
150
  Incoming shared memories are normally fetched and synced automatically before MCP `recall_context` / `read_context` and
140
151
  CLI `threadnote recall` / `threadnote read` return. If automatic sync reports a dirty worktree, a conflict, or another
141
152
  git issue, run `threadnote share sync` after resolving the local state to pull, reindex, and push explicitly.
@@ -144,9 +155,13 @@ git issue, run `threadnote share sync` after resolving the local state to pull,
144
155
  # MCP call shape
145
156
  share_publish({"uri":"viking://user/you/memories/durable/projects/foo/bar.md"})
146
157
  share_publish({"uri":"viking://user/you/memories/durable/projects/foo/bar.md","team":"friends","push":false})
158
+ share_skill({"path":"~/.codex/skills/reviewer/SKILL.md"})
159
+ list_shared_skills({})
160
+ install_shared_skill({"name":"reviewer","agent":"codex","kind":"skill"})
147
161
  ```
148
162
 
149
- Before publishing, confirm with the user unless they have already instructed you to share durable memories autonomously.
163
+ Before publishing, confirm with the user unless they have already instructed you to share durable memories or agent
164
+ artifacts autonomously.
150
165
 
151
166
  ## Handoff
152
167
 
@@ -184,5 +199,7 @@ threadnote forget viking://user/example/memories/events/duplicate.md
184
199
  threadnote handoff --project example --topic active-issue --task "short task summary" --tests "checks run" --next-step "what to do next"
185
200
  threadnote share init git@github.com:org/team-memories.git
186
201
  threadnote share publish viking://user/example/memories/durable/projects/foo/bar.md
202
+ threadnote share publish-artifact ~/.codex/skills/example/SKILL.md
203
+ threadnote share install-artifacts --name example --agent codex --kind skill --apply
187
204
  threadnote share sync
188
205
  ```
package/docs/index.html CHANGED
@@ -783,7 +783,7 @@
783
783
  team-shared decisions — recallable from any session, on any agent, in any worktree.
784
784
  </p>
785
785
  <div class="cta-row">
786
- <span class="pill"><span id="version-tag">v0.6.1</span> · local-first · GPL-3.0-or-later</span>
786
+ <span class="pill"><span id="version-tag">v0.6.1</span> · local-first · AGPL-3.0-or-later</span>
787
787
  </div>
788
788
  </div>
789
789
  <div class="scroll-cue">scroll ↓ &nbsp;·&nbsp; <span class="kbd">↓</span> / <span class="kbd">j</span></div>
@@ -1585,7 +1585,7 @@ threadnote doctor --dry-run</code></pre>
1585
1585
  </div>
1586
1586
 
1587
1587
  <p class="colophon">
1588
- threadnote · GPL-3.0-or-later · built on OpenViking 0.3.24 · use <span class="kbd">↑</span>
1588
+ threadnote · AGPL-3.0-or-later · built on OpenViking 0.3.24 · use <span class="kbd">↑</span>
1589
1589
  <span class="kbd">↓</span> / <span class="kbd">j</span> <span class="kbd">k</span> to navigate
1590
1590
  </p>
1591
1591
  </div>