threadnote 0.3.8 → 0.4.1

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,782 @@ 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");
9166
+ var import_node_process2 = require("node:process");
9167
+ var import_promises10 = require("node:readline/promises");
8404
9168
 
8405
9169
  // src/update.ts
8406
9170
  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");
9171
+ var import_promises7 = require("node:fs/promises");
9172
+ var import_node_os5 = require("node:os");
9173
+ var import_node_path7 = require("node:path");
9174
+ var import_promises8 = require("node:readline/promises");
8411
9175
  var import_node_process = require("node:process");
8412
9176
  var NPM_PACKAGE_NAME = "threadnote";
8413
9177
  var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
@@ -8555,7 +9319,7 @@ async function getUpdateInfo(config, options) {
8555
9319
  };
8556
9320
  }
8557
9321
  async function currentPackageVersion() {
8558
- const rawPackage = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), "package.json"), "utf8");
9322
+ const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
8559
9323
  const parsed = JSON.parse(rawPackage);
8560
9324
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8561
9325
  throw new Error("Could not read current threadnote package version.");
@@ -8605,11 +9369,11 @@ async function readFreshCache(config, registry) {
8605
9369
  }
8606
9370
  async function writeUpdateCache(config, cache) {
8607
9371
  await ensureDirectory(config.agentContextHome, false);
8608
- await (0, import_promises6.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
9372
+ await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
8609
9373
  `, { encoding: "utf8", mode: 384 });
8610
9374
  }
8611
9375
  function updateCachePath(config) {
8612
- return (0, import_node_path6.join)(config.agentContextHome, "update-check.json");
9376
+ return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
8613
9377
  }
8614
9378
  async function runApplicablePostUpdateMigrations(config, options) {
8615
9379
  const state = await readPostUpdateState(config);
@@ -8673,7 +9437,7 @@ async function applicablePostUpdateMigrations(config, options) {
8673
9437
  return applicable;
8674
9438
  }
8675
9439
  async function readPostUpdateMigrations() {
8676
- const raw = await readFileIfExists((0, import_node_path6.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
9440
+ const raw = await readFileIfExists((0, import_node_path7.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
8677
9441
  if (!raw) {
8678
9442
  return [];
8679
9443
  }
@@ -8712,7 +9476,7 @@ function printPostUpdateMigration(migration) {
8712
9476
  }
8713
9477
  }
8714
9478
  async function confirmPostUpdateMigration(prompt) {
8715
- const readline = (0, import_promises7.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
9479
+ const readline = (0, import_promises8.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
8716
9480
  try {
8717
9481
  const answer = (await readline.question(prompt)).trim().toLowerCase();
8718
9482
  return answer === "y" || answer === "yes";
@@ -8744,11 +9508,11 @@ async function readPostUpdateState(config) {
8744
9508
  }
8745
9509
  async function writePostUpdateState(config, state) {
8746
9510
  await ensureDirectory(config.agentContextHome, false);
8747
- await (0, import_promises6.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
9511
+ await (0, import_promises7.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
8748
9512
  `, { encoding: "utf8", mode: 384 });
8749
9513
  }
8750
9514
  function postUpdateStatePath(config) {
8751
- return (0, import_node_path6.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
9515
+ return (0, import_node_path7.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
8752
9516
  }
8753
9517
  async function resolveUpdateRuntime(runtime) {
8754
9518
  if (runtime !== "auto") {
@@ -8778,18 +9542,18 @@ async function runtimeThreadnoteBin(runtime) {
8778
9542
  if (runtime === "npm") {
8779
9543
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
8780
9544
  const prefix = result.stdout.trim();
8781
- return prefix ? (0, import_node_path6.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
9545
+ return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
8782
9546
  }
8783
9547
  if (runtime === "bun") {
8784
9548
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
8785
9549
  const binDir = result.stdout.trim();
8786
- return binDir ? (0, import_node_path6.join)(binDir, NPM_PACKAGE_NAME) : void 0;
9550
+ return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
8787
9551
  }
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);
9552
+ 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
9553
  }
8790
9554
  async function isExecutable(path) {
8791
9555
  try {
8792
- await (0, import_promises6.access)(path, import_node_fs4.constants.X_OK);
9556
+ await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
8793
9557
  return true;
8794
9558
  } catch (_err) {
8795
9559
  return false;
@@ -8867,7 +9631,7 @@ function safeVersionNumber(value) {
8867
9631
  async function runDoctor(config, options) {
8868
9632
  const checks = [];
8869
9633
  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)() });
9634
+ checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
8871
9635
  checks.push(await commandCheck("node", ["--version"]));
8872
9636
  checks.push(await commandCheck("python3", ["--version"]));
8873
9637
  checks.push(await commandCheck("openviking-server", ["--help"]));
@@ -8880,9 +9644,9 @@ async function runDoctor(config, options) {
8880
9644
  checks.push(await commandShimCheck());
8881
9645
  checks.push(...await userAgentInstructionsChecks());
8882
9646
  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"));
9647
+ checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
9648
+ checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
9649
+ checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
8886
9650
  checks.push(await healthCheck(config));
8887
9651
  for (const check of checks) {
8888
9652
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -8899,9 +9663,9 @@ async function runInstall(config, options) {
8899
9663
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
8900
9664
  const dryRun = options.dryRun === true;
8901
9665
  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);
9666
+ await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "logs"), dryRun);
9667
+ await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "redacted"), dryRun);
9668
+ await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "mcp"), dryRun);
8905
9669
  await installCommandShim(dryRun);
8906
9670
  await installUserAgentInstructions(dryRun);
8907
9671
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
@@ -8927,17 +9691,17 @@ async function runInstall(config, options) {
8927
9691
  }
8928
9692
  await writeTemplateIfMissing({
8929
9693
  config,
8930
- destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ov.conf"),
9694
+ destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ov.conf"),
8931
9695
  dryRun,
8932
9696
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
8933
- templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ov.conf.template.json")
9697
+ templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json")
8934
9698
  });
8935
9699
  await writeTemplateIfMissing({
8936
9700
  config,
8937
- destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ovcli.conf"),
9701
+ destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ovcli.conf"),
8938
9702
  dryRun,
8939
9703
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
8940
- templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ovcli.conf.template.json")
9704
+ templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json")
8941
9705
  });
8942
9706
  if (options.start !== false) {
8943
9707
  const healthy = await repairServerHealth(config, dryRun);
@@ -8987,7 +9751,7 @@ async function runUninstall(config, options) {
8987
9751
  }
8988
9752
  console.log("Uninstalling local Threadnote setup.");
8989
9753
  await runStop(config, { dryRun });
8990
- await removePathIfExists((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
9754
+ await removePathIfExists((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
8991
9755
  await removeLaunchAgent(dryRun);
8992
9756
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
8993
9757
  await removeMcpSnippets(config, dryRun);
@@ -9041,16 +9805,16 @@ async function repairManifest(config, dryRun) {
9041
9805
  console.log(output2.trimEnd());
9042
9806
  return;
9043
9807
  }
9044
- await ensureDirectory((0, import_node_path7.dirname)(config.manifestPath), false);
9808
+ await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
9045
9809
  const currentContent = await readFileIfExists(config.manifestPath);
9046
9810
  if (currentContent !== void 0) {
9047
9811
  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);
9812
+ await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
9813
+ await (0, import_promises9.chmod)(backupPath, 384);
9050
9814
  console.log(`Backup: ${backupPath}`);
9051
9815
  }
9052
- await (0, import_promises8.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
9053
- await (0, import_promises8.chmod)(config.manifestPath, 384);
9816
+ await (0, import_promises9.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
9817
+ await (0, import_promises9.chmod)(config.manifestPath, 384);
9054
9818
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
9055
9819
  }
9056
9820
  async function repairServerHealth(config, dryRun) {
@@ -9091,7 +9855,7 @@ async function runStart(config, options) {
9091
9855
  );
9092
9856
  }
9093
9857
  const logPath = openVikingLogPath(config);
9094
- await ensureDirectory((0, import_node_path7.dirname)(logPath), false);
9858
+ await ensureDirectory((0, import_node_path8.dirname)(logPath), false);
9095
9859
  if (options.foreground === true) {
9096
9860
  const result = await runInteractive(server, args);
9097
9861
  process.exitCode = result;
@@ -9100,7 +9864,7 @@ async function runStart(config, options) {
9100
9864
  const logFd = (0, import_node_fs5.openSync)(logPath, "a");
9101
9865
  const child = spawnDetachedServerWithLog(server, args, logFd);
9102
9866
  child.unref();
9103
- await (0, import_promises8.writeFile)((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
9867
+ await (0, import_promises9.writeFile)((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
9104
9868
  `, "utf8");
9105
9869
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
9106
9870
  if (health) {
@@ -9126,14 +9890,14 @@ function spawnDetachedServer(server, args, logFd) {
9126
9890
  }
9127
9891
  async function runStop(config, options) {
9128
9892
  const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
9129
- if ((0, import_node_os5.platform)() === "darwin") {
9893
+ if ((0, import_node_os6.platform)() === "darwin") {
9130
9894
  if (options.dryRun === true || await exists(launchAgentPath)) {
9131
9895
  await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
9132
9896
  } else {
9133
9897
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
9134
9898
  }
9135
9899
  }
9136
- const pidPath = (0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid");
9900
+ const pidPath = (0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid");
9137
9901
  const pidText = await readFileIfExists(pidPath);
9138
9902
  if (!pidText) {
9139
9903
  console.log("No pid file found for detached OpenViking server.");
@@ -9229,7 +9993,7 @@ async function pythonSystemCertificatesCheck() {
9229
9993
  };
9230
9994
  }
9231
9995
  async function commandShimCheck() {
9232
- const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9996
+ const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9233
9997
  const content = await readFileIfExists(shimPath);
9234
9998
  if (content === void 0) {
9235
9999
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -9295,11 +10059,11 @@ async function hasPythonModule(serverPath, moduleName) {
9295
10059
  async function siblingPythonForExecutable(executablePath) {
9296
10060
  let resolvedPath;
9297
10061
  try {
9298
- resolvedPath = await (0, import_promises8.realpath)(executablePath);
10062
+ resolvedPath = await (0, import_promises9.realpath)(executablePath);
9299
10063
  } catch (_err) {
9300
10064
  return void 0;
9301
10065
  }
9302
- const pythonPath = (0, import_node_path7.join)((0, import_node_path7.dirname)(resolvedPath), "python");
10066
+ const pythonPath = (0, import_node_path8.join)((0, import_node_path8.dirname)(resolvedPath), "python");
9303
10067
  return await exists(pythonPath) ? pythonPath : void 0;
9304
10068
  }
9305
10069
  async function manifestCheck(path) {
@@ -9348,11 +10112,83 @@ async function waitForOpenVikingHealth(config, timeoutMs) {
9348
10112
  return void 0;
9349
10113
  }
9350
10114
  async function runInstallCommands(config, preferred, force, dryRun) {
9351
- const installCommands = await getInstallCommands(config, preferred, force);
10115
+ let manager = preferred;
10116
+ if (manager === void 0 && !dryRun) {
10117
+ const detected = await detectPackageManager();
10118
+ if (detected === "pip" && await offerToInstallUv()) {
10119
+ const rediscovered = await detectPackageManager();
10120
+ if (rediscovered === "uv") {
10121
+ manager = "uv";
10122
+ }
10123
+ }
10124
+ }
10125
+ const installCommands = await getInstallCommands(config, manager, force);
9352
10126
  for (const installCommand of installCommands) {
9353
10127
  await maybeRun(dryRun, installCommand.executable, installCommand.args);
9354
10128
  }
9355
10129
  }
10130
+ async function offerToInstallUv() {
10131
+ if (import_node_process2.stdin.isTTY !== true || import_node_process2.stdout.isTTY !== true) {
10132
+ console.warn(
10133
+ "Neither uv nor pipx was found on PATH. Falling back to `python3 -m pip install --user`, which fails on PEP 668 (Homebrew/system) Python.\nRe-run with --package-manager uv after installing uv (brew install uv), or pass --package-manager pipx."
10134
+ );
10135
+ return false;
10136
+ }
10137
+ const readline = (0, import_promises10.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
10138
+ let answer;
10139
+ try {
10140
+ answer = (await readline.question(
10141
+ "OpenViking installs into Python; neither uv nor pipx is on PATH so threadnote would fall back to `pip install --user`, which fails on PEP 668 setups.\nInstall uv now? [Y/n] "
10142
+ )).trim().toLowerCase();
10143
+ } finally {
10144
+ readline.close();
10145
+ }
10146
+ if (answer === "n" || answer === "no") {
10147
+ console.log("Continuing with `python3 -m pip install --user`. You may hit PEP 668 errors on managed Pythons.");
10148
+ return false;
10149
+ }
10150
+ return await installUv();
10151
+ }
10152
+ async function installUv() {
10153
+ const brew = await findExecutable(["brew"]);
10154
+ if (brew) {
10155
+ console.log("Installing uv via Homebrew...");
10156
+ const result = await runCommand(brew, ["install", "uv"], { allowFailure: true });
10157
+ if (result.exitCode === 0) {
10158
+ if (result.stdout.trim()) {
10159
+ console.log(result.stdout.trim());
10160
+ }
10161
+ if (await findExecutable(["uv"])) {
10162
+ return true;
10163
+ }
10164
+ } else {
10165
+ console.warn(`brew install uv failed: ${(result.stderr || result.stdout).trim()}`);
10166
+ }
10167
+ }
10168
+ if (await findExecutable(["curl"]) && await findExecutable(["sh"])) {
10169
+ console.log("Installing uv via the official install script (curl -LsSf https://astral.sh/uv/install.sh | sh)...");
10170
+ const result = await runCommand("sh", ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"], {
10171
+ allowFailure: true
10172
+ });
10173
+ if (result.exitCode === 0) {
10174
+ if (result.stdout.trim()) {
10175
+ console.log(result.stdout.trim());
10176
+ }
10177
+ if (await findExecutable(["uv"])) {
10178
+ return true;
10179
+ }
10180
+ console.warn(
10181
+ "uv installed, but the new binary is not yet on this shell PATH. Open a new shell (or `source ~/.zshrc` / `source ~/.bashrc`) and re-run `threadnote install`."
10182
+ );
10183
+ return false;
10184
+ }
10185
+ console.warn(`uv install script failed: ${(result.stderr || result.stdout).trim()}`);
10186
+ }
10187
+ console.warn(
10188
+ "Could not install uv automatically. Install it manually (brew install uv) and re-run threadnote install."
10189
+ );
10190
+ return false;
10191
+ }
9356
10192
  async function getPythonSystemCertificatesInstallCommand(serverPath) {
9357
10193
  const pythonPath = await siblingPythonForExecutable(serverPath);
9358
10194
  if (!pythonPath) {
@@ -9421,38 +10257,38 @@ function printInstallNextSteps(options) {
9421
10257
  }
9422
10258
  async function writeTemplateIfMissing(options) {
9423
10259
  if (await exists(options.destinationPath)) {
9424
- const currentContent = await (0, import_promises8.readFile)(options.destinationPath, "utf8");
10260
+ const currentContent = await (0, import_promises9.readFile)(options.destinationPath, "utf8");
9425
10261
  if (options.shouldRepair?.(currentContent) !== true) {
9426
10262
  console.log(`Already exists: ${options.destinationPath}`);
9427
10263
  return;
9428
10264
  }
9429
- const rendered2 = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
10265
+ const rendered2 = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
9430
10266
  if (options.dryRun) {
9431
10267
  console.log(`Would repair generated config: ${options.destinationPath}`);
9432
10268
  return;
9433
10269
  }
9434
10270
  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);
10271
+ await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10272
+ await (0, import_promises9.chmod)(backupPath, 384);
10273
+ await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
10274
+ await (0, import_promises9.chmod)(options.destinationPath, 384);
9439
10275
  console.log(`Repaired generated config: ${options.destinationPath}`);
9440
10276
  console.log(`Backup: ${backupPath}`);
9441
10277
  return;
9442
10278
  }
9443
- const rendered = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
10279
+ const rendered = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
9444
10280
  if (options.dryRun) {
9445
10281
  console.log(`Would write ${options.destinationPath}`);
9446
10282
  return;
9447
10283
  }
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);
10284
+ await ensureDirectory((0, import_node_path8.dirname)(options.destinationPath), false);
10285
+ await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
10286
+ await (0, import_promises9.chmod)(options.destinationPath, 384);
9451
10287
  console.log(`Wrote ${options.destinationPath}`);
9452
10288
  }
9453
10289
  async function installCommandShim(dryRun) {
9454
10290
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
9455
- const shimPath = (0, import_node_path7.join)(binDir, "threadnote");
10291
+ const shimPath = (0, import_node_path8.join)(binDir, "threadnote");
9456
10292
  const existingContent = await readFileIfExists(shimPath);
9457
10293
  if (existingContent && !isManagedCommandShim(existingContent)) {
9458
10294
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -9468,12 +10304,12 @@ async function installCommandShim(dryRun) {
9468
10304
  return;
9469
10305
  }
9470
10306
  await ensureDirectory(binDir, false);
9471
- await (0, import_promises8.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
9472
- await (0, import_promises8.chmod)(shimPath, 493);
10307
+ await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10308
+ await (0, import_promises9.chmod)(shimPath, 493);
9473
10309
  console.log(`Wrote command shim: ${shimPath}`);
9474
10310
  }
9475
10311
  async function removeCommandShim(dryRun) {
9476
- const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10312
+ const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9477
10313
  const content = await readFileIfExists(shimPath);
9478
10314
  if (content === void 0) {
9479
10315
  console.log(`Already absent: ${shimPath}`);
@@ -9507,8 +10343,8 @@ async function installUserAgentInstructions(dryRun) {
9507
10343
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
9508
10344
  continue;
9509
10345
  }
9510
- await ensureDirectory((0, import_node_path7.dirname)(targetPath), false);
9511
- await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10346
+ await ensureDirectory((0, import_node_path8.dirname)(targetPath), false);
10347
+ await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
9512
10348
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
9513
10349
  }
9514
10350
  }
@@ -9545,7 +10381,7 @@ async function removeUserAgentInstructions(dryRun) {
9545
10381
  console.log(`Would update ${targetPath}`);
9546
10382
  continue;
9547
10383
  }
9548
- await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10384
+ await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
9549
10385
  console.log(`Updated ${targetPath}`);
9550
10386
  }
9551
10387
  }
@@ -9565,7 +10401,7 @@ async function renderUserAgentInstructions(target) {
9565
10401
  ].join("\n");
9566
10402
  }
9567
10403
  async function renderUserAgentInstructionsBlock() {
9568
- const instructions = (await (0, import_promises8.readFile)((0, import_node_path7.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
10404
+ const instructions = (await (0, import_promises9.readFile)((0, import_node_path8.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
9569
10405
  return `${USER_INSTRUCTIONS_START_MARKER}
9570
10406
  ${instructions}
9571
10407
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -9640,12 +10476,12 @@ function isManagedCommandShim(content) {
9640
10476
  return content.includes(SHIM_MARKER);
9641
10477
  }
9642
10478
  async function installLaunchAgent(config, dryRun) {
9643
- if ((0, import_node_os5.platform)() !== "darwin") {
10479
+ if ((0, import_node_os6.platform)() !== "darwin") {
9644
10480
  throw new Error("launchd autostart is only supported on macOS.");
9645
10481
  }
9646
- const source = (0, import_node_path7.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
10482
+ const source = (0, import_node_path8.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
9647
10483
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
9648
- const rendered = renderTemplate(await (0, import_promises8.readFile)(source, "utf8"), config);
10484
+ const rendered = renderTemplate(await (0, import_promises9.readFile)(source, "utf8"), config);
9649
10485
  if (dryRun) {
9650
10486
  console.log(`Would write ${destination}`);
9651
10487
  console.log(`Would run: launchctl unload ${destination}`);
@@ -9653,9 +10489,9 @@ async function installLaunchAgent(config, dryRun) {
9653
10489
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
9654
10490
  return;
9655
10491
  }
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");
10492
+ await ensureDirectory((0, import_node_path8.dirname)(destination), false);
10493
+ await ensureDirectory((0, import_node_path8.dirname)(openVikingLogPath(config)), false);
10494
+ await (0, import_promises9.writeFile)(destination, rendered, "utf8");
9659
10495
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
9660
10496
  await maybeRun(false, "launchctl", ["load", destination]);
9661
10497
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
@@ -9718,7 +10554,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
9718
10554
  if (typeof parsed.default_user !== "string") {
9719
10555
  return false;
9720
10556
  }
9721
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path7.join)(config.agentContextHome, "data")) {
10557
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path8.join)(config.agentContextHome, "data")) {
9722
10558
  return false;
9723
10559
  }
9724
10560
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -9825,6 +10661,31 @@ async function main() {
9825
10661
  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
10662
  await runForget(getRuntimeConfig(program2), uri, options);
9827
10663
  });
10664
+ const share = program2.command("share").description("Share durable memories with teammates through a git-backed repository");
10665
+ 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(
10666
+ "--no-push",
10667
+ "Do not push the auto-generated .gitignore housekeeping commit. Does not affect future publishes."
10668
+ ).option("--dry-run", "Print actions without running them").action(async (remoteUrl, options) => {
10669
+ await runShareInit(getRuntimeConfig(program2), remoteUrl, options);
10670
+ });
10671
+ 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) => {
10672
+ await runShareStatus(getRuntimeConfig(program2), options);
10673
+ });
10674
+ 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) => {
10675
+ await runShareSync(getRuntimeConfig(program2), options);
10676
+ });
10677
+ 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) => {
10678
+ await runSharePublish(getRuntimeConfig(program2), uri, options);
10679
+ });
10680
+ 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) => {
10681
+ await runShareUnpublish(getRuntimeConfig(program2), uri, options);
10682
+ });
10683
+ share.command("list").description("List configured shared teams").option("--dry-run", "Print without side effects (no-op for list)").action(async (options) => {
10684
+ await runShareList(getRuntimeConfig(program2), options);
10685
+ });
10686
+ 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) => {
10687
+ await runShareRemove(getRuntimeConfig(program2), options);
10688
+ });
9828
10689
  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
10690
  await runExportPack(getRuntimeConfig(program2), options);
9830
10691
  });