threadnote 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7382,9 +7382,9 @@ async function runRead(config, uri, options) {
7382
7382
  const ov = await openVikingCliForMode(options.dryRun === true);
7383
7383
  const result = await maybeRun(options.dryRun === true, ov, withIdentity(config, ["read", uri]));
7384
7384
  if (result && result.stdout.includes("[Directory overview is not ready]") && (uri.endsWith("/.overview.md") || uri.endsWith("/.abstract.md"))) {
7385
- const parentUri = parentVikingUri(uri);
7385
+ const parentUri2 = parentVikingUri(uri);
7386
7386
  console.log("\nThis is a generated summary placeholder. To read the underlying content, inspect leaf nodes:");
7387
- console.log(` threadnote list ${parentUri} --all --recursive`);
7387
+ console.log(` threadnote list ${parentUri2} --all --recursive`);
7388
7388
  }
7389
7389
  }
7390
7390
  async function runList(config, uri, options) {
@@ -7727,6 +7727,7 @@ function exactMemoryScopes(config, includeArchived) {
7727
7727
  `${userBase}/handoffs/active`,
7728
7728
  `${userBase}/incidents/active`,
7729
7729
  `${userBase}/events`,
7730
+ `${userBase}/shared`,
7730
7731
  `viking://agent/${uriSegment(config.agentId)}/memories`
7731
7732
  ];
7732
7733
  return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
@@ -8395,19 +8396,780 @@ function detectSecretMatches(content) {
8395
8396
  return matches;
8396
8397
  }
8397
8398
 
8399
+ // src/share.ts
8400
+ var import_promises6 = require("node:fs/promises");
8401
+ var import_node_os4 = require("node:os");
8402
+ var import_node_path6 = require("node:path");
8403
+ var TEAMS_FILE_VERSION = 1;
8404
+ var SHARED_SEGMENT = "shared";
8405
+ var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
8406
+ var DEFAULT_GIT_REMOTE_NAME = "origin";
8407
+ var SCRUBBER_PATTERNS = [
8408
+ { name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
8409
+ { name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
8410
+ { name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
8411
+ { name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
8412
+ { name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
8413
+ { name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
8414
+ // Matches bare JWTs (three base64url segments). May surface a JWE token in
8415
+ // legitimate docs; if that becomes noisy we can switch to warn-only.
8416
+ { name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
8417
+ { name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
8418
+ // Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
8419
+ // xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
8420
+ { name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i }
8421
+ ];
8422
+ async function runShareInit(config, remoteUrl, options) {
8423
+ if (!remoteUrl.trim()) {
8424
+ throw new Error("Provide a git remote URL for the shared memories repo.");
8425
+ }
8426
+ const dryRun = options.dryRun === true;
8427
+ const teamName = normalizeTeamName(options.team);
8428
+ const teamsFile = await readTeamsFile(config);
8429
+ if (teamsFile.teams[teamName]) {
8430
+ throw new Error(
8431
+ `Team "${teamName}" is already configured (remote ${teamsFile.teams[teamName].remote}). Remove it first with: threadnote share remove --team ${teamName}`
8432
+ );
8433
+ }
8434
+ const worktree = teamWorktreePath(config, teamName);
8435
+ const gitdir = teamGitdirPath(config, teamName);
8436
+ await assertWorktreeUsable(worktree);
8437
+ if (await exists(gitdir)) {
8438
+ throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
8439
+ }
8440
+ await ensureDirectory((0, import_node_path6.dirname)(worktree), dryRun);
8441
+ await ensureDirectory((0, import_node_path6.dirname)(gitdir), dryRun);
8442
+ const git = await requiredExecutable("git");
8443
+ await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
8444
+ const newConfig = {
8445
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
8446
+ gitdir,
8447
+ name: teamName,
8448
+ remote: remoteUrl,
8449
+ worktree
8450
+ };
8451
+ const updatedTeams = {
8452
+ defaultTeam: shouldSetDefault(options, teamsFile) ? teamName : teamsFile.defaultTeam ?? teamName,
8453
+ teams: { ...teamsFile.teams, [teamName]: newConfig },
8454
+ version: TEAMS_FILE_VERSION
8455
+ };
8456
+ if (dryRun) {
8457
+ console.log(`Would write teams file: ${teamsFilePath(config)}`);
8458
+ console.log(`Would set ${teamName} as default? ${updatedTeams.defaultTeam === teamName}`);
8459
+ } else {
8460
+ await writeTeamsFile(config, updatedTeams);
8461
+ console.log(`Configured shared team "${teamName}" -> ${portablePath(worktree)}`);
8462
+ }
8463
+ if (!dryRun) {
8464
+ await ensureSharedGitignore(worktree, git, options.push !== false);
8465
+ const ingested = await ingestWorktreeFiles(config, newConfig, "create");
8466
+ console.log(`Ingested ${ingested} shared memory file(s) into OpenViking.`);
8467
+ }
8468
+ }
8469
+ var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
8470
+ var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
8471
+ async function ensureSharedGitignore(worktree, git, push) {
8472
+ const gitignorePath = (0, import_node_path6.join)(worktree, ".gitignore");
8473
+ const existing = await readFileIfExists(gitignorePath) ?? "";
8474
+ const lines = existing.split("\n").map((line) => line.trim());
8475
+ const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
8476
+ if (missingPatterns.length === 0) {
8477
+ return;
8478
+ }
8479
+ const hasHeader = lines.includes(SHARED_GITIGNORE_HEADER);
8480
+ const segments = [];
8481
+ if (existing.length > 0 && !existing.endsWith("\n")) {
8482
+ segments.push("\n");
8483
+ }
8484
+ if (existing.length > 0) {
8485
+ segments.push("\n");
8486
+ }
8487
+ if (!hasHeader) {
8488
+ segments.push(SHARED_GITIGNORE_HEADER, "\n");
8489
+ }
8490
+ segments.push(missingPatterns.join("\n"), "\n");
8491
+ await (0, import_promises6.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
8492
+ console.log(`Added ${missingPatterns.join(", ")} to ${portablePath(gitignorePath)}`);
8493
+ await maybeRun(false, git, ["-C", worktree, "add", ".gitignore"]);
8494
+ const commitResult = await runCommand(
8495
+ git,
8496
+ ["-C", worktree, "commit", "-m", "share: ignore OpenViking directory summaries"],
8497
+ { allowFailure: true }
8498
+ );
8499
+ if (commitResult.exitCode !== 0) {
8500
+ const detail = commitResult.stderr.trim() || commitResult.stdout.trim();
8501
+ if (!/nothing to commit|no changes added/i.test(detail)) {
8502
+ console.warn(
8503
+ `.gitignore housekeeping commit was rejected (${detail || "unknown"}); it will be retried on the next share sync.`
8504
+ );
8505
+ return;
8506
+ }
8507
+ }
8508
+ if (push) {
8509
+ await maybeRun(false, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8510
+ }
8511
+ }
8512
+ async function runShareStatus(config, options) {
8513
+ const team = await resolveTeam(config, options.team);
8514
+ const git = await requiredExecutable("git");
8515
+ console.log(`Team: ${team.name}`);
8516
+ console.log(`Remote: ${team.config.remote}`);
8517
+ console.log(`Worktree: ${portablePath(team.config.worktree)}`);
8518
+ console.log(`Gitdir: ${portablePath(team.config.gitdir)}`);
8519
+ await maybeRun(options.dryRun === true, git, ["-C", team.config.worktree, "status", "--short", "--branch"]);
8520
+ await maybeRun(options.dryRun === true, git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
8521
+ allowFailure: true
8522
+ });
8523
+ const ahead = await gitOutput(team.config.worktree, ["rev-list", "--count", "@{u}..HEAD"], options.dryRun === true);
8524
+ const behind = await gitOutput(team.config.worktree, ["rev-list", "--count", "HEAD..@{u}"], options.dryRun === true);
8525
+ if (ahead !== void 0) {
8526
+ console.log(`Ahead of upstream: ${ahead}`);
8527
+ }
8528
+ if (behind !== void 0) {
8529
+ console.log(`Behind upstream: ${behind}`);
8530
+ }
8531
+ }
8532
+ async function runShareSync(config, options) {
8533
+ const team = await resolveTeam(config, options.team);
8534
+ const dryRun = options.dryRun === true;
8535
+ const git = await requiredExecutable("git");
8536
+ const worktree = team.config.worktree;
8537
+ if (!dryRun) {
8538
+ await ensureSharedGitignore(worktree, git, false);
8539
+ }
8540
+ if (await hasUncommittedChanges(worktree)) {
8541
+ if (options.autoCommit === false) {
8542
+ throw new Error(
8543
+ `Worktree ${worktree} has uncommitted changes. Commit them yourself or rerun without --no-auto-commit.`
8544
+ );
8545
+ }
8546
+ const message = options.message ?? `share: sync ${(/* @__PURE__ */ new Date()).toISOString()}`;
8547
+ await stageShareableChanges(dryRun, git, worktree);
8548
+ await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
8549
+ }
8550
+ const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], dryRun);
8551
+ await maybeRun(dryRun, git, ["-C", worktree, "fetch", DEFAULT_GIT_REMOTE_NAME]);
8552
+ const pullResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8553
+ if (dryRun) {
8554
+ console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
8555
+ } else if (pullResult && pullResult.exitCode !== 0) {
8556
+ if (await exists((0, import_node_path6.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path6.join)(team.config.gitdir, "rebase-apply"))) {
8557
+ throw new Error(
8558
+ `git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
8559
+ Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
8560
+ );
8561
+ }
8562
+ throw new Error(
8563
+ `git pull --rebase failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
8564
+ );
8565
+ }
8566
+ const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], dryRun);
8567
+ if (!dryRun && beforeRev && afterRev && beforeRev !== afterRev) {
8568
+ const changes = await listChangedFiles(worktree, beforeRev, afterRev);
8569
+ await applyChangesToOpenViking(config, team.config, changes);
8570
+ console.log(`Reindexed ${changes.length} file change(s) into OpenViking.`);
8571
+ } else if (!dryRun) {
8572
+ console.log("No upstream changes to reindex.");
8573
+ }
8574
+ if (options.push !== false) {
8575
+ await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8576
+ }
8577
+ }
8578
+ async function stageShareableChanges(dryRun, git, worktree) {
8579
+ const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_MEMORY_KIND_DIRS.map((dir) => `:(top)${dir}`)];
8580
+ await maybeRun(dryRun, git, ["-C", worktree, "add", "--", ...pathspecs], { allowFailure: true });
8581
+ }
8582
+ async function runSharePublish(config, sourceUri, options) {
8583
+ assertVikingUri(sourceUri);
8584
+ const team = await resolveTeam(config, options.team);
8585
+ const dryRun = options.dryRun === true;
8586
+ if (isInSharedNamespace(config, sourceUri)) {
8587
+ throw new Error(`Memory ${sourceUri} is already in the shared namespace.`);
8588
+ }
8589
+ const ov = await openVikingCliForMode(dryRun);
8590
+ const content = await readMemoryContent(config, ov, sourceUri, dryRun);
8591
+ const blocker = scrubberBlocker(content);
8592
+ if (blocker) {
8593
+ throw new Error(`Refusing to publish ${sourceUri}: possible ${blocker}. Strip the sensitive value, then retry.`);
8594
+ }
8595
+ const targetUri = sharedUriFor(config, sourceUri, team.name);
8596
+ if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
8597
+ throw new Error(
8598
+ `Refusing to publish: ${targetUri} already exists in the shared namespace. Inspect it via threadnote read; if it should be replaced, forget the existing shared copy first.`
8599
+ );
8600
+ }
8601
+ await ensureSharedDirectoryChain(config, ov, targetUri, dryRun);
8602
+ await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
8603
+ await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "publish");
8604
+ const git = await requiredExecutable("git");
8605
+ const worktree = team.config.worktree;
8606
+ const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
8607
+ const message = options.message ?? `share: publish ${relativePath}`;
8608
+ await maybeRun(dryRun, git, ["-C", worktree, "add", "--", relativePath]);
8609
+ await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
8610
+ if (options.push !== false) {
8611
+ const pushResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8612
+ if (dryRun) {
8613
+ console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME])}`);
8614
+ } else if (pushResult && pushResult.exitCode !== 0) {
8615
+ const detail = pushResult.stderr.trim() || pushResult.stdout.trim() || "unknown error";
8616
+ throw new Error(
8617
+ `Memory was committed locally but git push failed: ${detail}
8618
+ Resolve the remote issue (auth, network, branch protection), then run: threadnote share sync`
8619
+ );
8620
+ }
8621
+ }
8622
+ console.log(`Published ${sourceUri} -> ${targetUri}`);
8623
+ }
8624
+ async function runShareUnpublish(config, sourceUri, options) {
8625
+ assertVikingUri(sourceUri);
8626
+ const team = await resolveTeam(config, options.team);
8627
+ const dryRun = options.dryRun === true;
8628
+ if (!isInTeamNamespace(config, sourceUri, team.name)) {
8629
+ throw new Error(`Memory ${sourceUri} is not in team "${team.name}" shared namespace.`);
8630
+ }
8631
+ const ov = await openVikingCliForMode(dryRun);
8632
+ const content = await readMemoryContent(config, ov, sourceUri, dryRun);
8633
+ const targetUri = personalUriFor(config, sourceUri, team.name);
8634
+ if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
8635
+ throw new Error(
8636
+ `Refusing to unpublish: a personal memory already exists at ${targetUri}. Move or forget it first, then retry.`
8637
+ );
8638
+ }
8639
+ await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
8640
+ await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "unpublish");
8641
+ const git = await requiredExecutable("git");
8642
+ const worktree = team.config.worktree;
8643
+ const relativePath = vikingUriToWorktreeRelative(config, sourceUri, team.name);
8644
+ const message = options.message ?? `share: unpublish ${relativePath}`;
8645
+ await maybeRun(dryRun, git, ["-C", worktree, "rm", relativePath], { allowFailure: true });
8646
+ await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
8647
+ if (options.push !== false) {
8648
+ await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8649
+ }
8650
+ console.log(`Unpublished ${sourceUri} -> ${targetUri}`);
8651
+ }
8652
+ async function runShareList(config, _options) {
8653
+ const teams = await readTeamsFile(config);
8654
+ const entries = Object.values(teams.teams);
8655
+ if (entries.length === 0) {
8656
+ console.log("No shared teams configured. Run: threadnote share init <remote-url>");
8657
+ return;
8658
+ }
8659
+ for (const team of entries) {
8660
+ const marker = team.name === teams.defaultTeam ? " (default)" : "";
8661
+ console.log(`- ${team.name}${marker}`);
8662
+ console.log(` remote: ${team.remote}`);
8663
+ console.log(` worktree: ${portablePath(team.worktree)}`);
8664
+ console.log(` gitdir: ${portablePath(team.gitdir)}`);
8665
+ console.log(` added: ${team.addedAt}`);
8666
+ }
8667
+ }
8668
+ async function runShareRemove(config, options) {
8669
+ const team = await resolveTeam(config, options.team);
8670
+ const dryRun = options.dryRun === true;
8671
+ const teamsFile = await readTeamsFile(config);
8672
+ const remaining = {};
8673
+ for (const [name, value] of Object.entries(teamsFile.teams)) {
8674
+ if (name !== team.name) {
8675
+ remaining[name] = value;
8676
+ }
8677
+ }
8678
+ const remainingNames = Object.keys(remaining);
8679
+ const nextDefault = teamsFile.defaultTeam === team.name ? remainingNames[0] : teamsFile.defaultTeam;
8680
+ const updated = { defaultTeam: nextDefault, teams: remaining, version: TEAMS_FILE_VERSION };
8681
+ if (dryRun) {
8682
+ console.log(`Would update teams file: ${teamsFilePath(config)}`);
8683
+ } else {
8684
+ await writeTeamsFile(config, updated);
8685
+ console.log(`Removed team "${team.name}" from teams.json.`);
8686
+ }
8687
+ if (options.keepFiles !== true) {
8688
+ await removePath(team.config.worktree, "shared worktree", dryRun);
8689
+ await removePath(team.config.gitdir, "shared gitdir", dryRun);
8690
+ } else {
8691
+ console.log(`Keeping files at ${portablePath(team.config.worktree)} and ${portablePath(team.config.gitdir)}`);
8692
+ }
8693
+ }
8694
+ function normalizeTeamName(input2) {
8695
+ const candidate = (input2 ?? "default").trim();
8696
+ if (!candidate) {
8697
+ return "default";
8698
+ }
8699
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(candidate) || /^\.+$/.test(candidate)) {
8700
+ throw new Error(
8701
+ `Invalid team name "${input2}". Team names must start with a lowercase letter or digit and contain only [a-z0-9._-]. Single-dot or dot-only names are rejected so they don't collapse to the shared-root or parent directory.`
8702
+ );
8703
+ }
8704
+ return candidate;
8705
+ }
8706
+ function teamsFilePath(config) {
8707
+ return (0, import_node_path6.join)(config.agentContextHome, "share", "teams.json");
8708
+ }
8709
+ function teamWorktreePath(config, team) {
8710
+ return (0, import_node_path6.join)(
8711
+ config.agentContextHome,
8712
+ "data",
8713
+ "viking",
8714
+ config.account,
8715
+ "user",
8716
+ uriSegment(config.user),
8717
+ "memories",
8718
+ SHARED_SEGMENT,
8719
+ team
8720
+ );
8721
+ }
8722
+ function teamGitdirPath(config, team) {
8723
+ return (0, import_node_path6.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
8724
+ }
8725
+ async function readTeamsFile(config) {
8726
+ const path = teamsFilePath(config);
8727
+ const raw = await readFileIfExists(path);
8728
+ if (!raw) {
8729
+ return { teams: {}, version: TEAMS_FILE_VERSION };
8730
+ }
8731
+ const parsed = parseJsonConfigObject(raw);
8732
+ if (!parsed) {
8733
+ throw new Error(`Could not parse teams file ${path}`);
8734
+ }
8735
+ if (typeof parsed.version === "number" && parsed.version > TEAMS_FILE_VERSION) {
8736
+ throw new Error(
8737
+ `Teams file ${path} was written with version ${parsed.version}; this Threadnote binary understands up to version ${TEAMS_FILE_VERSION}. Upgrade Threadnote (\`threadnote update\`) before continuing.`
8738
+ );
8739
+ }
8740
+ const teams = {};
8741
+ if (typeof parsed.teams === "object" && parsed.teams !== null && !Array.isArray(parsed.teams)) {
8742
+ for (const [name, value] of Object.entries(parsed.teams)) {
8743
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
8744
+ console.warn(`Skipping non-object team entry "${name}" in ${path}.`);
8745
+ continue;
8746
+ }
8747
+ const entry = value;
8748
+ if (typeof entry.remote !== "string" || entry.remote.length === 0) {
8749
+ console.warn(`Skipping team entry "${name}" in ${path}: missing or empty "remote" field.`);
8750
+ continue;
8751
+ }
8752
+ teams[name] = {
8753
+ addedAt: typeof entry.addedAt === "string" ? entry.addedAt : (/* @__PURE__ */ new Date(0)).toISOString(),
8754
+ gitdir: typeof entry.gitdir === "string" ? entry.gitdir : teamGitdirPath(config, name),
8755
+ name,
8756
+ remote: entry.remote,
8757
+ worktree: typeof entry.worktree === "string" ? entry.worktree : teamWorktreePath(config, name)
8758
+ };
8759
+ }
8760
+ }
8761
+ const defaultTeam = typeof parsed.defaultTeam === "string" ? parsed.defaultTeam : void 0;
8762
+ return { defaultTeam, teams, version: TEAMS_FILE_VERSION };
8763
+ }
8764
+ async function writeTeamsFile(config, contents) {
8765
+ const path = teamsFilePath(config);
8766
+ await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(path), { recursive: true });
8767
+ const serializable = {
8768
+ defaultTeam: contents.defaultTeam,
8769
+ teams: contents.teams,
8770
+ version: contents.version
8771
+ };
8772
+ await (0, import_promises6.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
8773
+ `, { encoding: "utf8", mode: 384 });
8774
+ }
8775
+ async function resolveTeam(config, requested) {
8776
+ const teamsFile = await readTeamsFile(config);
8777
+ const entries = Object.entries(teamsFile.teams);
8778
+ if (entries.length === 0) {
8779
+ throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
8780
+ }
8781
+ const wantName = requested ? normalizeTeamName(requested) : teamsFile.defaultTeam ?? entries[0][0];
8782
+ const found = teamsFile.teams[wantName];
8783
+ if (!found) {
8784
+ const known = entries.map(([name]) => name).join(", ");
8785
+ throw new Error(`Team "${wantName}" is not configured. Known teams: ${known}`);
8786
+ }
8787
+ return { config: found, name: wantName };
8788
+ }
8789
+ function shouldSetDefault(options, existing) {
8790
+ if (options.setDefault === true) {
8791
+ return true;
8792
+ }
8793
+ return existing.defaultTeam === void 0;
8794
+ }
8795
+ async function assertWorktreeUsable(worktree) {
8796
+ if (!await exists(worktree)) {
8797
+ return;
8798
+ }
8799
+ if (!await isDirectory(worktree)) {
8800
+ throw new Error(`Cannot use ${worktree} as a worktree: not a directory.`);
8801
+ }
8802
+ const entries = await (0, import_promises6.readdir)(worktree);
8803
+ if (entries.length > 0) {
8804
+ const preview = entries.slice(0, 5).join(", ");
8805
+ const suffix = entries.length > 5 ? `, +${entries.length - 5} more` : "";
8806
+ throw new Error(
8807
+ `Worktree ${worktree} is not empty (contains: ${preview}${suffix}). Move or remove its contents, then retry threadnote share init.`
8808
+ );
8809
+ }
8810
+ }
8811
+ async function ingestWorktreeFiles(config, team, initialMode) {
8812
+ const ov = await openVikingCliForMode(false);
8813
+ const files = await walkMemoryFiles(team.worktree);
8814
+ for (const file of files) {
8815
+ const uri = workfileToVikingUri(config, team, file);
8816
+ await ensureSharedDirectoryChain(config, ov, uri, false);
8817
+ await ingestSingleFile(ov, config, uri, file, initialMode);
8818
+ }
8819
+ return files.length;
8820
+ }
8821
+ async function walkMemoryFiles(root) {
8822
+ const out = [];
8823
+ async function visit(path, depth) {
8824
+ let entries;
8825
+ try {
8826
+ entries = await (0, import_promises6.readdir)(path, { withFileTypes: true });
8827
+ } catch (err) {
8828
+ console.warn(`Skipping ${path} during shared-tree walk: ${err instanceof Error ? err.message : String(err)}`);
8829
+ return;
8830
+ }
8831
+ for (const entry of entries) {
8832
+ if (entry.name === ".git") {
8833
+ continue;
8834
+ }
8835
+ const full = (0, import_node_path6.join)(path, entry.name);
8836
+ if (entry.isDirectory()) {
8837
+ if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
8838
+ continue;
8839
+ }
8840
+ await visit(full, depth + 1);
8841
+ continue;
8842
+ }
8843
+ if (!entry.isFile()) {
8844
+ continue;
8845
+ }
8846
+ if (depth === 0) {
8847
+ continue;
8848
+ }
8849
+ if (!entry.name.endsWith(".md")) {
8850
+ continue;
8851
+ }
8852
+ out.push(full);
8853
+ }
8854
+ }
8855
+ await visit(root, 0);
8856
+ return out;
8857
+ }
8858
+ function workfileToVikingUri(config, team, filePath) {
8859
+ const rel = (0, import_node_path6.relative)(team.worktree, filePath).split(import_node_path6.sep).join("/");
8860
+ return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
8861
+ }
8862
+ function isInSharedNamespace(config, uri) {
8863
+ return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`);
8864
+ }
8865
+ function isInTeamNamespace(config, uri, team) {
8866
+ return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`);
8867
+ }
8868
+ function sharedUriFor(config, personalUri, team) {
8869
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/`;
8870
+ if (!personalUri.startsWith(prefix)) {
8871
+ throw new Error(`Refusing to publish memory outside the current user namespace: ${personalUri}`);
8872
+ }
8873
+ const rest = personalUri.slice(prefix.length);
8874
+ return `${prefix}${SHARED_SEGMENT}/${team}/${rest}`;
8875
+ }
8876
+ function personalUriFor(config, sharedUri, team) {
8877
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`;
8878
+ if (!sharedUri.startsWith(prefix)) {
8879
+ throw new Error(`Refusing to unpublish a URI outside team "${team}" shared namespace: ${sharedUri}`);
8880
+ }
8881
+ const rest = sharedUri.slice(prefix.length);
8882
+ return `viking://user/${uriSegment(config.user)}/memories/${rest}`;
8883
+ }
8884
+ function vikingUriToWorktreeRelative(config, uri, team) {
8885
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`;
8886
+ if (!uri.startsWith(prefix)) {
8887
+ throw new Error(`URI ${uri} is not inside team "${team}" shared subtree.`);
8888
+ }
8889
+ return uri.slice(prefix.length);
8890
+ }
8891
+ function scrubberBlocker(content) {
8892
+ return SCRUBBER_PATTERNS.find((pattern) => pattern.regex.test(content))?.name;
8893
+ }
8894
+ async function readMemoryContent(config, ov, uri, dryRun) {
8895
+ const args = withIdentity(config, ["read", uri]);
8896
+ if (dryRun) {
8897
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
8898
+ return "<dry-run memory body>";
8899
+ }
8900
+ const result = await runCommand(ov, args);
8901
+ if (!result.stdout.trim()) {
8902
+ throw new Error(`Refusing to publish empty memory at ${uri}`);
8903
+ }
8904
+ return result.stdout;
8905
+ }
8906
+ async function ensureSharedDirectoryChain(config, ov, memoryUri, dryRun) {
8907
+ const directoryUri = parentUri(memoryUri);
8908
+ for (const uri of sharedDirectoryChain(config, directoryUri)) {
8909
+ const args = withIdentity(config, ["stat", uri]);
8910
+ if (dryRun) {
8911
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
8912
+ continue;
8913
+ }
8914
+ const statResult = await runCommand(ov, args, { allowFailure: true });
8915
+ if (statResult.exitCode === 0) {
8916
+ continue;
8917
+ }
8918
+ await maybeRun(false, ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared memories."]));
8919
+ }
8920
+ }
8921
+ function parentUri(uri) {
8922
+ const lastSlash = uri.lastIndexOf("/");
8923
+ return lastSlash === -1 ? uri : uri.slice(0, lastSlash);
8924
+ }
8925
+ function sharedDirectoryChain(config, directoryUri) {
8926
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
8927
+ if (!directoryUri.startsWith(prefix)) {
8928
+ return [directoryUri];
8929
+ }
8930
+ const parts = directoryUri.slice(prefix.length).split("/").filter(Boolean);
8931
+ const chain = [];
8932
+ for (let index = 1; index <= parts.length; index += 1) {
8933
+ chain.push(`${prefix}${parts.slice(0, index).join("/")}`);
8934
+ }
8935
+ return chain;
8936
+ }
8937
+ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun) {
8938
+ if (dryRun) {
8939
+ const args = withIdentity(config, [
8940
+ "write",
8941
+ uri,
8942
+ "--from-file",
8943
+ "<staged temp file>",
8944
+ "--mode",
8945
+ initialMode,
8946
+ "--wait",
8947
+ "--timeout",
8948
+ "120"
8949
+ ]);
8950
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
8951
+ return;
8952
+ }
8953
+ const stagingDir = await (0, import_promises6.mkdtemp)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
8954
+ const tempPath = (0, import_node_path6.join)(stagingDir, "body.txt");
8955
+ try {
8956
+ await (0, import_promises6.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
8957
+ await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode);
8958
+ } finally {
8959
+ await (0, import_promises6.rm)(stagingDir, { force: true, recursive: true });
8960
+ }
8961
+ }
8962
+ async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode) {
8963
+ const maxAttempts = 4;
8964
+ const existedBeforeWrite = await vikingResourceExists2(ov, config, uri);
8965
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
8966
+ const existsNow = attempt === 0 ? existedBeforeWrite : await vikingResourceExists2(ov, config, uri);
8967
+ const ourWriteLanded = existsNow && !existedBeforeWrite;
8968
+ const mode = attempt === 0 ? initialMode : ourWriteLanded ? "replace" : initialMode;
8969
+ const args = withIdentity(config, [
8970
+ "write",
8971
+ uri,
8972
+ "--from-file",
8973
+ fromFile,
8974
+ "--mode",
8975
+ mode,
8976
+ "--wait",
8977
+ "--timeout",
8978
+ "120"
8979
+ ]);
8980
+ console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
8981
+ const result = await runCommand(ov, args, { allowFailure: true });
8982
+ if (result.exitCode === 0) {
8983
+ if (result.stdout.trim()) {
8984
+ console.log(result.stdout.trim());
8985
+ }
8986
+ if (result.stderr.trim()) {
8987
+ console.error(result.stderr.trim());
8988
+ }
8989
+ return;
8990
+ }
8991
+ if (isTransientOvFailure(result.stderr, result.stdout) && await vikingResourceExists2(ov, config, uri) && !existedBeforeWrite) {
8992
+ console.log("OpenViking accepted the write but returned an error before the wait completed; draining the queue.");
8993
+ await waitForOvQueue(ov, config);
8994
+ return;
8995
+ }
8996
+ if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
8997
+ throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
8998
+ }
8999
+ await sleep(1e3 * (attempt + 1));
9000
+ }
9001
+ }
9002
+ async function waitForOvQueue(ov, config) {
9003
+ const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
9004
+ if (result.stdout.trim()) {
9005
+ console.log(result.stdout.trim());
9006
+ }
9007
+ if (result.stderr.trim()) {
9008
+ console.error(result.stderr.trim());
9009
+ }
9010
+ }
9011
+ function isTransientOvFailure(stderr, stdout) {
9012
+ const output2 = `${stderr}
9013
+ ${stdout}`.toLowerCase();
9014
+ return output2.includes("resource is busy") || output2.includes("resource is being processed") || output2.includes("network error") || output2.includes("error sending request") || output2.includes("http request failed") || output2.includes("connection refused") || output2.includes("connection reset") || output2.includes("timed out");
9015
+ }
9016
+ async function ingestSingleFile(ov, config, uri, filePath, initialMode) {
9017
+ const content = await (0, import_promises6.readFile)(filePath, "utf8");
9018
+ await writeMemoryFile(config, ov, uri, content, initialMode, false);
9019
+ }
9020
+ async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
9021
+ try {
9022
+ await removeMemoryUri(config, ov, sourceUri, dryRun);
9023
+ } catch (sourceErr) {
9024
+ if (dryRun) {
9025
+ throw sourceErr;
9026
+ }
9027
+ console.error(
9028
+ `Source removal failed during ${label}; rolling back ${rollbackUri} so the system is back to the pre-${label} state.`
9029
+ );
9030
+ try {
9031
+ await removeMemoryUri(config, ov, rollbackUri, false);
9032
+ } catch (rollbackErr) {
9033
+ console.error(
9034
+ `Rollback of ${rollbackUri} also failed. Manual cleanup needed via: threadnote forget ${rollbackUri}
9035
+ Rollback error: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`
9036
+ );
9037
+ }
9038
+ await bestEffortRemoveWorktreeFile(rollbackUri, worktree, label);
9039
+ throw sourceErr;
9040
+ }
9041
+ }
9042
+ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
9043
+ if (label !== "publish") {
9044
+ return;
9045
+ }
9046
+ const prefix = "viking://";
9047
+ if (!rollbackUri.startsWith(prefix)) {
9048
+ return;
9049
+ }
9050
+ const parts = rollbackUri.slice(prefix.length).split("/");
9051
+ const sharedIndex = parts.indexOf("shared");
9052
+ if (sharedIndex === -1 || sharedIndex + 2 >= parts.length) {
9053
+ return;
9054
+ }
9055
+ const relative3 = parts.slice(sharedIndex + 2).join("/");
9056
+ if (!relative3) {
9057
+ return;
9058
+ }
9059
+ await (0, import_promises6.rm)((0, import_node_path6.join)(worktree, relative3), { force: true });
9060
+ }
9061
+ async function removeMemoryUri(config, ov, uri, dryRun) {
9062
+ const args = withIdentity(config, ["rm", uri]);
9063
+ if (dryRun) {
9064
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
9065
+ return;
9066
+ }
9067
+ const maxAttempts = 4;
9068
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
9069
+ const result = await runCommand(ov, args, { allowFailure: true });
9070
+ if (result.exitCode === 0) {
9071
+ if (result.stdout.trim()) {
9072
+ console.log(result.stdout.trim());
9073
+ }
9074
+ return;
9075
+ }
9076
+ if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
9077
+ throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
9078
+ }
9079
+ await sleep(1e3 * (attempt + 1));
9080
+ }
9081
+ }
9082
+ async function vikingResourceExists2(ov, config, uri) {
9083
+ const result = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
9084
+ return result.exitCode === 0;
9085
+ }
9086
+ async function hasUncommittedChanges(worktree) {
9087
+ const result = await runCommand("git", ["-C", worktree, "status", "--porcelain"], { allowFailure: true });
9088
+ return result.stdout.trim().length > 0;
9089
+ }
9090
+ async function gitOutput(worktree, args, dryRun) {
9091
+ if (dryRun) {
9092
+ return void 0;
9093
+ }
9094
+ const result = await runCommand("git", ["-C", worktree, ...args], { allowFailure: true });
9095
+ if (result.exitCode !== 0) {
9096
+ return void 0;
9097
+ }
9098
+ return result.stdout.trim();
9099
+ }
9100
+ async function listChangedFiles(worktree, beforeRev, afterRev) {
9101
+ const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
9102
+ allowFailure: true
9103
+ });
9104
+ if (result.exitCode !== 0) {
9105
+ return [];
9106
+ }
9107
+ const entries = result.stdout.split("\0").filter((part) => part.length > 0);
9108
+ const changes = [];
9109
+ for (let index = 0; index < entries.length; ) {
9110
+ const raw = entries[index];
9111
+ const head = raw.slice(0, 1);
9112
+ if (head === "R" || head === "C") {
9113
+ const oldRel = entries[index + 1];
9114
+ const newRel = entries[index + 2];
9115
+ if (oldRel && newRel) {
9116
+ changes.push({ path: (0, import_node_path6.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
9117
+ changes.push({ path: (0, import_node_path6.join)(worktree, newRel), relativePath: newRel, status: "added" });
9118
+ }
9119
+ index += 3;
9120
+ continue;
9121
+ }
9122
+ const rel = entries[index + 1];
9123
+ if (rel) {
9124
+ const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
9125
+ changes.push({ path: (0, import_node_path6.join)(worktree, rel), relativePath: rel, status });
9126
+ }
9127
+ index += 2;
9128
+ }
9129
+ return changes;
9130
+ }
9131
+ async function applyChangesToOpenViking(config, team, changes) {
9132
+ const ov = await openVikingCliForMode(false);
9133
+ for (const change of changes) {
9134
+ if (!change.relativePath.endsWith(".md")) {
9135
+ continue;
9136
+ }
9137
+ const firstSegment = change.relativePath.split("/")[0];
9138
+ if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
9139
+ continue;
9140
+ }
9141
+ const uri = workfileToVikingUri(config, team, change.path);
9142
+ if (change.status === "removed") {
9143
+ await removeMemoryUri(config, ov, uri, false);
9144
+ continue;
9145
+ }
9146
+ if (!await isFile(change.path)) {
9147
+ continue;
9148
+ }
9149
+ if (change.status === "modified" && await vikingResourceExists2(ov, config, uri)) {
9150
+ console.warn(
9151
+ `share sync: overwriting local ${uri} with the upstream version (local edits to the shared subtree are not preserved across sync).`
9152
+ );
9153
+ }
9154
+ await ensureSharedDirectoryChain(config, ov, uri, false);
9155
+ const writeMode = change.status === "modified" ? "replace" : "create";
9156
+ await ingestSingleFile(ov, config, uri, change.path, writeMode);
9157
+ }
9158
+ }
9159
+
8398
9160
  // src/lifecycle.ts
8399
9161
  var import_node_child_process2 = require("node:child_process");
8400
9162
  var import_node_fs5 = require("node:fs");
8401
- var import_promises8 = require("node:fs/promises");
8402
- var import_node_os5 = require("node:os");
8403
- var import_node_path7 = require("node:path");
9163
+ var import_promises9 = require("node:fs/promises");
9164
+ var import_node_os6 = require("node:os");
9165
+ var import_node_path8 = require("node:path");
8404
9166
 
8405
9167
  // src/update.ts
8406
9168
  var import_node_fs4 = require("node:fs");
8407
- var import_promises6 = require("node:fs/promises");
8408
- var import_node_os4 = require("node:os");
8409
- var import_node_path6 = require("node:path");
8410
- var import_promises7 = require("node:readline/promises");
9169
+ var import_promises7 = require("node:fs/promises");
9170
+ var import_node_os5 = require("node:os");
9171
+ var import_node_path7 = require("node:path");
9172
+ var import_promises8 = require("node:readline/promises");
8411
9173
  var import_node_process = require("node:process");
8412
9174
  var NPM_PACKAGE_NAME = "threadnote";
8413
9175
  var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
@@ -8555,7 +9317,7 @@ async function getUpdateInfo(config, options) {
8555
9317
  };
8556
9318
  }
8557
9319
  async function currentPackageVersion() {
8558
- const rawPackage = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), "package.json"), "utf8");
9320
+ const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
8559
9321
  const parsed = JSON.parse(rawPackage);
8560
9322
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8561
9323
  throw new Error("Could not read current threadnote package version.");
@@ -8605,11 +9367,11 @@ async function readFreshCache(config, registry) {
8605
9367
  }
8606
9368
  async function writeUpdateCache(config, cache) {
8607
9369
  await ensureDirectory(config.agentContextHome, false);
8608
- await (0, import_promises6.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
9370
+ await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
8609
9371
  `, { encoding: "utf8", mode: 384 });
8610
9372
  }
8611
9373
  function updateCachePath(config) {
8612
- return (0, import_node_path6.join)(config.agentContextHome, "update-check.json");
9374
+ return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
8613
9375
  }
8614
9376
  async function runApplicablePostUpdateMigrations(config, options) {
8615
9377
  const state = await readPostUpdateState(config);
@@ -8673,7 +9435,7 @@ async function applicablePostUpdateMigrations(config, options) {
8673
9435
  return applicable;
8674
9436
  }
8675
9437
  async function readPostUpdateMigrations() {
8676
- const raw = await readFileIfExists((0, import_node_path6.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
9438
+ const raw = await readFileIfExists((0, import_node_path7.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
8677
9439
  if (!raw) {
8678
9440
  return [];
8679
9441
  }
@@ -8712,7 +9474,7 @@ function printPostUpdateMigration(migration) {
8712
9474
  }
8713
9475
  }
8714
9476
  async function confirmPostUpdateMigration(prompt) {
8715
- const readline = (0, import_promises7.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
9477
+ const readline = (0, import_promises8.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
8716
9478
  try {
8717
9479
  const answer = (await readline.question(prompt)).trim().toLowerCase();
8718
9480
  return answer === "y" || answer === "yes";
@@ -8744,11 +9506,11 @@ async function readPostUpdateState(config) {
8744
9506
  }
8745
9507
  async function writePostUpdateState(config, state) {
8746
9508
  await ensureDirectory(config.agentContextHome, false);
8747
- await (0, import_promises6.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
9509
+ await (0, import_promises7.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
8748
9510
  `, { encoding: "utf8", mode: 384 });
8749
9511
  }
8750
9512
  function postUpdateStatePath(config) {
8751
- return (0, import_node_path6.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
9513
+ return (0, import_node_path7.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
8752
9514
  }
8753
9515
  async function resolveUpdateRuntime(runtime) {
8754
9516
  if (runtime !== "auto") {
@@ -8778,18 +9540,18 @@ async function runtimeThreadnoteBin(runtime) {
8778
9540
  if (runtime === "npm") {
8779
9541
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
8780
9542
  const prefix = result.stdout.trim();
8781
- return prefix ? (0, import_node_path6.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
9543
+ return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
8782
9544
  }
8783
9545
  if (runtime === "bun") {
8784
9546
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
8785
9547
  const binDir = result.stdout.trim();
8786
- return binDir ? (0, import_node_path6.join)(binDir, NPM_PACKAGE_NAME) : void 0;
9548
+ return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
8787
9549
  }
8788
- return (0, import_node_path6.join)(process.env.DENO_INSTALL ?? (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
9550
+ return (0, import_node_path7.join)(process.env.DENO_INSTALL ?? (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
8789
9551
  }
8790
9552
  async function isExecutable(path) {
8791
9553
  try {
8792
- await (0, import_promises6.access)(path, import_node_fs4.constants.X_OK);
9554
+ await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
8793
9555
  return true;
8794
9556
  } catch (_err) {
8795
9557
  return false;
@@ -8867,7 +9629,7 @@ function safeVersionNumber(value) {
8867
9629
  async function runDoctor(config, options) {
8868
9630
  const checks = [];
8869
9631
  checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
8870
- checks.push({ name: "platform", status: (0, import_node_os5.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os5.platform)() });
9632
+ checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
8871
9633
  checks.push(await commandCheck("node", ["--version"]));
8872
9634
  checks.push(await commandCheck("python3", ["--version"]));
8873
9635
  checks.push(await commandCheck("openviking-server", ["--help"]));
@@ -8880,9 +9642,9 @@ async function runDoctor(config, options) {
8880
9642
  checks.push(await commandShimCheck());
8881
9643
  checks.push(...await userAgentInstructionsChecks());
8882
9644
  checks.push(await manifestCheck(config.manifestPath));
8883
- checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
8884
- checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
8885
- checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
9645
+ checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
9646
+ checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
9647
+ checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
8886
9648
  checks.push(await healthCheck(config));
8887
9649
  for (const check of checks) {
8888
9650
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -8899,9 +9661,9 @@ async function runInstall(config, options) {
8899
9661
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
8900
9662
  const dryRun = options.dryRun === true;
8901
9663
  await ensureDirectory(config.agentContextHome, dryRun);
8902
- await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "logs"), dryRun);
8903
- await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "redacted"), dryRun);
8904
- await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "mcp"), dryRun);
9664
+ await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "logs"), dryRun);
9665
+ await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "redacted"), dryRun);
9666
+ await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "mcp"), dryRun);
8905
9667
  await installCommandShim(dryRun);
8906
9668
  await installUserAgentInstructions(dryRun);
8907
9669
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
@@ -8927,17 +9689,17 @@ async function runInstall(config, options) {
8927
9689
  }
8928
9690
  await writeTemplateIfMissing({
8929
9691
  config,
8930
- destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ov.conf"),
9692
+ destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ov.conf"),
8931
9693
  dryRun,
8932
9694
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
8933
- templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ov.conf.template.json")
9695
+ templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json")
8934
9696
  });
8935
9697
  await writeTemplateIfMissing({
8936
9698
  config,
8937
- destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ovcli.conf"),
9699
+ destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ovcli.conf"),
8938
9700
  dryRun,
8939
9701
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
8940
- templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ovcli.conf.template.json")
9702
+ templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json")
8941
9703
  });
8942
9704
  if (options.start !== false) {
8943
9705
  const healthy = await repairServerHealth(config, dryRun);
@@ -8987,7 +9749,7 @@ async function runUninstall(config, options) {
8987
9749
  }
8988
9750
  console.log("Uninstalling local Threadnote setup.");
8989
9751
  await runStop(config, { dryRun });
8990
- await removePathIfExists((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
9752
+ await removePathIfExists((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
8991
9753
  await removeLaunchAgent(dryRun);
8992
9754
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
8993
9755
  await removeMcpSnippets(config, dryRun);
@@ -9041,16 +9803,16 @@ async function repairManifest(config, dryRun) {
9041
9803
  console.log(output2.trimEnd());
9042
9804
  return;
9043
9805
  }
9044
- await ensureDirectory((0, import_node_path7.dirname)(config.manifestPath), false);
9806
+ await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
9045
9807
  const currentContent = await readFileIfExists(config.manifestPath);
9046
9808
  if (currentContent !== void 0) {
9047
9809
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
9048
- await (0, import_promises8.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
9049
- await (0, import_promises8.chmod)(backupPath, 384);
9810
+ await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
9811
+ await (0, import_promises9.chmod)(backupPath, 384);
9050
9812
  console.log(`Backup: ${backupPath}`);
9051
9813
  }
9052
- await (0, import_promises8.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
9053
- await (0, import_promises8.chmod)(config.manifestPath, 384);
9814
+ await (0, import_promises9.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
9815
+ await (0, import_promises9.chmod)(config.manifestPath, 384);
9054
9816
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
9055
9817
  }
9056
9818
  async function repairServerHealth(config, dryRun) {
@@ -9091,7 +9853,7 @@ async function runStart(config, options) {
9091
9853
  );
9092
9854
  }
9093
9855
  const logPath = openVikingLogPath(config);
9094
- await ensureDirectory((0, import_node_path7.dirname)(logPath), false);
9856
+ await ensureDirectory((0, import_node_path8.dirname)(logPath), false);
9095
9857
  if (options.foreground === true) {
9096
9858
  const result = await runInteractive(server, args);
9097
9859
  process.exitCode = result;
@@ -9100,7 +9862,7 @@ async function runStart(config, options) {
9100
9862
  const logFd = (0, import_node_fs5.openSync)(logPath, "a");
9101
9863
  const child = spawnDetachedServerWithLog(server, args, logFd);
9102
9864
  child.unref();
9103
- await (0, import_promises8.writeFile)((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
9865
+ await (0, import_promises9.writeFile)((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
9104
9866
  `, "utf8");
9105
9867
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
9106
9868
  if (health) {
@@ -9126,14 +9888,14 @@ function spawnDetachedServer(server, args, logFd) {
9126
9888
  }
9127
9889
  async function runStop(config, options) {
9128
9890
  const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
9129
- if ((0, import_node_os5.platform)() === "darwin") {
9891
+ if ((0, import_node_os6.platform)() === "darwin") {
9130
9892
  if (options.dryRun === true || await exists(launchAgentPath)) {
9131
9893
  await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
9132
9894
  } else {
9133
9895
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
9134
9896
  }
9135
9897
  }
9136
- const pidPath = (0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid");
9898
+ const pidPath = (0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid");
9137
9899
  const pidText = await readFileIfExists(pidPath);
9138
9900
  if (!pidText) {
9139
9901
  console.log("No pid file found for detached OpenViking server.");
@@ -9229,7 +9991,7 @@ async function pythonSystemCertificatesCheck() {
9229
9991
  };
9230
9992
  }
9231
9993
  async function commandShimCheck() {
9232
- const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9994
+ const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9233
9995
  const content = await readFileIfExists(shimPath);
9234
9996
  if (content === void 0) {
9235
9997
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -9295,11 +10057,11 @@ async function hasPythonModule(serverPath, moduleName) {
9295
10057
  async function siblingPythonForExecutable(executablePath) {
9296
10058
  let resolvedPath;
9297
10059
  try {
9298
- resolvedPath = await (0, import_promises8.realpath)(executablePath);
10060
+ resolvedPath = await (0, import_promises9.realpath)(executablePath);
9299
10061
  } catch (_err) {
9300
10062
  return void 0;
9301
10063
  }
9302
- const pythonPath = (0, import_node_path7.join)((0, import_node_path7.dirname)(resolvedPath), "python");
10064
+ const pythonPath = (0, import_node_path8.join)((0, import_node_path8.dirname)(resolvedPath), "python");
9303
10065
  return await exists(pythonPath) ? pythonPath : void 0;
9304
10066
  }
9305
10067
  async function manifestCheck(path) {
@@ -9421,38 +10183,38 @@ function printInstallNextSteps(options) {
9421
10183
  }
9422
10184
  async function writeTemplateIfMissing(options) {
9423
10185
  if (await exists(options.destinationPath)) {
9424
- const currentContent = await (0, import_promises8.readFile)(options.destinationPath, "utf8");
10186
+ const currentContent = await (0, import_promises9.readFile)(options.destinationPath, "utf8");
9425
10187
  if (options.shouldRepair?.(currentContent) !== true) {
9426
10188
  console.log(`Already exists: ${options.destinationPath}`);
9427
10189
  return;
9428
10190
  }
9429
- const rendered2 = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
10191
+ const rendered2 = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
9430
10192
  if (options.dryRun) {
9431
10193
  console.log(`Would repair generated config: ${options.destinationPath}`);
9432
10194
  return;
9433
10195
  }
9434
10196
  const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
9435
- await (0, import_promises8.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
9436
- await (0, import_promises8.chmod)(backupPath, 384);
9437
- await (0, import_promises8.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
9438
- await (0, import_promises8.chmod)(options.destinationPath, 384);
10197
+ await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10198
+ await (0, import_promises9.chmod)(backupPath, 384);
10199
+ await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
10200
+ await (0, import_promises9.chmod)(options.destinationPath, 384);
9439
10201
  console.log(`Repaired generated config: ${options.destinationPath}`);
9440
10202
  console.log(`Backup: ${backupPath}`);
9441
10203
  return;
9442
10204
  }
9443
- const rendered = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
10205
+ const rendered = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
9444
10206
  if (options.dryRun) {
9445
10207
  console.log(`Would write ${options.destinationPath}`);
9446
10208
  return;
9447
10209
  }
9448
- await ensureDirectory((0, import_node_path7.dirname)(options.destinationPath), false);
9449
- await (0, import_promises8.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
9450
- await (0, import_promises8.chmod)(options.destinationPath, 384);
10210
+ await ensureDirectory((0, import_node_path8.dirname)(options.destinationPath), false);
10211
+ await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
10212
+ await (0, import_promises9.chmod)(options.destinationPath, 384);
9451
10213
  console.log(`Wrote ${options.destinationPath}`);
9452
10214
  }
9453
10215
  async function installCommandShim(dryRun) {
9454
10216
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
9455
- const shimPath = (0, import_node_path7.join)(binDir, "threadnote");
10217
+ const shimPath = (0, import_node_path8.join)(binDir, "threadnote");
9456
10218
  const existingContent = await readFileIfExists(shimPath);
9457
10219
  if (existingContent && !isManagedCommandShim(existingContent)) {
9458
10220
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -9468,12 +10230,12 @@ async function installCommandShim(dryRun) {
9468
10230
  return;
9469
10231
  }
9470
10232
  await ensureDirectory(binDir, false);
9471
- await (0, import_promises8.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
9472
- await (0, import_promises8.chmod)(shimPath, 493);
10233
+ await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10234
+ await (0, import_promises9.chmod)(shimPath, 493);
9473
10235
  console.log(`Wrote command shim: ${shimPath}`);
9474
10236
  }
9475
10237
  async function removeCommandShim(dryRun) {
9476
- const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10238
+ const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9477
10239
  const content = await readFileIfExists(shimPath);
9478
10240
  if (content === void 0) {
9479
10241
  console.log(`Already absent: ${shimPath}`);
@@ -9507,8 +10269,8 @@ async function installUserAgentInstructions(dryRun) {
9507
10269
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
9508
10270
  continue;
9509
10271
  }
9510
- await ensureDirectory((0, import_node_path7.dirname)(targetPath), false);
9511
- await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10272
+ await ensureDirectory((0, import_node_path8.dirname)(targetPath), false);
10273
+ await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
9512
10274
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
9513
10275
  }
9514
10276
  }
@@ -9545,7 +10307,7 @@ async function removeUserAgentInstructions(dryRun) {
9545
10307
  console.log(`Would update ${targetPath}`);
9546
10308
  continue;
9547
10309
  }
9548
- await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10310
+ await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
9549
10311
  console.log(`Updated ${targetPath}`);
9550
10312
  }
9551
10313
  }
@@ -9565,7 +10327,7 @@ async function renderUserAgentInstructions(target) {
9565
10327
  ].join("\n");
9566
10328
  }
9567
10329
  async function renderUserAgentInstructionsBlock() {
9568
- const instructions = (await (0, import_promises8.readFile)((0, import_node_path7.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
10330
+ const instructions = (await (0, import_promises9.readFile)((0, import_node_path8.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
9569
10331
  return `${USER_INSTRUCTIONS_START_MARKER}
9570
10332
  ${instructions}
9571
10333
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -9640,12 +10402,12 @@ function isManagedCommandShim(content) {
9640
10402
  return content.includes(SHIM_MARKER);
9641
10403
  }
9642
10404
  async function installLaunchAgent(config, dryRun) {
9643
- if ((0, import_node_os5.platform)() !== "darwin") {
10405
+ if ((0, import_node_os6.platform)() !== "darwin") {
9644
10406
  throw new Error("launchd autostart is only supported on macOS.");
9645
10407
  }
9646
- const source = (0, import_node_path7.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
10408
+ const source = (0, import_node_path8.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
9647
10409
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
9648
- const rendered = renderTemplate(await (0, import_promises8.readFile)(source, "utf8"), config);
10410
+ const rendered = renderTemplate(await (0, import_promises9.readFile)(source, "utf8"), config);
9649
10411
  if (dryRun) {
9650
10412
  console.log(`Would write ${destination}`);
9651
10413
  console.log(`Would run: launchctl unload ${destination}`);
@@ -9653,9 +10415,9 @@ async function installLaunchAgent(config, dryRun) {
9653
10415
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
9654
10416
  return;
9655
10417
  }
9656
- await ensureDirectory((0, import_node_path7.dirname)(destination), false);
9657
- await ensureDirectory((0, import_node_path7.dirname)(openVikingLogPath(config)), false);
9658
- await (0, import_promises8.writeFile)(destination, rendered, "utf8");
10418
+ await ensureDirectory((0, import_node_path8.dirname)(destination), false);
10419
+ await ensureDirectory((0, import_node_path8.dirname)(openVikingLogPath(config)), false);
10420
+ await (0, import_promises9.writeFile)(destination, rendered, "utf8");
9659
10421
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
9660
10422
  await maybeRun(false, "launchctl", ["load", destination]);
9661
10423
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
@@ -9718,7 +10480,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
9718
10480
  if (typeof parsed.default_user !== "string") {
9719
10481
  return false;
9720
10482
  }
9721
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path7.join)(config.agentContextHome, "data")) {
10483
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path8.join)(config.agentContextHome, "data")) {
9722
10484
  return false;
9723
10485
  }
9724
10486
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -9825,6 +10587,31 @@ async function main() {
9825
10587
  program2.command("forget").description("Remove a viking:// URI from local OpenViking context").argument("<uri>", "viking:// URI to remove").option("--dry-run", "Print ov command without deleting").action(async (uri, options) => {
9826
10588
  await runForget(getRuntimeConfig(program2), uri, options);
9827
10589
  });
10590
+ const share = program2.command("share").description("Share durable memories with teammates through a git-backed repository");
10591
+ share.command("init").description("Configure a shared memories repo for a team (clones the remote into the local memory tree)").argument("<remote-url>", "git remote URL of the shared memories repo").option("--team <name>", 'Team name; defaults to "default"').option("--set-default", "Mark this team as the default for future share commands").option(
10592
+ "--no-push",
10593
+ "Do not push the auto-generated .gitignore housekeeping commit. Does not affect future publishes."
10594
+ ).option("--dry-run", "Print actions without running them").action(async (remoteUrl, options) => {
10595
+ await runShareInit(getRuntimeConfig(program2), remoteUrl, options);
10596
+ });
10597
+ share.command("status").description("Show git status and ahead/behind counts for a shared team").option("--team <name>", "Team name; defaults to the configured default team").option("--dry-run", "Print git commands without running them").action(async (options) => {
10598
+ await runShareStatus(getRuntimeConfig(program2), options);
10599
+ });
10600
+ share.command("sync").description("Pull, reindex, and push the shared memories repo for a team").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message when auto-committing local edits").option("--no-auto-commit", "Refuse to sync if there are uncommitted local changes").option("--no-push", "Skip the push step after pulling and reindexing").option("--dry-run", "Print actions without running them").action(async (options) => {
10601
+ await runShareSync(getRuntimeConfig(program2), options);
10602
+ });
10603
+ share.command("publish").description("Move a personal memory into the shared team namespace, commit and push").argument("<viking-uri>", "viking:// memory URI to publish").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) => {
10604
+ await runSharePublish(getRuntimeConfig(program2), uri, options);
10605
+ });
10606
+ 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) => {
10607
+ await runShareUnpublish(getRuntimeConfig(program2), uri, options);
10608
+ });
10609
+ share.command("list").description("List configured shared teams").option("--dry-run", "Print without side effects (no-op for list)").action(async (options) => {
10610
+ await runShareList(getRuntimeConfig(program2), options);
10611
+ });
10612
+ share.command("remove").description("Forget a configured team and optionally delete its worktree/gitdir").option("--team <name>", "Team name; defaults to the configured default team").option("--keep-files", "Keep the worktree and gitdir on disk; only forget the team entry").option("--dry-run", "Print actions without running them").action(async (options) => {
10613
+ await runShareRemove(getRuntimeConfig(program2), options);
10614
+ });
9828
10615
  program2.command("export-pack").description("Export local OpenViking context to an .ovpack").option("--dry-run", "Print ov command without exporting").option("--path <path>", "Output .ovpack path").option("--uri <uri>", "Source viking:// URI to export", "viking://").action(async (options) => {
9829
10616
  await runExportPack(getRuntimeConfig(program2), options);
9830
10617
  });