threadnote 1.1.2 → 1.1.4

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.
@@ -33081,7 +33081,6 @@ var StreamableHTTPClientTransport = class {
33081
33081
 
33082
33082
  // src/mcp_server.ts
33083
33083
  var import_promises5 = require("node:fs/promises");
33084
- var import_node_os3 = require("node:os");
33085
33084
  var import_node_path5 = require("node:path");
33086
33085
 
33087
33086
  // src/constants.ts
@@ -35724,6 +35723,7 @@ var jsYaml = {
35724
35723
  // src/utils.ts
35725
35724
  var import_node_child_process = require("node:child_process");
35726
35725
  var import_node_crypto = require("node:crypto");
35726
+ var import_node_fs = require("node:fs");
35727
35727
  var import_promises = require("node:fs/promises");
35728
35728
  var import_node_os = require("node:os");
35729
35729
  var import_node_path = require("node:path");
@@ -35778,18 +35778,57 @@ function redactText(content) {
35778
35778
  ).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh_[REDACTED]");
35779
35779
  }
35780
35780
  async function requiredOpenVikingCli() {
35781
- const command2 = await findExecutable(["ov", "openviking"]);
35781
+ const command2 = await findOpenVikingCli();
35782
35782
  if (!command2) {
35783
- throw new Error("Neither ov nor openviking was found in PATH. Run threadnote install first.");
35783
+ throw new Error(
35784
+ "Neither ov nor openviking was found in PATH, uv tool bin dir, $UV_TOOL_BIN_DIR, or ~/.local/bin. Run threadnote install first."
35785
+ );
35784
35786
  }
35785
35787
  return command2;
35786
35788
  }
35787
35789
  async function openVikingCliForMode(dryRun) {
35788
35790
  if (dryRun) {
35789
- return await findExecutable(["ov", "openviking"]) ?? "ov";
35791
+ return await findOpenVikingCli() ?? "ov";
35790
35792
  }
35791
35793
  return requiredOpenVikingCli();
35792
35794
  }
35795
+ async function findOpenVikingCli() {
35796
+ const override = process.env.THREADNOTE_OV?.trim();
35797
+ if (override) {
35798
+ return override;
35799
+ }
35800
+ const onPath = await findExecutable(["ov", "openviking"]);
35801
+ if (onPath) {
35802
+ return onPath;
35803
+ }
35804
+ for (const candidateDir of await openVikingToolCandidateDirs()) {
35805
+ for (const command2 of ["ov", "openviking"]) {
35806
+ const candidate = (0, import_node_path.join)(candidateDir, command2);
35807
+ if (await isExecutable(candidate)) {
35808
+ return candidate;
35809
+ }
35810
+ }
35811
+ }
35812
+ return void 0;
35813
+ }
35814
+ async function openVikingToolCandidateDirs() {
35815
+ const dirs = [];
35816
+ const uv = await findExecutable(["uv"]);
35817
+ if (uv) {
35818
+ const result = await runCommand(uv, ["tool", "dir", "--bin"], { allowFailure: true });
35819
+ if (result.exitCode === 0) {
35820
+ const dir = result.stdout.trim();
35821
+ if (dir) {
35822
+ dirs.push(dir);
35823
+ }
35824
+ }
35825
+ }
35826
+ if (process.env.UV_TOOL_BIN_DIR) {
35827
+ dirs.push(process.env.UV_TOOL_BIN_DIR);
35828
+ }
35829
+ dirs.push((0, import_node_path.join)((0, import_node_os.homedir)(), ".local", "bin"));
35830
+ return Array.from(new Set(dirs));
35831
+ }
35793
35832
  async function requiredExecutable(command2) {
35794
35833
  const executable = await findExecutable([command2]);
35795
35834
  if (!executable) {
@@ -35946,6 +35985,14 @@ async function exists(path) {
35946
35985
  return false;
35947
35986
  }
35948
35987
  }
35988
+ async function isExecutable(path) {
35989
+ try {
35990
+ await (0, import_promises.access)(path, import_node_fs.constants.X_OK);
35991
+ return true;
35992
+ } catch (_err) {
35993
+ return false;
35994
+ }
35995
+ }
35949
35996
  async function isFile(path) {
35950
35997
  try {
35951
35998
  return (await (0, import_promises.stat)(path)).isFile();
@@ -35953,6 +36000,13 @@ async function isFile(path) {
35953
36000
  return false;
35954
36001
  }
35955
36002
  }
36003
+ async function isDirectory(path) {
36004
+ try {
36005
+ return (await (0, import_promises.stat)(path)).isDirectory();
36006
+ } catch (_err) {
36007
+ return false;
36008
+ }
36009
+ }
35956
36010
  async function readFileIfExists(path) {
35957
36011
  try {
35958
36012
  return await (0, import_promises.readFile)(path, "utf8");
@@ -35976,6 +36030,17 @@ function expandPath(path) {
35976
36030
  }
35977
36031
  return (0, import_node_path.isAbsolute)(path) ? path : (0, import_node_path.resolve)(getInvocationCwd(), path);
35978
36032
  }
36033
+ function portablePath(path) {
36034
+ const home = (0, import_node_os.homedir)();
36035
+ const resolvedPath = (0, import_node_path.resolve)(path);
36036
+ if (resolvedPath === home) {
36037
+ return "~";
36038
+ }
36039
+ if (resolvedPath.startsWith(`${home}${import_node_path.sep}`)) {
36040
+ return `~/${resolvedPath.slice(home.length + 1).split(import_node_path.sep).join("/")}`;
36041
+ }
36042
+ return resolvedPath;
36043
+ }
35979
36044
  function getInvocationCwd() {
35980
36045
  return process.env.THREADNOTE_CALLER_CWD ?? process.cwd();
35981
36046
  }
@@ -37082,6 +37147,9 @@ var import_node_path4 = require("node:path");
37082
37147
  var TEAMS_FILE_VERSION = 1;
37083
37148
  var SHARED_SEGMENT = "shared";
37084
37149
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
37150
+ var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
37151
+ var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
37152
+ var ARTIFACT_INSTALL_METADATA_VERSION = 1;
37085
37153
  var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
37086
37154
  var DEFAULT_GIT_REMOTE_NAME = "origin";
37087
37155
  var SCRUBBER_PATTERNS = [
@@ -37401,6 +37469,155 @@ async function runGitCommand(dryRun, git, args, failureLabel) {
37401
37469
  }
37402
37470
  return result;
37403
37471
  }
37472
+ async function shareAgentArtifact(config2, sourcePath, options) {
37473
+ const team = await resolveTeam(config2, options.team);
37474
+ const dryRun = options.dryRun === true;
37475
+ const preview = options.preview === true;
37476
+ const resolvedSourcePath = expandPath(sourcePath);
37477
+ if (!await isRegularFileNoSymlink(resolvedSourcePath)) {
37478
+ throw new Error(`Agent artifact source is not a regular file: ${resolvedSourcePath}`);
37479
+ }
37480
+ const artifact = inferShareArtifact(resolvedSourcePath, options);
37481
+ const rawContent = await (0, import_promises4.readFile)(resolvedSourcePath, "utf8");
37482
+ if (!rawContent.trim()) {
37483
+ throw new Error(`Refusing to share empty agent artifact: ${resolvedSourcePath}`);
37484
+ }
37485
+ const scrub = applyScrubber(rawContent, { redact: options.redact === true });
37486
+ const relativePath = sharedArtifactRelativePath(artifact);
37487
+ const targetPath = (0, import_node_path4.join)(team.config.worktree, ...relativePath.split("/"));
37488
+ const targetUri = workfileToVikingUri(config2, team.config, targetPath);
37489
+ const messages = [
37490
+ `${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} ${artifact.kind} ${artifact.agent}/${artifact.name}`,
37491
+ `Source: ${portablePath(resolvedSourcePath)}`,
37492
+ `Destination: ${targetUri}`
37493
+ ];
37494
+ if (preview) {
37495
+ if (scrub.blocker) {
37496
+ messages.push(`PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or pass --redact.`);
37497
+ return {
37498
+ artifact,
37499
+ gitMessages: [],
37500
+ messages,
37501
+ sourcePath: resolvedSourcePath,
37502
+ targetPath,
37503
+ targetUri
37504
+ };
37505
+ }
37506
+ for (const redaction of scrub.redactions) {
37507
+ messages.push(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
37508
+ }
37509
+ return {
37510
+ artifact,
37511
+ gitMessages: [],
37512
+ messages,
37513
+ previewContent: scrub.cleaned,
37514
+ sourcePath: resolvedSourcePath,
37515
+ targetPath,
37516
+ targetUri
37517
+ };
37518
+ }
37519
+ if (scrub.blocker) {
37520
+ throw new Error(
37521
+ `Refusing to share ${resolvedSourcePath}: possible ${scrub.blocker}. Strip the sensitive value or pass --redact for soft-leak patterns.`
37522
+ );
37523
+ }
37524
+ for (const redaction of scrub.redactions) {
37525
+ messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before sharing.`);
37526
+ }
37527
+ const content = scrub.cleaned;
37528
+ const existingContent = await readFileIfExists(targetPath) ?? void 0;
37529
+ if (existingContent !== void 0 && existingContent !== content && options.force !== true) {
37530
+ throw new Error(
37531
+ `Shared artifact already exists with different content: ${portablePath(targetPath)}. Pass --force to replace it.`
37532
+ );
37533
+ }
37534
+ if (dryRun) {
37535
+ messages.push(`Would write shared artifact: ${portablePath(targetPath)}`);
37536
+ }
37537
+ const ov = await openVikingCliForMode(dryRun);
37538
+ const ovHasResource = !dryRun && await vikingResourceExists(ov, config2, targetUri);
37539
+ await ensureSharedDirectoryChain(config2, ov, targetUri, dryRun, { quiet: true });
37540
+ await writeMemoryFile(config2, ov, targetUri, content, ovHasResource ? "replace" : "create", dryRun, { quiet: true });
37541
+ const message = options.message ?? `share: publish ${relativePath}`;
37542
+ const gitMessages = await publishShareGitChange(team.config.worktree, relativePath, message, {
37543
+ dryRun,
37544
+ push: options.push
37545
+ });
37546
+ return { artifact, gitMessages, messages, sourcePath: resolvedSourcePath, targetPath, targetUri };
37547
+ }
37548
+ async function listSharedAgentArtifacts(config2, options = {}) {
37549
+ const syncResult = await maybeSyncSharedArtifacts(config2, options);
37550
+ const team = await resolveTeam(config2, options.team);
37551
+ const artifacts = filterSharedArtifacts(await collectSharedArtifacts(team.config.worktree, team.name), options);
37552
+ const summaries = [];
37553
+ for (const artifact of artifacts) {
37554
+ summaries.push({
37555
+ ...artifact,
37556
+ installStatus: await sharedArtifactInstallStatus(artifact),
37557
+ metadataPath: sharedArtifactMetadataPath(artifact)
37558
+ });
37559
+ }
37560
+ return { artifacts: summaries, syncedTeams: syncResult.syncedTeams, team: team.name, warnings: syncResult.warnings };
37561
+ }
37562
+ async function installSharedAgentArtifacts(config2, options) {
37563
+ const syncResult = await maybeSyncSharedArtifacts(config2, options);
37564
+ const team = await resolveTeam(config2, options.team);
37565
+ const dryRun = options.dryRun === true || options.apply !== true;
37566
+ const allArtifacts = await collectSharedArtifacts(team.config.worktree, team.name);
37567
+ const artifacts = filterSharedArtifacts(allArtifacts, options);
37568
+ const messages = [];
37569
+ if (artifacts.length === 0) {
37570
+ const filters = sharedArtifactFilterLabel(options);
37571
+ if (filters) {
37572
+ throw new Error(`No shared agent artifacts found for team "${team.name}" matching ${filters}.`);
37573
+ }
37574
+ return {
37575
+ installedCount: 0,
37576
+ messages: [`No shared agent artifacts found for team "${team.name}".`],
37577
+ syncedTeams: syncResult.syncedTeams,
37578
+ team: team.name,
37579
+ warnings: syncResult.warnings
37580
+ };
37581
+ }
37582
+ if (options.name !== void 0 && artifacts.length > 1 && (options.agent === void 0 || options.kind === void 0)) {
37583
+ throw new Error(
37584
+ `Shared artifact "${options.name}" is ambiguous. Specify agent and kind. Matches: ${artifacts.map((artifact) => sharedArtifactLabel(artifact.artifact)).join(", ")}`
37585
+ );
37586
+ }
37587
+ let installedCount = 0;
37588
+ for (const artifact of artifacts) {
37589
+ const label = sharedArtifactLabel(artifact.artifact);
37590
+ const state = await sharedArtifactInstallState(artifact);
37591
+ if (dryRun) {
37592
+ const verb = sharedArtifactDryRunVerb(state.status, options.force === true);
37593
+ const suffix = sharedArtifactDryRunSuffix(state.status, options.force === true);
37594
+ messages.push(`${verb} ${label}: ${portablePath(artifact.installPath)}${suffix}`);
37595
+ continue;
37596
+ }
37597
+ if ((state.status === "local_modified" || state.status === "remote_changed_and_local_modified") && options.force !== true) {
37598
+ throw new Error(`Refusing to overwrite ${portablePath(artifact.installPath)}. Pass force=true or --force.`);
37599
+ }
37600
+ if (state.status === "current") {
37601
+ await writeSharedArtifactMetadata(artifact, state.sourceSha);
37602
+ messages.push(`Already installed ${label}: ${portablePath(artifact.installPath)}`);
37603
+ continue;
37604
+ }
37605
+ await ensureDirectory((0, import_node_path4.dirname)(artifact.installPath), false);
37606
+ await (0, import_promises4.writeFile)(artifact.installPath, state.sourceContent, { encoding: "utf8", mode: 384 });
37607
+ await writeSharedArtifactMetadata(artifact, state.sourceSha);
37608
+ installedCount += 1;
37609
+ messages.push(
37610
+ `${sharedArtifactInstallVerb(state.status, options.force === true)} ${label}: ${portablePath(artifact.installPath)}`
37611
+ );
37612
+ }
37613
+ return {
37614
+ installedCount,
37615
+ messages,
37616
+ syncedTeams: syncResult.syncedTeams,
37617
+ team: team.name,
37618
+ warnings: syncResult.warnings
37619
+ };
37620
+ }
37404
37621
  function normalizeTeamName(input) {
37405
37622
  const candidate = (input ?? "default").trim();
37406
37623
  if (!candidate) {
@@ -37535,6 +37752,254 @@ function vikingUriToWorktreeRelative(config2, uri, team) {
37535
37752
  }
37536
37753
  return uri.slice(prefix.length);
37537
37754
  }
37755
+ async function isRegularFileNoSymlink(path) {
37756
+ try {
37757
+ const stat2 = await (0, import_promises4.lstat)(path);
37758
+ return stat2.isFile();
37759
+ } catch (_err) {
37760
+ return false;
37761
+ }
37762
+ }
37763
+ function inferShareArtifact(path, options) {
37764
+ const normalizedPath = path.split(import_node_path4.sep).join("/");
37765
+ const fileName = (0, import_node_path4.basename)(path);
37766
+ const lowerFileName = fileName.toLowerCase();
37767
+ const lowerPath = normalizedPath.toLowerCase();
37768
+ const inferredKind = lowerFileName === "skill.md" ? "skill" : lowerPath.includes("/.claude/commands/") && lowerFileName.endsWith(".md") ? "command" : void 0;
37769
+ const inferredAgent = lowerPath.includes("/.codex/skills/") ? "codex" : lowerPath.includes("/.claude/skills/") || lowerPath.includes("/.claude/commands/") ? "claude" : void 0;
37770
+ const extensionIndex = fileName.lastIndexOf(".");
37771
+ const stem = extensionIndex > 0 ? fileName.slice(0, extensionIndex) : fileName;
37772
+ const inferredName = lowerFileName === "skill.md" ? (0, import_node_path4.basename)((0, import_node_path4.dirname)(path)) : stem;
37773
+ const kind = options.kind ?? inferredKind;
37774
+ const agent = options.agent ?? inferredAgent;
37775
+ const name = options.name ?? inferredName;
37776
+ if (kind !== "skill" && kind !== "command") {
37777
+ throw new Error("Could not infer artifact kind. Pass --kind skill or --kind command.");
37778
+ }
37779
+ if (agent !== "codex" && agent !== "claude") {
37780
+ throw new Error("Could not infer artifact agent. Pass --agent codex or --agent claude.");
37781
+ }
37782
+ if (kind === "skill" && lowerFileName !== "skill.md") {
37783
+ throw new Error("Skill artifacts must point at a SKILL.md file.");
37784
+ }
37785
+ if (kind === "command" && !lowerFileName.endsWith(".md")) {
37786
+ throw new Error("Command artifacts must be Markdown files.");
37787
+ }
37788
+ if (kind === "command" && agent !== "claude") {
37789
+ throw new Error("Only Claude command artifacts are supported.");
37790
+ }
37791
+ if (name.trim().length === 0) {
37792
+ throw new Error("Artifact name cannot be empty.");
37793
+ }
37794
+ return { agent, kind, name: uriSegment(name) };
37795
+ }
37796
+ function sharedArtifactRelativePath(artifact) {
37797
+ if (artifact.kind === "skill") {
37798
+ return `${SHAREABLE_ARTIFACT_DIR}/skills/${artifact.agent}/${artifact.name}/SKILL.md`;
37799
+ }
37800
+ return `${SHAREABLE_ARTIFACT_DIR}/commands/${artifact.agent}/${artifact.name}.md`;
37801
+ }
37802
+ function sharedArtifactFromRelativePath(relativePath) {
37803
+ const parts = relativePath.split("/");
37804
+ if (parts[0] !== SHAREABLE_ARTIFACT_DIR) {
37805
+ return void 0;
37806
+ }
37807
+ if (parts.length === 5 && parts[1] === "skills" && (parts[2] === "codex" || parts[2] === "claude") && parts[4] === "SKILL.md") {
37808
+ return { agent: parts[2], kind: "skill", name: parts[3] };
37809
+ }
37810
+ if (parts.length === 4 && parts[1] === "commands" && parts[2] === "claude" && parts[3].endsWith(".md")) {
37811
+ return { agent: "claude", kind: "command", name: parts[3].slice(0, -".md".length) };
37812
+ }
37813
+ return void 0;
37814
+ }
37815
+ async function collectSharedArtifacts(worktree, team) {
37816
+ const root = (0, import_node_path4.join)(worktree, SHAREABLE_ARTIFACT_DIR);
37817
+ if (!await isDirectory(root)) {
37818
+ return [];
37819
+ }
37820
+ const out = [];
37821
+ async function visit(path) {
37822
+ const entries = await (0, import_promises4.readdir)(path, { withFileTypes: true });
37823
+ for (const entry of entries) {
37824
+ const full = (0, import_node_path4.join)(path, entry.name);
37825
+ if (entry.isDirectory()) {
37826
+ await visit(full);
37827
+ continue;
37828
+ }
37829
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
37830
+ continue;
37831
+ }
37832
+ const relativePath = (0, import_node_path4.relative)(worktree, full).split(import_node_path4.sep).join("/");
37833
+ const artifact = sharedArtifactFromRelativePath(relativePath);
37834
+ if (artifact === void 0) {
37835
+ continue;
37836
+ }
37837
+ out.push({
37838
+ artifact,
37839
+ installPath: sharedArtifactInstallPath(team, artifact),
37840
+ sourcePath: full,
37841
+ sourceRelativePath: relativePath,
37842
+ team
37843
+ });
37844
+ }
37845
+ }
37846
+ await visit(root);
37847
+ return out.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
37848
+ }
37849
+ function filterSharedArtifacts(artifacts, options) {
37850
+ const name = options.name === void 0 ? void 0 : uriSegment(options.name);
37851
+ return artifacts.filter((artifact) => {
37852
+ if (options.agent !== void 0 && artifact.artifact.agent !== options.agent) {
37853
+ return false;
37854
+ }
37855
+ if (options.kind !== void 0 && artifact.artifact.kind !== options.kind) {
37856
+ return false;
37857
+ }
37858
+ if (name !== void 0 && artifact.artifact.name !== name) {
37859
+ return false;
37860
+ }
37861
+ return true;
37862
+ });
37863
+ }
37864
+ async function maybeSyncSharedArtifacts(config2, options) {
37865
+ if (options.sync === false) {
37866
+ return { syncedTeams: [], warnings: [] };
37867
+ }
37868
+ return syncSharedReposBeforeAgentRead(config2);
37869
+ }
37870
+ async function sharedArtifactInstallStatus(artifact) {
37871
+ return (await sharedArtifactInstallState(artifact)).status;
37872
+ }
37873
+ async function sharedArtifactInstallState(artifact) {
37874
+ const sourceContent = await (0, import_promises4.readFile)(artifact.sourcePath, "utf8");
37875
+ const sourceSha = sha256(sourceContent);
37876
+ const existingContent = await readFileIfExists(artifact.installPath) ?? void 0;
37877
+ if (existingContent === void 0) {
37878
+ return { sourceContent, sourceSha, status: "not_installed" };
37879
+ }
37880
+ const existingSha = sha256(existingContent);
37881
+ const metadata = await readSharedArtifactMetadata(artifact);
37882
+ if (existingSha === sourceSha) {
37883
+ return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "current" };
37884
+ }
37885
+ if (metadata === void 0) {
37886
+ return { existingContent, existingSha, sourceContent, sourceSha, status: "local_modified" };
37887
+ }
37888
+ const remoteChanged = metadata.sourceSha256 !== sourceSha;
37889
+ const localChanged = metadata.installedSha256 !== existingSha;
37890
+ if (remoteChanged && localChanged) {
37891
+ return {
37892
+ existingContent,
37893
+ existingSha,
37894
+ metadata,
37895
+ sourceContent,
37896
+ sourceSha,
37897
+ status: "remote_changed_and_local_modified"
37898
+ };
37899
+ }
37900
+ if (remoteChanged) {
37901
+ return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "update_available" };
37902
+ }
37903
+ return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "local_modified" };
37904
+ }
37905
+ async function readSharedArtifactMetadata(artifact) {
37906
+ const raw = await readFileIfExists(sharedArtifactMetadataPath(artifact));
37907
+ if (raw === void 0) {
37908
+ return void 0;
37909
+ }
37910
+ const parsed = parseJsonConfigObject(raw);
37911
+ if (parsed === void 0 || parsed.version !== ARTIFACT_INSTALL_METADATA_VERSION) {
37912
+ return void 0;
37913
+ }
37914
+ const artifactValue = parsed.artifact;
37915
+ 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)) {
37916
+ return void 0;
37917
+ }
37918
+ const metadataArtifact = artifactValue;
37919
+ if (metadataArtifact.agent !== artifact.artifact.agent || metadataArtifact.kind !== artifact.artifact.kind || metadataArtifact.name !== artifact.artifact.name) {
37920
+ return void 0;
37921
+ }
37922
+ return {
37923
+ artifact: artifact.artifact,
37924
+ installedAt: parsed.installedAt,
37925
+ installedSha256: parsed.installedSha256,
37926
+ source: parsed.source,
37927
+ sourceSha256: parsed.sourceSha256,
37928
+ team: parsed.team,
37929
+ version: ARTIFACT_INSTALL_METADATA_VERSION
37930
+ };
37931
+ }
37932
+ async function writeSharedArtifactMetadata(artifact, sourceSha) {
37933
+ const metadata = {
37934
+ artifact: artifact.artifact,
37935
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
37936
+ installedSha256: sourceSha,
37937
+ source: artifact.sourceRelativePath,
37938
+ sourceSha256: sourceSha,
37939
+ team: artifact.team,
37940
+ version: ARTIFACT_INSTALL_METADATA_VERSION
37941
+ };
37942
+ const metadataPath = sharedArtifactMetadataPath(artifact);
37943
+ await ensureDirectory((0, import_node_path4.dirname)(metadataPath), false);
37944
+ await (0, import_promises4.writeFile)(metadataPath, `${JSON.stringify(metadata, void 0, 2)}
37945
+ `, { encoding: "utf8", mode: 384 });
37946
+ }
37947
+ function sharedArtifactMetadataPath(artifact) {
37948
+ return `${artifact.installPath}.threadnote-install.json`;
37949
+ }
37950
+ function sharedArtifactDryRunVerb(status, force) {
37951
+ switch (status) {
37952
+ case "not_installed":
37953
+ return "Would install";
37954
+ case "current":
37955
+ return "Already installed";
37956
+ case "update_available":
37957
+ return "Would update";
37958
+ case "local_modified":
37959
+ case "remote_changed_and_local_modified":
37960
+ return force ? "Would replace" : "Would skip modified";
37961
+ }
37962
+ }
37963
+ function sharedArtifactDryRunSuffix(status, force) {
37964
+ if ((status === "local_modified" || status === "remote_changed_and_local_modified") && !force) {
37965
+ return " (pass --force to replace local changes)";
37966
+ }
37967
+ return "";
37968
+ }
37969
+ function sharedArtifactInstallVerb(status, force) {
37970
+ if (force && (status === "local_modified" || status === "remote_changed_and_local_modified")) {
37971
+ return "Replaced";
37972
+ }
37973
+ if (status === "update_available") {
37974
+ return "Updated";
37975
+ }
37976
+ return "Installed";
37977
+ }
37978
+ function sharedArtifactFilterLabel(options) {
37979
+ const filters = [];
37980
+ if (options.kind !== void 0) {
37981
+ filters.push(`kind=${options.kind}`);
37982
+ }
37983
+ if (options.agent !== void 0) {
37984
+ filters.push(`agent=${options.agent}`);
37985
+ }
37986
+ if (options.name !== void 0) {
37987
+ filters.push(`name=${uriSegment(options.name)}`);
37988
+ }
37989
+ return filters.join(", ");
37990
+ }
37991
+ function sharedArtifactLabel(artifact) {
37992
+ return `${artifact.kind} ${artifact.agent}/${artifact.name}`;
37993
+ }
37994
+ function sharedArtifactInstallPath(team, artifact) {
37995
+ if (artifact.kind === "skill" && artifact.agent === "codex") {
37996
+ return (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".codex", "skills", "threadnote", team, artifact.name, "SKILL.md");
37997
+ }
37998
+ if (artifact.kind === "skill" && artifact.agent === "claude") {
37999
+ return (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".claude", "skills", "threadnote", team, artifact.name, "SKILL.md");
38000
+ }
38001
+ return (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".claude", "commands", "threadnote", team, `${artifact.name}.md`);
38002
+ }
37538
38003
  function stripPersonalProvenance(content) {
37539
38004
  const lines = content.split("\n");
37540
38005
  let headerEnd = lines.length;
@@ -37571,9 +38036,9 @@ async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun, option
37571
38036
  continue;
37572
38037
  }
37573
38038
  if (options.quiet === true) {
37574
- await runCommand(ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
38039
+ await runCommand(ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared context."]));
37575
38040
  } else {
37576
- await maybeRun(false, ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
38041
+ await maybeRun(false, ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared context."]));
37577
38042
  }
37578
38043
  }
37579
38044
  }
@@ -37802,7 +38267,7 @@ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
37802
38267
  continue;
37803
38268
  }
37804
38269
  const firstSegment = change.relativePath.split("/")[0];
37805
- if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
38270
+ if (!SHAREABLE_TOP_LEVEL_DIRS.includes(firstSegment)) {
37806
38271
  continue;
37807
38272
  }
37808
38273
  const uri = workfileToVikingUri(config2, team, change.path);
@@ -37879,6 +38344,8 @@ async function main() {
37879
38344
  "When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
37880
38345
  "Use compact_context with dryRun=true for scoped memory hygiene when recall surfaces overlapping active memories.",
37881
38346
  "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.",
38347
+ "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.",
38348
+ "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
38349
  "Do not store secrets, customer data, raw production logs, or credentials."
37883
38350
  ].join("\n")
37884
38351
  }
@@ -38079,6 +38546,80 @@ function registerTools(server, config2) {
38079
38546
  return runSharePublishTool(config2, checkedUri.value, { message, preview, push, redact, team });
38080
38547
  }
38081
38548
  );
38549
+ server.registerTool(
38550
+ "share_skill",
38551
+ {
38552
+ annotations: { readOnlyHint: false, destructiveHint: true },
38553
+ 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.",
38554
+ inputSchema: {
38555
+ agent: external_exports.enum(["codex", "claude"]).optional().describe("Agent owner when path inference is ambiguous"),
38556
+ force: external_exports.boolean().optional().describe("Replace an existing shared artifact with different content"),
38557
+ kind: external_exports.enum(["skill", "command"]).optional().describe("Artifact kind when path inference is ambiguous"),
38558
+ message: external_exports.string().optional().describe('Commit message override; defaults to "share: publish <path>"'),
38559
+ name: external_exports.string().optional().describe("Shared artifact name; defaults to skill directory or command file stem"),
38560
+ path: external_exports.string().optional().describe("Required local path to SKILL.md or a Claude command markdown file"),
38561
+ preview: external_exports.boolean().optional().describe("Return the bytes that would land in the shared git repo"),
38562
+ push: external_exports.boolean().optional().describe("Push to remote after committing; defaults to true"),
38563
+ redact: external_exports.boolean().optional().describe("Replace soft-leak matches (local paths) with placeholders and continue; credentials still block."),
38564
+ team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
38565
+ }
38566
+ },
38567
+ async ({ agent, force, kind, message, name, path, preview, push, redact, team }) => {
38568
+ const checkedPath = requiredText(path, "share_skill", "path", {
38569
+ path: "~/.codex/skills/example/SKILL.md"
38570
+ });
38571
+ if (!checkedPath.ok) {
38572
+ return checkedPath.error;
38573
+ }
38574
+ return runShareSkillTool(config2, checkedPath.value, {
38575
+ agent,
38576
+ force,
38577
+ kind,
38578
+ message,
38579
+ name,
38580
+ preview,
38581
+ push,
38582
+ redact,
38583
+ team
38584
+ });
38585
+ }
38586
+ );
38587
+ server.registerTool(
38588
+ "list_shared_skills",
38589
+ {
38590
+ annotations: { readOnlyHint: true, destructiveHint: false },
38591
+ description: "List shared Codex/Claude skills and Claude commands available in a configured Threadnote team repo, including whether each one is already installed locally.",
38592
+ inputSchema: {
38593
+ agent: external_exports.enum(["codex", "claude"]).optional().describe("Optional agent filter"),
38594
+ kind: external_exports.enum(["skill", "command"]).optional().describe("Optional kind filter"),
38595
+ name: external_exports.string().optional().describe("Optional shared artifact name filter"),
38596
+ team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
38597
+ }
38598
+ },
38599
+ async ({ agent, kind, name, team }) => runListSharedSkillsTool(config2, { agent, kind, name, team })
38600
+ );
38601
+ server.registerTool(
38602
+ "install_shared_skill",
38603
+ {
38604
+ annotations: { readOnlyHint: false, destructiveHint: true },
38605
+ 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.",
38606
+ inputSchema: {
38607
+ agent: external_exports.enum(["codex", "claude"]).optional().describe("Agent owner; required when name is ambiguous"),
38608
+ dryRun: external_exports.boolean().optional().describe("Preview install without writing local files"),
38609
+ force: external_exports.boolean().optional().describe("Replace an existing installed artifact with different content"),
38610
+ kind: external_exports.enum(["skill", "command"]).optional().describe("Artifact kind; required when name is ambiguous"),
38611
+ name: external_exports.string().optional().describe("Required shared artifact name to install"),
38612
+ team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
38613
+ }
38614
+ },
38615
+ async ({ agent, dryRun, force, kind, name, team }) => {
38616
+ const checkedName = requiredText(name, "install_shared_skill", "name", { name: "reviewer" });
38617
+ if (!checkedName.ok) {
38618
+ return checkedName.error;
38619
+ }
38620
+ return runInstallSharedSkillTool(config2, checkedName.value, { agent, dryRun, force, kind, team });
38621
+ }
38622
+ );
38082
38623
  }
38083
38624
  function registerOpenVikingParityTools(server, config2) {
38084
38625
  server.registerTool(
@@ -39439,27 +39980,85 @@ async function runSharePublishTool(config2, sourceUri, options) {
39439
39980
  return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
39440
39981
  }
39441
39982
  }
39983
+ async function runShareSkillTool(config2, sourcePath, options) {
39984
+ try {
39985
+ const result = await shareAgentArtifact(config2, sourcePath, options);
39986
+ const lines = [...result.messages, ...result.gitMessages];
39987
+ if (result.previewContent !== void 0) {
39988
+ lines.push("-----BEGIN PREVIEW-----");
39989
+ lines.push(result.previewContent);
39990
+ lines.push("-----END PREVIEW-----");
39991
+ }
39992
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
39993
+ } catch (err) {
39994
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
39995
+ }
39996
+ }
39997
+ async function runListSharedSkillsTool(config2, options) {
39998
+ try {
39999
+ const result = await listSharedAgentArtifacts(config2, options);
40000
+ if (result.artifacts.length === 0) {
40001
+ const lines2 = shareArtifactToolHeader(result.team, result.syncedTeams, result.warnings);
40002
+ lines2.push(`No shared skills or commands found for team "${result.team}".`);
40003
+ return { content: [{ type: "text", text: lines2.join("\n") }] };
40004
+ }
40005
+ const lines = shareArtifactToolHeader(result.team, result.syncedTeams, result.warnings);
40006
+ lines.push(`Shared skills and commands for team "${result.team}":`);
40007
+ for (const artifact of result.artifacts) {
40008
+ lines.push(
40009
+ `- ${artifact.artifact.kind} ${artifact.artifact.agent}/${artifact.artifact.name} (${artifact.installStatus})`
40010
+ );
40011
+ lines.push(
40012
+ ` install: install_shared_skill({"name":"${artifact.artifact.name}","agent":"${artifact.artifact.agent}","kind":"${artifact.artifact.kind}"})`
40013
+ );
40014
+ }
40015
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
40016
+ } catch (err) {
40017
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
40018
+ }
40019
+ }
40020
+ async function runInstallSharedSkillTool(config2, name, options) {
40021
+ try {
40022
+ const result = await installSharedAgentArtifacts(config2, {
40023
+ ...options,
40024
+ apply: options.dryRun !== true,
40025
+ name
40026
+ });
40027
+ return {
40028
+ content: [
40029
+ {
40030
+ type: "text",
40031
+ text: [...shareArtifactToolHeader(result.team, result.syncedTeams, result.warnings), ...result.messages].join(
40032
+ "\n"
40033
+ )
40034
+ }
40035
+ ],
40036
+ isError: false
40037
+ };
40038
+ } catch (err) {
40039
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
40040
+ }
40041
+ }
40042
+ function shareArtifactToolHeader(team, syncedTeams, warnings) {
40043
+ const lines = [`Team: ${team}`];
40044
+ if (syncedTeams.length > 0) {
40045
+ lines.push(`Synced shared teams: ${syncedTeams.join(", ")}`);
40046
+ }
40047
+ for (const warning2 of warnings) {
40048
+ lines.push(`Warning: ${warning2}`);
40049
+ }
40050
+ return lines;
40051
+ }
39442
40052
  function withIdentity2(config2, args) {
39443
40053
  return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
39444
40054
  }
39445
40055
  async function requiredOpenVikingCli2() {
39446
- const command2 = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0, import_node_path5.join)((0, import_node_os3.homedir)(), ".local", "bin", "ov"), (0, import_node_path5.join)((0, import_node_os3.homedir)(), ".local", "bin", "openviking")]);
40056
+ const command2 = await findOpenVikingCli();
39447
40057
  if (!command2) {
39448
40058
  throw new Error("Neither ov nor openviking was found. Run threadnote install first.");
39449
40059
  }
39450
40060
  return command2;
39451
40061
  }
39452
- async function firstExistingPath(paths) {
39453
- for (const path of paths) {
39454
- try {
39455
- await (0, import_promises5.access)(path);
39456
- return await (0, import_promises5.realpath)(path);
39457
- } catch (_err) {
39458
- continue;
39459
- }
39460
- }
39461
- return void 0;
39462
- }
39463
40062
  main().catch((err) => {
39464
40063
  process.stderr.write(`${errorMessage(err)}
39465
40064
  `);