threadnote 0.5.0 → 0.6.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.
@@ -3522,8 +3522,8 @@ var DEFAULT_SEED_PATTERNS = [
3522
3522
  ];
3523
3523
 
3524
3524
  // src/hooks.ts
3525
- var import_promises5 = require("node:fs/promises");
3526
- var import_node_path5 = require("node:path");
3525
+ var import_promises6 = require("node:fs/promises");
3526
+ var import_node_path7 = require("node:path");
3527
3527
 
3528
3528
  // src/mcp.ts
3529
3529
  var import_node_fs2 = require("node:fs");
@@ -7240,12 +7240,16 @@ async function runRemember(config, options) {
7240
7240
  project: normalizeOptionalMetadata(options.project),
7241
7241
  sourceAgentClient: options.sourceAgentClient ?? "codex",
7242
7242
  status: options.status ?? "active",
7243
- supersedes: options.replace,
7244
7243
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7245
7244
  topic: normalizeOptionalMetadata(options.topic)
7246
7245
  };
7247
- const memory = formatMemoryDocument("MEMORY", metadata, text.trim());
7248
- await storeMemory(config, memory, { dryRun: options.dryRun === true, metadata, replaceUri: options.replace });
7246
+ await storeMemory(config, {
7247
+ bodyText: text.trim(),
7248
+ dryRun: options.dryRun === true,
7249
+ metadata,
7250
+ replaceUri: options.replace,
7251
+ title: "MEMORY"
7252
+ });
7249
7253
  }
7250
7254
  async function runMigrateMemories(config, options) {
7251
7255
  const dryRun = options.dryRun === true;
@@ -7418,8 +7422,14 @@ async function runList(config, uri, options) {
7418
7422
  await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
7419
7423
  }
7420
7424
  async function runHandoff(config, options) {
7421
- const { handoff, metadata } = await buildHandoff(options);
7422
- await storeMemory(config, handoff, { dryRun: options.dryRun === true, metadata, replaceUri: options.replace });
7425
+ const { bodyText, metadata } = await buildHandoff(options);
7426
+ await storeMemory(config, {
7427
+ bodyText,
7428
+ dryRun: options.dryRun === true,
7429
+ metadata,
7430
+ replaceUri: options.replace,
7431
+ title: "HANDOFF"
7432
+ });
7423
7433
  }
7424
7434
  async function runArchive(config, uri, options) {
7425
7435
  assertVikingUri(uri);
@@ -7436,12 +7446,12 @@ async function runArchive(config, uri, options) {
7436
7446
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7437
7447
  topic: normalizeOptionalMetadata(options.topic)
7438
7448
  };
7439
- const archiveMemory2 = formatMemoryDocument(
7440
- "MEMORY",
7441
- fallbackMetadata,
7442
- ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n")
7443
- );
7444
- await storeMemory(config, archiveMemory2, { dryRun: true, metadata: fallbackMetadata });
7449
+ await storeMemory(config, {
7450
+ bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
7451
+ dryRun: true,
7452
+ metadata: fallbackMetadata,
7453
+ title: "MEMORY"
7454
+ });
7445
7455
  console.log(formatShellCommand(ov, withIdentity(config, ["rm", uri])));
7446
7456
  return;
7447
7457
  }
@@ -7458,12 +7468,12 @@ async function runArchive(config, uri, options) {
7458
7468
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7459
7469
  topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
7460
7470
  };
7461
- const archiveMemory = formatMemoryDocument(
7462
- "MEMORY",
7471
+ await storeMemory(config, {
7472
+ bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
7473
+ dryRun: false,
7463
7474
  metadata,
7464
- ["Archived original Threadnote memory.", "", original].join("\n")
7465
- );
7466
- await storeMemory(config, archiveMemory, { dryRun: false, metadata });
7475
+ title: "MEMORY"
7476
+ });
7467
7477
  const removedOriginal = await removeVikingResourceWithRetry(ov, config, uri);
7468
7478
  if (removedOriginal) {
7469
7479
  console.log(`Archived original memory: ${uri}`);
@@ -7544,14 +7554,19 @@ async function printExactMemoryMatches(config, ov, query, options) {
7544
7554
  console.log("\nExact durable memory matches:");
7545
7555
  console.log(outputs.join("\n\n"));
7546
7556
  }
7547
- async function storeMemory(config, memory, options) {
7557
+ async function storeMemory(config, options) {
7548
7558
  if (options.replaceUri) {
7549
7559
  assertVikingUri(options.replaceUri);
7550
7560
  }
7551
7561
  const ov = await openVikingCliForMode(options.dryRun);
7552
7562
  const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
7553
- const memoryUri = memoryUriFor(config, memory, options.metadata);
7554
- const writeMode = await memoryWriteMode(ov, config, memoryUri, options.metadata);
7563
+ const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
7564
+ const candidateMemory = formatMemoryDocument(options.title, candidateMetadata, options.bodyText);
7565
+ const memoryUri = memoryUriFor(config, candidateMemory, candidateMetadata);
7566
+ const isInPlaceUpdate = options.replaceUri !== void 0 && options.replaceUri === memoryUri;
7567
+ const finalMetadata = isInPlaceUpdate ? { ...options.metadata, supersedes: void 0 } : candidateMetadata;
7568
+ const memory = isInPlaceUpdate ? formatMemoryDocument(options.title, finalMetadata, options.bodyText) : candidateMemory;
7569
+ const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
7555
7570
  if (options.dryRun) {
7556
7571
  console.log(memory);
7557
7572
  console.log("\nWould run:");
@@ -7571,17 +7586,17 @@ async function storeMemory(config, memory, options) {
7571
7586
  ])
7572
7587
  )
7573
7588
  );
7574
- if (options.replaceUri && options.replaceUri !== memoryUri) {
7589
+ if (options.replaceUri && !isInPlaceUpdate) {
7575
7590
  console.log(formatShellCommand(ov, withIdentity(config, ["rm", options.replaceUri])));
7576
7591
  }
7577
7592
  return;
7578
7593
  }
7579
7594
  await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
7580
7595
  await (0, import_promises4.chmod)(memoryPath, 384);
7581
- await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, options.metadata));
7596
+ await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, finalMetadata));
7582
7597
  await writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode);
7583
7598
  console.log(`Stored memory: ${memoryUri}`);
7584
- if (options.replaceUri && options.replaceUri !== memoryUri) {
7599
+ if (options.replaceUri && !isInPlaceUpdate) {
7585
7600
  const removedReplacedMemory = await removeVikingResourceWithRetry(ov, config, options.replaceUri);
7586
7601
  if (removedReplacedMemory) {
7587
7602
  console.log(`Forgot replaced memory: ${options.replaceUri}`);
@@ -7590,7 +7605,7 @@ async function storeMemory(config, memory, options) {
7590
7605
  `Replacement stored, but the superseded memory is still processing. Retry later: threadnote forget ${options.replaceUri}`
7591
7606
  );
7592
7607
  }
7593
- } else if (options.replaceUri === memoryUri) {
7608
+ } else if (isInPlaceUpdate) {
7594
7609
  console.log(`Updated existing memory in place: ${memoryUri}`);
7595
7610
  }
7596
7611
  }
@@ -8043,11 +8058,10 @@ async function buildHandoff(options) {
8043
8058
  project: normalizeOptionalMetadata(options.project) ?? repoName,
8044
8059
  sourceAgentClient: options.sourceAgentClient ?? "codex",
8045
8060
  status: "active",
8046
- supersedes: options.replace,
8047
8061
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8048
8062
  topic: normalizeOptionalMetadata(options.topic)
8049
8063
  };
8050
- const body = [
8064
+ const bodyText = [
8051
8065
  `repo: ${repoName}`,
8052
8066
  `repo_path: ${repoRoot}`,
8053
8067
  `branch: ${branch || "unknown"}`,
@@ -8071,7 +8085,7 @@ async function buildHandoff(options) {
8071
8085
  "next_step:",
8072
8086
  options.nextStep ?? "- inspect the current repo state and continue from this handoff"
8073
8087
  ].join("\n");
8074
- return { handoff: formatMemoryDocument("HANDOFF", metadata, body), metadata };
8088
+ return { bodyText, metadata };
8075
8089
  }
8076
8090
  async function gitTouchedFiles(cwd) {
8077
8091
  const changedFiles = await gitValue(["diff", "--name-only", "HEAD"], cwd);
@@ -8095,6 +8109,128 @@ function formatBlock(value, emptyValue) {
8095
8109
  return trimmed.split("\n").map((line) => `- ${line}`).join("\n");
8096
8110
  }
8097
8111
 
8112
+ // src/update-check.ts
8113
+ var import_node_child_process2 = require("node:child_process");
8114
+ var import_promises5 = require("node:fs/promises");
8115
+ var import_node_path5 = require("node:path");
8116
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
8117
+ var NPM_LATEST_URL = "https://registry.npmjs.org/threadnote/latest";
8118
+ var FETCH_TIMEOUT_MS = 3e3;
8119
+ async function checkForThreadnoteUpdate(args) {
8120
+ if (args.currentVersion === "unknown") {
8121
+ return void 0;
8122
+ }
8123
+ const cached = await readUpdateCache(args.cachePath);
8124
+ if (cached && isCacheFresh(cached)) {
8125
+ return compareVersions(args.currentVersion, cached.latestVersion);
8126
+ }
8127
+ const fresh = await fetchLatestVersion();
8128
+ if (fresh) {
8129
+ await writeUpdateCache(args.cachePath, {
8130
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
8131
+ latestVersion: fresh,
8132
+ version: 1
8133
+ });
8134
+ return compareVersions(args.currentVersion, fresh);
8135
+ }
8136
+ if (cached) {
8137
+ return compareVersions(args.currentVersion, cached.latestVersion);
8138
+ }
8139
+ return void 0;
8140
+ }
8141
+ function spawnDetachedAutoUpdate() {
8142
+ try {
8143
+ const entry = process.argv[1];
8144
+ if (typeof entry !== "string" || entry.length === 0) {
8145
+ return;
8146
+ }
8147
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [entry, "update", "--yes"], {
8148
+ detached: true,
8149
+ stdio: "ignore"
8150
+ });
8151
+ child.unref();
8152
+ } catch {
8153
+ }
8154
+ }
8155
+ function compareVersions(currentVersion, latestVersion) {
8156
+ return {
8157
+ currentVersion,
8158
+ latestVersion,
8159
+ outdated: isNewerVersion(latestVersion, currentVersion)
8160
+ };
8161
+ }
8162
+ function isNewerVersion(candidate, baseline) {
8163
+ const candidateParts = parseVersion(candidate);
8164
+ const baselineParts = parseVersion(baseline);
8165
+ for (let index = 0; index < 3; index += 1) {
8166
+ if (candidateParts[index] !== baselineParts[index]) {
8167
+ return candidateParts[index] > baselineParts[index];
8168
+ }
8169
+ }
8170
+ return false;
8171
+ }
8172
+ function parseVersion(value) {
8173
+ const parts = value.split(".").map((part) => parseInt(part, 10));
8174
+ return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
8175
+ }
8176
+ function isCacheFresh(cache) {
8177
+ const checkedAt = new Date(cache.checkedAt).getTime();
8178
+ return Number.isFinite(checkedAt) && Date.now() - checkedAt < CACHE_TTL_MS;
8179
+ }
8180
+ async function readUpdateCache(cachePath) {
8181
+ try {
8182
+ const raw = await (0, import_promises5.readFile)(cachePath, "utf8");
8183
+ const parsed = JSON.parse(raw);
8184
+ if (parsed.version !== 1 || typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
8185
+ return void 0;
8186
+ }
8187
+ return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, version: 1 };
8188
+ } catch {
8189
+ return void 0;
8190
+ }
8191
+ }
8192
+ async function writeUpdateCache(cachePath, contents) {
8193
+ try {
8194
+ await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(cachePath), { recursive: true });
8195
+ await (0, import_promises5.writeFile)(cachePath, `${JSON.stringify(contents)}
8196
+ `, { encoding: "utf8", mode: 384 });
8197
+ } catch {
8198
+ }
8199
+ }
8200
+ async function fetchLatestVersion() {
8201
+ try {
8202
+ const response = await fetch(NPM_LATEST_URL, {
8203
+ headers: { accept: "application/json" },
8204
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
8205
+ });
8206
+ if (!response.ok) {
8207
+ return void 0;
8208
+ }
8209
+ const data = await response.json();
8210
+ return typeof data.version === "string" && data.version.length > 0 ? data.version : void 0;
8211
+ } catch {
8212
+ return void 0;
8213
+ }
8214
+ }
8215
+
8216
+ // src/version.ts
8217
+ var import_node_fs4 = require("node:fs");
8218
+ var import_node_path6 = require("node:path");
8219
+ var cachedVersion;
8220
+ function getThreadnoteVersion() {
8221
+ if (cachedVersion !== void 0) {
8222
+ return cachedVersion;
8223
+ }
8224
+ try {
8225
+ const packageJsonPath = (0, import_node_path6.join)(__dirname, "..", "package.json");
8226
+ const parsed = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf8"));
8227
+ cachedVersion = typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : "unknown";
8228
+ } catch {
8229
+ cachedVersion = "unknown";
8230
+ }
8231
+ return cachedVersion;
8232
+ }
8233
+
8098
8234
  // src/hooks.ts
8099
8235
  var MANAGED_HOOKS = [
8100
8236
  {
@@ -8128,7 +8264,7 @@ async function runHooksInstall(config, agent, options) {
8128
8264
  }
8129
8265
  async function runClaudeHooksInstall(options) {
8130
8266
  const path = expandPath(CLAUDE_SETTINGS_PATH);
8131
- const existingRaw = await exists(path) ? await (0, import_promises5.readFile)(path, "utf8") : "{}";
8267
+ const existingRaw = await exists(path) ? await (0, import_promises6.readFile)(path, "utf8") : "{}";
8132
8268
  const parsed = parseJsonConfigObject(existingRaw) ?? {};
8133
8269
  const next = options.remove ? withoutThreadnoteHooks(parsed) : withThreadnoteHooks(parsed);
8134
8270
  const before = JSON.stringify(parsed);
@@ -8145,11 +8281,11 @@ async function runClaudeHooksInstall(options) {
8145
8281
  console.log("\nRe-run with --apply to actually modify the file.");
8146
8282
  return;
8147
8283
  }
8148
- await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
8284
+ await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(path), { recursive: true });
8149
8285
  const serialized = `${JSON.stringify(next, void 0, 2)}
8150
8286
  `;
8151
- await (0, import_promises5.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
8152
- await (0, import_promises5.chmod)(path, 384);
8287
+ await (0, import_promises6.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
8288
+ await (0, import_promises6.chmod)(path, 384);
8153
8289
  console.log(`${options.remove ? "Removed" : "Installed"} threadnote-managed Claude hooks.`);
8154
8290
  }
8155
8291
  function withThreadnoteHooks(input2) {
@@ -8227,7 +8363,7 @@ async function hasManagedClaudeHooks() {
8227
8363
  if (!await exists(path)) {
8228
8364
  return false;
8229
8365
  }
8230
- const raw = await (0, import_promises5.readFile)(path, "utf8");
8366
+ const raw = await (0, import_promises6.readFile)(path, "utf8");
8231
8367
  const parsed = parseJsonConfigObject(raw);
8232
8368
  if (!parsed || !isJsonObject(parsed.hooks)) {
8233
8369
  return false;
@@ -8245,7 +8381,7 @@ async function hasManagedClaudeHooks() {
8245
8381
  async function runPreCompactHook(config, options = {}) {
8246
8382
  try {
8247
8383
  const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
8248
- const project = repoRoot ? (0, import_node_path5.basename)(repoRoot) : "general";
8384
+ const project = repoRoot ? (0, import_node_path7.basename)(repoRoot) : "general";
8249
8385
  await runHandoff(config, {
8250
8386
  blockers: "- none recorded",
8251
8387
  dryRun: options.dryRun === true,
@@ -8269,7 +8405,8 @@ async function runSessionStartHook(config, options = {}) {
8269
8405
  if (!repoRoot) {
8270
8406
  return;
8271
8407
  }
8272
- const project = (0, import_node_path5.basename)(repoRoot);
8408
+ const project = (0, import_node_path7.basename)(repoRoot);
8409
+ await emitUpdateBannerIfOutdated(config);
8273
8410
  process.stdout.write(`## Threadnote \u2014 latest context for ${project}
8274
8411
 
8275
8412
  `);
@@ -8286,10 +8423,36 @@ async function runSessionStartHook(config, options = {}) {
8286
8423
  );
8287
8424
  }
8288
8425
  }
8426
+ async function emitUpdateBannerIfOutdated(config) {
8427
+ try {
8428
+ const result = await checkForThreadnoteUpdate({
8429
+ cachePath: (0, import_node_path7.join)(config.agentContextHome, ".update-state.json"),
8430
+ currentVersion: getThreadnoteVersion()
8431
+ });
8432
+ if (!result || !result.outdated) {
8433
+ return;
8434
+ }
8435
+ if (process.env.THREADNOTE_AUTO_UPDATE === "1") {
8436
+ process.stdout.write(
8437
+ `[threadnote] v${result.latestVersion} available (current v${result.currentVersion}). Auto-updating in the background; the new version takes effect next session.
8438
+
8439
+ `
8440
+ );
8441
+ spawnDetachedAutoUpdate();
8442
+ return;
8443
+ }
8444
+ process.stdout.write(
8445
+ `[threadnote] v${result.latestVersion} available (current v${result.currentVersion}). Run: threadnote update
8446
+
8447
+ `
8448
+ );
8449
+ } catch {
8450
+ }
8451
+ }
8289
8452
 
8290
8453
  // src/seeding.ts
8291
- var import_promises6 = require("node:fs/promises");
8292
- var import_node_path6 = require("node:path");
8454
+ var import_promises7 = require("node:fs/promises");
8455
+ var import_node_path8 = require("node:path");
8293
8456
  async function runSeed(config, options) {
8294
8457
  const manifest = await readSeedManifest(config.manifestPath);
8295
8458
  const ignorePatterns = await loadIgnorePatterns();
@@ -8318,7 +8481,7 @@ async function runSeed(config, options) {
8318
8481
  }
8319
8482
  async function runInitManifest(config, options) {
8320
8483
  const manifestPath = expandPath(
8321
- options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path6.join)(config.agentContextHome, USER_MANIFEST_NAME)
8484
+ options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path8.join)(config.agentContextHome, USER_MANIFEST_NAME)
8322
8485
  );
8323
8486
  const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
8324
8487
  const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
@@ -8361,9 +8524,9 @@ async function runInitManifest(config, options) {
8361
8524
  console.log(output2.trimEnd());
8362
8525
  return;
8363
8526
  }
8364
- await ensureDirectory((0, import_node_path6.dirname)(manifestPath), false);
8365
- await (0, import_promises6.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
8366
- await (0, import_promises6.chmod)(manifestPath, 384);
8527
+ await ensureDirectory((0, import_node_path8.dirname)(manifestPath), false);
8528
+ await (0, import_promises7.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
8529
+ await (0, import_promises7.chmod)(manifestPath, 384);
8367
8530
  console.log(`Wrote manifest: ${manifestPath}`);
8368
8531
  console.log("Seed with:");
8369
8532
  console.log(" threadnote seed --dry-run");
@@ -8393,13 +8556,13 @@ async function resolveRepoRoot(repoInput) {
8393
8556
  async function projectIdentity(path) {
8394
8557
  const expanded = expandPath(path);
8395
8558
  try {
8396
- return await (0, import_promises6.realpath)(expanded);
8559
+ return await (0, import_promises7.realpath)(expanded);
8397
8560
  } catch (_err) {
8398
8561
  return expanded;
8399
8562
  }
8400
8563
  }
8401
8564
  function projectManifestForRepo(repoRoot, existingProjects) {
8402
- const baseName = uriSegment((0, import_node_path6.basename)(repoRoot));
8565
+ const baseName = uriSegment((0, import_node_path8.basename)(repoRoot));
8403
8566
  const usedNames = new Set(existingProjects.map((project) => project.name));
8404
8567
  const usedUris = new Set(existingProjects.map((project) => project.uri));
8405
8568
  let name = baseName;
@@ -8421,7 +8584,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
8421
8584
  for (const pattern of project.seed) {
8422
8585
  const files = await resolveProjectPattern(projectRoot, pattern);
8423
8586
  for (const filePath of files) {
8424
- const relativePath = toPosixPath((0, import_node_path6.relative)(projectRoot, filePath));
8587
+ const relativePath = toPosixPath((0, import_node_path8.relative)(projectRoot, filePath));
8425
8588
  if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
8426
8589
  continue;
8427
8590
  }
@@ -8439,20 +8602,20 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
8439
8602
  async function resolveProjectPattern(projectRoot, pattern) {
8440
8603
  const normalizedPattern = toPosixPath(pattern);
8441
8604
  if (!hasGlob(normalizedPattern)) {
8442
- const filePath = (0, import_node_path6.join)(projectRoot, normalizedPattern);
8605
+ const filePath = (0, import_node_path8.join)(projectRoot, normalizedPattern);
8443
8606
  return await isFile(filePath) ? [filePath] : [];
8444
8607
  }
8445
8608
  const globBase = getGlobBase(normalizedPattern);
8446
- const basePath = (0, import_node_path6.join)(projectRoot, globBase);
8609
+ const basePath = (0, import_node_path8.join)(projectRoot, globBase);
8447
8610
  if (!await exists(basePath)) {
8448
8611
  return [];
8449
8612
  }
8450
8613
  const regex = globToRegExp(normalizedPattern);
8451
8614
  const files = await walkFiles(basePath);
8452
- return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path6.relative)(projectRoot, filePath))));
8615
+ return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path8.relative)(projectRoot, filePath))));
8453
8616
  }
8454
8617
  async function prepareSeedFile(config, candidate, dryRun) {
8455
- const content = await (0, import_promises6.readFile)(candidate.filePath, "utf8");
8618
+ const content = await (0, import_promises7.readFile)(candidate.filePath, "utf8");
8456
8619
  const redactedContent = shouldRedactPath(candidate.relativePath) ? redactContent(candidate.relativePath, content) : content;
8457
8620
  const secretMatches = detectSecretMatches(redactedContent);
8458
8621
  if (secretMatches.length > 0) {
@@ -8464,14 +8627,14 @@ async function prepareSeedFile(config, candidate, dryRun) {
8464
8627
  if (redactedContent === content) {
8465
8628
  return candidate.filePath;
8466
8629
  }
8467
- const redactedPath = (0, import_node_path6.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
8630
+ const redactedPath = (0, import_node_path8.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
8468
8631
  if (dryRun) {
8469
8632
  console.log(`Would write redacted copy: ${redactedPath}`);
8470
8633
  return redactedPath;
8471
8634
  }
8472
- await ensureDirectory((0, import_node_path6.dirname)(redactedPath), false);
8473
- await (0, import_promises6.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
8474
- await (0, import_promises6.chmod)(redactedPath, 384);
8635
+ await ensureDirectory((0, import_node_path8.dirname)(redactedPath), false);
8636
+ await (0, import_promises7.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
8637
+ await (0, import_promises7.chmod)(redactedPath, 384);
8475
8638
  return redactedPath;
8476
8639
  }
8477
8640
  async function collectSkillCandidates(config) {
@@ -8496,7 +8659,7 @@ async function collectSkillCandidates(config) {
8496
8659
  for (const source of sources) {
8497
8660
  const files = await resolveAbsolutePattern(expandPath(source.pattern));
8498
8661
  for (const filePath of files) {
8499
- const content = await (0, import_promises6.readFile)(filePath, "utf8");
8662
+ const content = await (0, import_promises7.readFile)(filePath, "utf8");
8500
8663
  const matches = detectSecretMatches(content);
8501
8664
  if (matches.length > 0) {
8502
8665
  console.log(`SKIP skill with possible secret: ${filePath}`);
@@ -8527,10 +8690,10 @@ async function resolveAbsolutePattern(pattern) {
8527
8690
  return files.filter((filePath) => regex.test(toPosixPath(filePath)));
8528
8691
  }
8529
8692
  function skillResourceUri(skill) {
8530
- return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path6.basename)((0, import_node_path6.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
8693
+ return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path8.basename)((0, import_node_path8.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
8531
8694
  }
8532
8695
  async function loadIgnorePatterns() {
8533
- const raw = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), ".threadnoteignore"), "utf8");
8696
+ const raw = await (0, import_promises7.readFile)((0, import_node_path8.join)(toolRoot(), ".threadnoteignore"), "utf8");
8534
8697
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8535
8698
  }
8536
8699
  function matchesIgnore(relativePath, patterns) {
@@ -8601,14 +8764,16 @@ function detectSecretMatches(content) {
8601
8764
  }
8602
8765
 
8603
8766
  // src/share.ts
8604
- var import_promises7 = require("node:fs/promises");
8767
+ var import_promises8 = require("node:fs/promises");
8605
8768
  var import_node_os4 = require("node:os");
8606
- var import_node_path7 = require("node:path");
8769
+ var import_node_path9 = require("node:path");
8607
8770
  var TEAMS_FILE_VERSION = 1;
8608
8771
  var SHARED_SEGMENT = "shared";
8609
8772
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
8610
8773
  var DEFAULT_GIT_REMOTE_NAME = "origin";
8611
8774
  var SCRUBBER_PATTERNS = [
8775
+ // Credentials: never redactable. Blocking is the only safe response —
8776
+ // automated redaction risks false negatives that leave material in git.
8612
8777
  { name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
8613
8778
  { name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
8614
8779
  { name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
@@ -8621,8 +8786,35 @@ var SCRUBBER_PATTERNS = [
8621
8786
  { name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
8622
8787
  // Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
8623
8788
  // xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
8624
- { name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i }
8789
+ { name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i },
8790
+ // Soft leaks: block by default (so the agent sees them and decides), but
8791
+ // allow opt-in redaction so curated memories with incidental matches can
8792
+ // ship without a manual rewrite. Local home paths are the recurring
8793
+ // real-world leak; the regexes greedily consume the whole path segment
8794
+ // (including subdirectories) up to whitespace or common closing punctuation
8795
+ // so redaction collapses an entire path to a single placeholder rather than
8796
+ // leaving the subpath visible.
8797
+ { name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
8798
+ { name: "linux home path", placeholder: "<local-path>", regex: /\b\/home\/[^\s)>"'`,]+/ }
8625
8799
  ];
8800
+ function applyScrubber(content, { redact }) {
8801
+ let cleaned = content;
8802
+ const redactions = [];
8803
+ for (const pattern of SCRUBBER_PATTERNS) {
8804
+ if (!pattern.regex.test(cleaned)) {
8805
+ continue;
8806
+ }
8807
+ if (!pattern.placeholder || !redact) {
8808
+ return { blocker: pattern.name, cleaned: content, redactions: [] };
8809
+ }
8810
+ const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
8811
+ const globalRegex = new RegExp(pattern.regex.source, flags);
8812
+ const matches = cleaned.match(globalRegex) ?? [];
8813
+ cleaned = cleaned.replace(globalRegex, pattern.placeholder);
8814
+ redactions.push({ count: matches.length, name: pattern.name });
8815
+ }
8816
+ return { cleaned, redactions };
8817
+ }
8626
8818
  async function runShareInit(config, remoteUrl, options) {
8627
8819
  if (!remoteUrl.trim()) {
8628
8820
  throw new Error("Provide a git remote URL for the shared memories repo.");
@@ -8641,8 +8833,8 @@ async function runShareInit(config, remoteUrl, options) {
8641
8833
  if (await exists(gitdir)) {
8642
8834
  throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
8643
8835
  }
8644
- await ensureDirectory((0, import_node_path7.dirname)(worktree), dryRun);
8645
- await ensureDirectory((0, import_node_path7.dirname)(gitdir), dryRun);
8836
+ await ensureDirectory((0, import_node_path9.dirname)(worktree), dryRun);
8837
+ await ensureDirectory((0, import_node_path9.dirname)(gitdir), dryRun);
8646
8838
  const git = await requiredExecutable("git");
8647
8839
  await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
8648
8840
  const newConfig = {
@@ -8673,7 +8865,7 @@ async function runShareInit(config, remoteUrl, options) {
8673
8865
  var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
8674
8866
  var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
8675
8867
  async function ensureSharedGitignore(worktree, git, push) {
8676
- const gitignorePath = (0, import_node_path7.join)(worktree, ".gitignore");
8868
+ const gitignorePath = (0, import_node_path9.join)(worktree, ".gitignore");
8677
8869
  const existing = await readFileIfExists(gitignorePath) ?? "";
8678
8870
  const lines = existing.split("\n").map((line) => line.trim());
8679
8871
  const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
@@ -8692,7 +8884,7 @@ async function ensureSharedGitignore(worktree, git, push) {
8692
8884
  segments.push(SHARED_GITIGNORE_HEADER, "\n");
8693
8885
  }
8694
8886
  segments.push(missingPatterns.join("\n"), "\n");
8695
- await (0, import_promises7.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
8887
+ await (0, import_promises8.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
8696
8888
  console.log(`Added ${missingPatterns.join(", ")} to ${portablePath(gitignorePath)}`);
8697
8889
  await maybeRun(false, git, ["-C", worktree, "add", ".gitignore"]);
8698
8890
  const commitResult = await runCommand(
@@ -8757,7 +8949,7 @@ async function runShareSync(config, options) {
8757
8949
  if (dryRun) {
8758
8950
  console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
8759
8951
  } else if (pullResult && pullResult.exitCode !== 0) {
8760
- if (await exists((0, import_node_path7.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path7.join)(team.config.gitdir, "rebase-apply"))) {
8952
+ if (await exists((0, import_node_path9.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path9.join)(team.config.gitdir, "rebase-apply"))) {
8761
8953
  throw new Error(
8762
8954
  `git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
8763
8955
  Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
@@ -8787,16 +8979,41 @@ async function runSharePublish(config, sourceUri, options) {
8787
8979
  assertVikingUri(sourceUri);
8788
8980
  const team = await resolveTeam(config, options.team);
8789
8981
  const dryRun = options.dryRun === true;
8982
+ const preview = options.preview === true;
8790
8983
  if (isInSharedNamespace(config, sourceUri)) {
8791
8984
  throw new Error(`Memory ${sourceUri} is already in the shared namespace.`);
8792
8985
  }
8793
8986
  const ov = await openVikingCliForMode(dryRun);
8794
- const content = await readMemoryContent(config, ov, sourceUri, dryRun);
8795
- const blocker = scrubberBlocker(content);
8796
- if (blocker) {
8797
- throw new Error(`Refusing to publish ${sourceUri}: possible ${blocker}. Strip the sensitive value, then retry.`);
8798
- }
8987
+ const rawContent = await readMemoryContent(config, ov, sourceUri, dryRun);
8988
+ const stripped = stripPersonalProvenance(rawContent);
8989
+ const scrub = applyScrubber(stripped, { redact: options.redact === true });
8799
8990
  const targetUri = sharedUriFor(config, sourceUri, team.name);
8991
+ if (preview) {
8992
+ console.log(`PREVIEW source: ${sourceUri}`);
8993
+ console.log(`PREVIEW destination: ${targetUri}`);
8994
+ if (scrub.blocker) {
8995
+ console.log(
8996
+ `PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or rerun with --redact for soft-leak patterns.`
8997
+ );
8998
+ return;
8999
+ }
9000
+ for (const redaction of scrub.redactions) {
9001
+ console.log(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
9002
+ }
9003
+ console.log("-----BEGIN PREVIEW-----");
9004
+ console.log(scrub.cleaned);
9005
+ console.log("-----END PREVIEW-----");
9006
+ return;
9007
+ }
9008
+ if (scrub.blocker) {
9009
+ throw new Error(
9010
+ `Refusing to publish ${sourceUri}: possible ${scrub.blocker}. Strip the sensitive value or pass --redact for soft-leak patterns.`
9011
+ );
9012
+ }
9013
+ for (const redaction of scrub.redactions) {
9014
+ console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before publish.`);
9015
+ }
9016
+ const content = scrub.cleaned;
8800
9017
  if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
8801
9018
  throw new Error(
8802
9019
  `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.`
@@ -8908,10 +9125,10 @@ function normalizeTeamName(input2) {
8908
9125
  return candidate;
8909
9126
  }
8910
9127
  function teamsFilePath(config) {
8911
- return (0, import_node_path7.join)(config.agentContextHome, "share", "teams.json");
9128
+ return (0, import_node_path9.join)(config.agentContextHome, "share", "teams.json");
8912
9129
  }
8913
9130
  function teamWorktreePath(config, team) {
8914
- return (0, import_node_path7.join)(
9131
+ return (0, import_node_path9.join)(
8915
9132
  config.agentContextHome,
8916
9133
  "data",
8917
9134
  "viking",
@@ -8924,7 +9141,7 @@ function teamWorktreePath(config, team) {
8924
9141
  );
8925
9142
  }
8926
9143
  function teamGitdirPath(config, team) {
8927
- return (0, import_node_path7.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
9144
+ return (0, import_node_path9.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
8928
9145
  }
8929
9146
  async function readTeamsFile(config) {
8930
9147
  const path = teamsFilePath(config);
@@ -8967,13 +9184,13 @@ async function readTeamsFile(config) {
8967
9184
  }
8968
9185
  async function writeTeamsFile(config, contents) {
8969
9186
  const path = teamsFilePath(config);
8970
- await (0, import_promises7.mkdir)((0, import_node_path7.dirname)(path), { recursive: true });
9187
+ await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(path), { recursive: true });
8971
9188
  const serializable = {
8972
9189
  defaultTeam: contents.defaultTeam,
8973
9190
  teams: contents.teams,
8974
9191
  version: contents.version
8975
9192
  };
8976
- await (0, import_promises7.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
9193
+ await (0, import_promises8.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
8977
9194
  `, { encoding: "utf8", mode: 384 });
8978
9195
  }
8979
9196
  async function resolveTeam(config, requested) {
@@ -9003,7 +9220,7 @@ async function assertWorktreeUsable(worktree) {
9003
9220
  if (!await isDirectory(worktree)) {
9004
9221
  throw new Error(`Cannot use ${worktree} as a worktree: not a directory.`);
9005
9222
  }
9006
- const entries = await (0, import_promises7.readdir)(worktree);
9223
+ const entries = await (0, import_promises8.readdir)(worktree);
9007
9224
  if (entries.length > 0) {
9008
9225
  const preview = entries.slice(0, 5).join(", ");
9009
9226
  const suffix = entries.length > 5 ? `, +${entries.length - 5} more` : "";
@@ -9027,7 +9244,7 @@ async function walkMemoryFiles(root) {
9027
9244
  async function visit(path, depth) {
9028
9245
  let entries;
9029
9246
  try {
9030
- entries = await (0, import_promises7.readdir)(path, { withFileTypes: true });
9247
+ entries = await (0, import_promises8.readdir)(path, { withFileTypes: true });
9031
9248
  } catch (err) {
9032
9249
  console.warn(`Skipping ${path} during shared-tree walk: ${err instanceof Error ? err.message : String(err)}`);
9033
9250
  return;
@@ -9036,7 +9253,7 @@ async function walkMemoryFiles(root) {
9036
9253
  if (entry.name === ".git") {
9037
9254
  continue;
9038
9255
  }
9039
- const full = (0, import_node_path7.join)(path, entry.name);
9256
+ const full = (0, import_node_path9.join)(path, entry.name);
9040
9257
  if (entry.isDirectory()) {
9041
9258
  if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
9042
9259
  continue;
@@ -9060,7 +9277,7 @@ async function walkMemoryFiles(root) {
9060
9277
  return out;
9061
9278
  }
9062
9279
  function workfileToVikingUri(config, team, filePath) {
9063
- const rel = (0, import_node_path7.relative)(team.worktree, filePath).split(import_node_path7.sep).join("/");
9280
+ const rel = (0, import_node_path9.relative)(team.worktree, filePath).split(import_node_path9.sep).join("/");
9064
9281
  return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
9065
9282
  }
9066
9283
  function isInSharedNamespace(config, uri) {
@@ -9092,8 +9309,26 @@ function vikingUriToWorktreeRelative(config, uri, team) {
9092
9309
  }
9093
9310
  return uri.slice(prefix.length);
9094
9311
  }
9095
- function scrubberBlocker(content) {
9096
- return SCRUBBER_PATTERNS.find((pattern) => pattern.regex.test(content))?.name;
9312
+ function stripPersonalProvenance(content) {
9313
+ const lines = content.split("\n");
9314
+ let headerEnd = lines.length;
9315
+ for (let index = 0; index < lines.length; index += 1) {
9316
+ if (lines[index].trim() === "") {
9317
+ headerEnd = index;
9318
+ break;
9319
+ }
9320
+ }
9321
+ const cleaned = [];
9322
+ for (let index = 0; index < headerEnd; index += 1) {
9323
+ if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
9324
+ continue;
9325
+ }
9326
+ cleaned.push(lines[index]);
9327
+ }
9328
+ for (let index = headerEnd; index < lines.length; index += 1) {
9329
+ cleaned.push(lines[index]);
9330
+ }
9331
+ return cleaned.join("\n");
9097
9332
  }
9098
9333
  async function readMemoryContent(config, ov, uri, dryRun) {
9099
9334
  const args = withIdentity(config, ["read", uri]);
@@ -9154,13 +9389,13 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun) {
9154
9389
  console.log(`Would run: ${formatShellCommand(ov, args)}`);
9155
9390
  return;
9156
9391
  }
9157
- const stagingDir = await (0, import_promises7.mkdtemp)((0, import_node_path7.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
9158
- const tempPath = (0, import_node_path7.join)(stagingDir, "body.txt");
9392
+ const stagingDir = await (0, import_promises8.mkdtemp)((0, import_node_path9.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
9393
+ const tempPath = (0, import_node_path9.join)(stagingDir, "body.txt");
9159
9394
  try {
9160
- await (0, import_promises7.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
9395
+ await (0, import_promises8.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
9161
9396
  await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode);
9162
9397
  } finally {
9163
- await (0, import_promises7.rm)(stagingDir, { force: true, recursive: true });
9398
+ await (0, import_promises8.rm)(stagingDir, { force: true, recursive: true });
9164
9399
  }
9165
9400
  }
9166
9401
  async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode) {
@@ -9218,7 +9453,7 @@ ${stdout}`.toLowerCase();
9218
9453
  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");
9219
9454
  }
9220
9455
  async function ingestSingleFile(ov, config, uri, filePath, initialMode) {
9221
- const content = await (0, import_promises7.readFile)(filePath, "utf8");
9456
+ const content = await (0, import_promises8.readFile)(filePath, "utf8");
9222
9457
  await writeMemoryFile(config, ov, uri, content, initialMode, false);
9223
9458
  }
9224
9459
  async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
@@ -9260,7 +9495,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
9260
9495
  if (!relative3) {
9261
9496
  return;
9262
9497
  }
9263
- await (0, import_promises7.rm)((0, import_node_path7.join)(worktree, relative3), { force: true });
9498
+ await (0, import_promises8.rm)((0, import_node_path9.join)(worktree, relative3), { force: true });
9264
9499
  }
9265
9500
  async function removeMemoryUri(config, ov, uri, dryRun) {
9266
9501
  const args = withIdentity(config, ["rm", uri]);
@@ -9317,8 +9552,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
9317
9552
  const oldRel = entries[index + 1];
9318
9553
  const newRel = entries[index + 2];
9319
9554
  if (oldRel && newRel) {
9320
- changes.push({ path: (0, import_node_path7.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
9321
- changes.push({ path: (0, import_node_path7.join)(worktree, newRel), relativePath: newRel, status: "added" });
9555
+ changes.push({ path: (0, import_node_path9.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
9556
+ changes.push({ path: (0, import_node_path9.join)(worktree, newRel), relativePath: newRel, status: "added" });
9322
9557
  }
9323
9558
  index += 3;
9324
9559
  continue;
@@ -9326,7 +9561,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
9326
9561
  const rel = entries[index + 1];
9327
9562
  if (rel) {
9328
9563
  const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
9329
- changes.push({ path: (0, import_node_path7.join)(worktree, rel), relativePath: rel, status });
9564
+ changes.push({ path: (0, import_node_path9.join)(worktree, rel), relativePath: rel, status });
9330
9565
  }
9331
9566
  index += 2;
9332
9567
  }
@@ -9350,32 +9585,32 @@ async function applyChangesToOpenViking(config, team, changes) {
9350
9585
  if (!await isFile(change.path)) {
9351
9586
  continue;
9352
9587
  }
9353
- if (change.status === "modified" && await vikingResourceExists2(ov, config, uri)) {
9354
- console.warn(
9355
- `share sync: overwriting local ${uri} with the upstream version (local edits to the shared subtree are not preserved across sync).`
9356
- );
9588
+ const ovHasResource = await vikingResourceExists2(ov, config, uri);
9589
+ if (ovHasResource) {
9590
+ const reason = change.status === "modified" ? "overwriting local with upstream (local edits to the shared subtree are not preserved across sync)" : "aligning OV to upstream (resource pre-existed in OV, likely from an earlier local publish or sync)";
9591
+ console.warn(`share sync: ${uri}: ${reason}.`);
9357
9592
  }
9358
9593
  await ensureSharedDirectoryChain(config, ov, uri, false);
9359
- const writeMode = change.status === "modified" ? "replace" : "create";
9594
+ const writeMode = ovHasResource ? "replace" : "create";
9360
9595
  await ingestSingleFile(ov, config, uri, change.path, writeMode);
9361
9596
  }
9362
9597
  }
9363
9598
 
9364
9599
  // src/lifecycle.ts
9365
- var import_node_child_process2 = require("node:child_process");
9366
- var import_node_fs5 = require("node:fs");
9367
- var import_promises10 = require("node:fs/promises");
9600
+ var import_node_child_process3 = require("node:child_process");
9601
+ var import_node_fs6 = require("node:fs");
9602
+ var import_promises11 = require("node:fs/promises");
9368
9603
  var import_node_os6 = require("node:os");
9369
- var import_node_path9 = require("node:path");
9604
+ var import_node_path11 = require("node:path");
9370
9605
  var import_node_process2 = require("node:process");
9371
- var import_promises11 = require("node:readline/promises");
9606
+ var import_promises12 = require("node:readline/promises");
9372
9607
 
9373
9608
  // src/update.ts
9374
- var import_node_fs4 = require("node:fs");
9375
- var import_promises8 = require("node:fs/promises");
9609
+ var import_node_fs5 = require("node:fs");
9610
+ var import_promises9 = require("node:fs/promises");
9376
9611
  var import_node_os5 = require("node:os");
9377
- var import_node_path8 = require("node:path");
9378
- var import_promises9 = require("node:readline/promises");
9612
+ var import_node_path10 = require("node:path");
9613
+ var import_promises10 = require("node:readline/promises");
9379
9614
  var import_node_process = require("node:process");
9380
9615
  var NPM_PACKAGE_NAME = "threadnote";
9381
9616
  var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
@@ -9423,7 +9658,7 @@ async function runUpdate(config, options) {
9423
9658
  console.log(`Update available. Run: threadnote update`);
9424
9659
  } else {
9425
9660
  console.log(
9426
- compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
9661
+ compareVersions2(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
9427
9662
  );
9428
9663
  }
9429
9664
  return;
@@ -9511,26 +9746,26 @@ async function maybeRunPostUpdateAfterRepair(config, options) {
9511
9746
  async function getUpdateInfo(config, options) {
9512
9747
  const currentVersion = await currentPackageVersion();
9513
9748
  const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
9514
- const latestVersion = cached?.latestVersion ?? await fetchLatestVersion(options.registry);
9749
+ const latestVersion = cached?.latestVersion ?? await fetchLatestVersion2(options.registry);
9515
9750
  if (!cached && options.allowCacheWrite) {
9516
- await writeUpdateCache(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
9751
+ await writeUpdateCache2(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
9517
9752
  }
9518
9753
  return {
9519
9754
  currentVersion,
9520
- isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
9755
+ isUpdateAvailable: compareVersions2(currentVersion, latestVersion) < 0,
9521
9756
  latestVersion,
9522
9757
  registry: options.registry
9523
9758
  };
9524
9759
  }
9525
9760
  async function currentPackageVersion() {
9526
- const rawPackage = await (0, import_promises8.readFile)((0, import_node_path8.join)(toolRoot(), "package.json"), "utf8");
9761
+ const rawPackage = await (0, import_promises9.readFile)((0, import_node_path10.join)(toolRoot(), "package.json"), "utf8");
9527
9762
  const parsed = JSON.parse(rawPackage);
9528
9763
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
9529
9764
  throw new Error("Could not read current threadnote package version.");
9530
9765
  }
9531
9766
  return parsed.version;
9532
9767
  }
9533
- async function fetchLatestVersion(registry) {
9768
+ async function fetchLatestVersion2(registry) {
9534
9769
  const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
9535
9770
  const controller = new AbortController();
9536
9771
  const timeout = setTimeout(() => {
@@ -9571,13 +9806,13 @@ async function readFreshCache(config, registry) {
9571
9806
  return void 0;
9572
9807
  }
9573
9808
  }
9574
- async function writeUpdateCache(config, cache) {
9809
+ async function writeUpdateCache2(config, cache) {
9575
9810
  await ensureDirectory(config.agentContextHome, false);
9576
- await (0, import_promises8.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
9811
+ await (0, import_promises9.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
9577
9812
  `, { encoding: "utf8", mode: 384 });
9578
9813
  }
9579
9814
  function updateCachePath(config) {
9580
- return (0, import_node_path8.join)(config.agentContextHome, "update-check.json");
9815
+ return (0, import_node_path10.join)(config.agentContextHome, "update-check.json");
9581
9816
  }
9582
9817
  async function runApplicablePostUpdateMigrations(config, options) {
9583
9818
  const state = await readPostUpdateState(config);
@@ -9627,10 +9862,10 @@ async function applicablePostUpdateMigrations(config, options) {
9627
9862
  if (handled.has(migration.id)) {
9628
9863
  continue;
9629
9864
  }
9630
- if (compareVersions(options.fromVersion, migration.introducedIn) >= 0) {
9865
+ if (compareVersions2(options.fromVersion, migration.introducedIn) >= 0) {
9631
9866
  continue;
9632
9867
  }
9633
- if (compareVersions(migration.introducedIn, options.toVersion) > 0) {
9868
+ if (compareVersions2(migration.introducedIn, options.toVersion) > 0) {
9634
9869
  continue;
9635
9870
  }
9636
9871
  if (migration.requiresLegacyHandoffs === true && !await hasLegacyLifecycleHandoffCandidates(config)) {
@@ -9641,7 +9876,7 @@ async function applicablePostUpdateMigrations(config, options) {
9641
9876
  return applicable;
9642
9877
  }
9643
9878
  async function readPostUpdateMigrations() {
9644
- const raw = await readFileIfExists((0, import_node_path8.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
9879
+ const raw = await readFileIfExists((0, import_node_path10.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
9645
9880
  if (!raw) {
9646
9881
  return [];
9647
9882
  }
@@ -9680,7 +9915,7 @@ function printPostUpdateMigration(migration) {
9680
9915
  }
9681
9916
  }
9682
9917
  async function confirmPostUpdateMigration(prompt) {
9683
- const readline = (0, import_promises9.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
9918
+ const readline = (0, import_promises10.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
9684
9919
  try {
9685
9920
  const answer = (await readline.question(prompt)).trim().toLowerCase();
9686
9921
  return answer === "y" || answer === "yes";
@@ -9712,11 +9947,11 @@ async function readPostUpdateState(config) {
9712
9947
  }
9713
9948
  async function writePostUpdateState(config, state) {
9714
9949
  await ensureDirectory(config.agentContextHome, false);
9715
- await (0, import_promises8.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
9950
+ await (0, import_promises9.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
9716
9951
  `, { encoding: "utf8", mode: 384 });
9717
9952
  }
9718
9953
  function postUpdateStatePath(config) {
9719
- return (0, import_node_path8.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
9954
+ return (0, import_node_path10.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
9720
9955
  }
9721
9956
  async function resolveUpdateRuntime(runtime) {
9722
9957
  if (runtime !== "auto") {
@@ -9746,18 +9981,18 @@ async function runtimeThreadnoteBin(runtime) {
9746
9981
  if (runtime === "npm") {
9747
9982
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
9748
9983
  const prefix = result.stdout.trim();
9749
- return prefix ? (0, import_node_path8.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
9984
+ return prefix ? (0, import_node_path10.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
9750
9985
  }
9751
9986
  if (runtime === "bun") {
9752
9987
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
9753
9988
  const binDir = result.stdout.trim();
9754
- return binDir ? (0, import_node_path8.join)(binDir, NPM_PACKAGE_NAME) : void 0;
9989
+ return binDir ? (0, import_node_path10.join)(binDir, NPM_PACKAGE_NAME) : void 0;
9755
9990
  }
9756
- return (0, import_node_path8.join)(process.env.DENO_INSTALL ?? (0, import_node_path8.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
9991
+ return (0, import_node_path10.join)(process.env.DENO_INSTALL ?? (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
9757
9992
  }
9758
9993
  async function isExecutable(path) {
9759
9994
  try {
9760
- await (0, import_promises8.access)(path, import_node_fs4.constants.X_OK);
9995
+ await (0, import_promises9.access)(path, import_node_fs5.constants.X_OK);
9761
9996
  return true;
9762
9997
  } catch (_err) {
9763
9998
  return false;
@@ -9798,9 +10033,9 @@ function updateRegistry() {
9798
10033
  function isUpdateNotificationDisabled() {
9799
10034
  return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
9800
10035
  }
9801
- function compareVersions(currentVersion, latestVersion) {
9802
- const current = parseVersion(currentVersion);
9803
- const latest = parseVersion(latestVersion);
10036
+ function compareVersions2(currentVersion, latestVersion) {
10037
+ const current = parseVersion2(currentVersion);
10038
+ const latest = parseVersion2(latestVersion);
9804
10039
  for (let index = 0; index < 3; index += 1) {
9805
10040
  const difference = current.numbers[index] - latest.numbers[index];
9806
10041
  if (difference !== 0) {
@@ -9818,7 +10053,7 @@ function compareVersions(currentVersion, latestVersion) {
9818
10053
  }
9819
10054
  return current.prerelease.localeCompare(latest.prerelease);
9820
10055
  }
9821
- function parseVersion(version) {
10056
+ function parseVersion2(version) {
9822
10057
  const normalized = version.trim().replace(/^v/, "");
9823
10058
  const [core2, prerelease] = normalized.split("-", 2);
9824
10059
  const parts = core2.split(".").map((part) => Number(part));
@@ -9848,9 +10083,9 @@ async function runDoctor(config, options) {
9848
10083
  checks.push(await commandShimCheck());
9849
10084
  checks.push(...await userAgentInstructionsChecks());
9850
10085
  checks.push(await manifestCheck(config.manifestPath));
9851
- checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
9852
- checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
9853
- checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
10086
+ checks.push(await fileCheck((0, import_node_path11.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
10087
+ checks.push(await fileCheck((0, import_node_path11.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
10088
+ checks.push(await fileCheck((0, import_node_path11.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
9854
10089
  checks.push(await healthCheck(config));
9855
10090
  for (const check of checks) {
9856
10091
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -9867,9 +10102,9 @@ async function runInstall(config, options) {
9867
10102
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
9868
10103
  const dryRun = options.dryRun === true;
9869
10104
  await ensureDirectory(config.agentContextHome, dryRun);
9870
- await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "logs"), dryRun);
9871
- await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "redacted"), dryRun);
9872
- await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "mcp"), dryRun);
10105
+ await ensureDirectory((0, import_node_path11.join)(config.agentContextHome, "logs"), dryRun);
10106
+ await ensureDirectory((0, import_node_path11.join)(config.agentContextHome, "redacted"), dryRun);
10107
+ await ensureDirectory((0, import_node_path11.join)(config.agentContextHome, "mcp"), dryRun);
9873
10108
  await installCommandShim(dryRun);
9874
10109
  await installUserAgentInstructions(dryRun);
9875
10110
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
@@ -9895,17 +10130,17 @@ async function runInstall(config, options) {
9895
10130
  }
9896
10131
  await writeTemplateIfMissing({
9897
10132
  config,
9898
- destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ov.conf"),
10133
+ destinationPath: (0, import_node_path11.join)(config.agentContextHome, "ov.conf"),
9899
10134
  dryRun,
9900
10135
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
9901
- templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json")
10136
+ templatePath: (0, import_node_path11.join)(toolRoot(), "config", "ov.conf.template.json")
9902
10137
  });
9903
10138
  await writeTemplateIfMissing({
9904
10139
  config,
9905
- destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ovcli.conf"),
10140
+ destinationPath: (0, import_node_path11.join)(config.agentContextHome, "ovcli.conf"),
9906
10141
  dryRun,
9907
10142
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
9908
- templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json")
10143
+ templatePath: (0, import_node_path11.join)(toolRoot(), "config", "ovcli.conf.template.json")
9909
10144
  });
9910
10145
  if (options.start !== false) {
9911
10146
  const healthy = await repairServerHealth(config, dryRun);
@@ -9959,7 +10194,7 @@ async function runUninstall(config, options) {
9959
10194
  }
9960
10195
  console.log("Uninstalling local Threadnote setup.");
9961
10196
  await runStop(config, { dryRun });
9962
- await removePathIfExists((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
10197
+ await removePathIfExists((0, import_node_path11.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
9963
10198
  await removeLaunchAgent(dryRun);
9964
10199
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
9965
10200
  await removeMcpSnippets(config, dryRun);
@@ -10016,16 +10251,16 @@ async function repairManifest(config, dryRun) {
10016
10251
  console.log(output2.trimEnd());
10017
10252
  return;
10018
10253
  }
10019
- await ensureDirectory((0, import_node_path9.dirname)(config.manifestPath), false);
10254
+ await ensureDirectory((0, import_node_path11.dirname)(config.manifestPath), false);
10020
10255
  const currentContent = await readFileIfExists(config.manifestPath);
10021
10256
  if (currentContent !== void 0) {
10022
10257
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
10023
- await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10024
- await (0, import_promises10.chmod)(backupPath, 384);
10258
+ await (0, import_promises11.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10259
+ await (0, import_promises11.chmod)(backupPath, 384);
10025
10260
  console.log(`Backup: ${backupPath}`);
10026
10261
  }
10027
- await (0, import_promises10.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
10028
- await (0, import_promises10.chmod)(config.manifestPath, 384);
10262
+ await (0, import_promises11.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
10263
+ await (0, import_promises11.chmod)(config.manifestPath, 384);
10029
10264
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
10030
10265
  }
10031
10266
  async function repairServerHealth(config, dryRun) {
@@ -10066,16 +10301,16 @@ async function runStart(config, options) {
10066
10301
  );
10067
10302
  }
10068
10303
  const logPath = openVikingLogPath(config);
10069
- await ensureDirectory((0, import_node_path9.dirname)(logPath), false);
10304
+ await ensureDirectory((0, import_node_path11.dirname)(logPath), false);
10070
10305
  if (options.foreground === true) {
10071
10306
  const result = await runInteractive(server, args);
10072
10307
  process.exitCode = result;
10073
10308
  return;
10074
10309
  }
10075
- const logFd = (0, import_node_fs5.openSync)(logPath, "a");
10310
+ const logFd = (0, import_node_fs6.openSync)(logPath, "a");
10076
10311
  const child = spawnDetachedServerWithLog(server, args, logFd);
10077
10312
  child.unref();
10078
- await (0, import_promises10.writeFile)((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
10313
+ await (0, import_promises11.writeFile)((0, import_node_path11.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
10079
10314
  `, "utf8");
10080
10315
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
10081
10316
  if (health) {
@@ -10090,11 +10325,11 @@ function spawnDetachedServerWithLog(server, args, logFd) {
10090
10325
  try {
10091
10326
  return spawnDetachedServer(server, args, logFd);
10092
10327
  } finally {
10093
- (0, import_node_fs5.closeSync)(logFd);
10328
+ (0, import_node_fs6.closeSync)(logFd);
10094
10329
  }
10095
10330
  }
10096
10331
  function spawnDetachedServer(server, args, logFd) {
10097
- return (0, import_node_child_process2.spawn)(server, args, {
10332
+ return (0, import_node_child_process3.spawn)(server, args, {
10098
10333
  detached: true,
10099
10334
  stdio: ["ignore", logFd, logFd]
10100
10335
  });
@@ -10108,7 +10343,7 @@ async function runStop(config, options) {
10108
10343
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
10109
10344
  }
10110
10345
  }
10111
- const pidPath = (0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid");
10346
+ const pidPath = (0, import_node_path11.join)(config.agentContextHome, "openviking-server.pid");
10112
10347
  const pidText = await readFileIfExists(pidPath);
10113
10348
  if (!pidText) {
10114
10349
  console.log("No pid file found for detached OpenViking server.");
@@ -10204,7 +10439,7 @@ async function pythonSystemCertificatesCheck() {
10204
10439
  };
10205
10440
  }
10206
10441
  async function commandShimCheck() {
10207
- const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10442
+ const shimPath = (0, import_node_path11.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10208
10443
  const content = await readFileIfExists(shimPath);
10209
10444
  if (content === void 0) {
10210
10445
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -10270,11 +10505,11 @@ async function hasPythonModule(serverPath, moduleName) {
10270
10505
  async function siblingPythonForExecutable(executablePath) {
10271
10506
  let resolvedPath;
10272
10507
  try {
10273
- resolvedPath = await (0, import_promises10.realpath)(executablePath);
10508
+ resolvedPath = await (0, import_promises11.realpath)(executablePath);
10274
10509
  } catch (_err) {
10275
10510
  return void 0;
10276
10511
  }
10277
- const pythonPath = (0, import_node_path9.join)((0, import_node_path9.dirname)(resolvedPath), "python");
10512
+ const pythonPath = (0, import_node_path11.join)((0, import_node_path11.dirname)(resolvedPath), "python");
10278
10513
  return await exists(pythonPath) ? pythonPath : void 0;
10279
10514
  }
10280
10515
  async function manifestCheck(path) {
@@ -10345,7 +10580,7 @@ async function offerToInstallUv() {
10345
10580
  );
10346
10581
  return false;
10347
10582
  }
10348
- const readline = (0, import_promises11.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
10583
+ const readline = (0, import_promises12.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
10349
10584
  let answer;
10350
10585
  try {
10351
10586
  answer = (await readline.question(
@@ -10468,38 +10703,38 @@ function printInstallNextSteps(options) {
10468
10703
  }
10469
10704
  async function writeTemplateIfMissing(options) {
10470
10705
  if (await exists(options.destinationPath)) {
10471
- const currentContent = await (0, import_promises10.readFile)(options.destinationPath, "utf8");
10706
+ const currentContent = await (0, import_promises11.readFile)(options.destinationPath, "utf8");
10472
10707
  if (options.shouldRepair?.(currentContent) !== true) {
10473
10708
  console.log(`Already exists: ${options.destinationPath}`);
10474
10709
  return;
10475
10710
  }
10476
- const rendered2 = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
10711
+ const rendered2 = renderTemplate(await (0, import_promises11.readFile)(options.templatePath, "utf8"), options.config);
10477
10712
  if (options.dryRun) {
10478
10713
  console.log(`Would repair generated config: ${options.destinationPath}`);
10479
10714
  return;
10480
10715
  }
10481
10716
  const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
10482
- await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10483
- await (0, import_promises10.chmod)(backupPath, 384);
10484
- await (0, import_promises10.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
10485
- await (0, import_promises10.chmod)(options.destinationPath, 384);
10717
+ await (0, import_promises11.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10718
+ await (0, import_promises11.chmod)(backupPath, 384);
10719
+ await (0, import_promises11.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
10720
+ await (0, import_promises11.chmod)(options.destinationPath, 384);
10486
10721
  console.log(`Repaired generated config: ${options.destinationPath}`);
10487
10722
  console.log(`Backup: ${backupPath}`);
10488
10723
  return;
10489
10724
  }
10490
- const rendered = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
10725
+ const rendered = renderTemplate(await (0, import_promises11.readFile)(options.templatePath, "utf8"), options.config);
10491
10726
  if (options.dryRun) {
10492
10727
  console.log(`Would write ${options.destinationPath}`);
10493
10728
  return;
10494
10729
  }
10495
- await ensureDirectory((0, import_node_path9.dirname)(options.destinationPath), false);
10496
- await (0, import_promises10.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
10497
- await (0, import_promises10.chmod)(options.destinationPath, 384);
10730
+ await ensureDirectory((0, import_node_path11.dirname)(options.destinationPath), false);
10731
+ await (0, import_promises11.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
10732
+ await (0, import_promises11.chmod)(options.destinationPath, 384);
10498
10733
  console.log(`Wrote ${options.destinationPath}`);
10499
10734
  }
10500
10735
  async function installCommandShim(dryRun) {
10501
10736
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
10502
- const shimPath = (0, import_node_path9.join)(binDir, "threadnote");
10737
+ const shimPath = (0, import_node_path11.join)(binDir, "threadnote");
10503
10738
  const existingContent = await readFileIfExists(shimPath);
10504
10739
  if (existingContent && !isManagedCommandShim(existingContent)) {
10505
10740
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -10515,12 +10750,12 @@ async function installCommandShim(dryRun) {
10515
10750
  return;
10516
10751
  }
10517
10752
  await ensureDirectory(binDir, false);
10518
- await (0, import_promises10.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10519
- await (0, import_promises10.chmod)(shimPath, 493);
10753
+ await (0, import_promises11.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10754
+ await (0, import_promises11.chmod)(shimPath, 493);
10520
10755
  console.log(`Wrote command shim: ${shimPath}`);
10521
10756
  }
10522
10757
  async function removeCommandShim(dryRun) {
10523
- const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10758
+ const shimPath = (0, import_node_path11.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10524
10759
  const content = await readFileIfExists(shimPath);
10525
10760
  if (content === void 0) {
10526
10761
  console.log(`Already absent: ${shimPath}`);
@@ -10554,8 +10789,8 @@ async function installUserAgentInstructions(dryRun) {
10554
10789
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
10555
10790
  continue;
10556
10791
  }
10557
- await ensureDirectory((0, import_node_path9.dirname)(targetPath), false);
10558
- await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10792
+ await ensureDirectory((0, import_node_path11.dirname)(targetPath), false);
10793
+ await (0, import_promises11.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10559
10794
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
10560
10795
  }
10561
10796
  }
@@ -10592,7 +10827,7 @@ async function removeUserAgentInstructions(dryRun) {
10592
10827
  console.log(`Would update ${targetPath}`);
10593
10828
  continue;
10594
10829
  }
10595
- await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10830
+ await (0, import_promises11.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10596
10831
  console.log(`Updated ${targetPath}`);
10597
10832
  }
10598
10833
  }
@@ -10612,7 +10847,7 @@ async function renderUserAgentInstructions(target) {
10612
10847
  ].join("\n");
10613
10848
  }
10614
10849
  async function renderUserAgentInstructionsBlock() {
10615
- const instructions = (await (0, import_promises10.readFile)((0, import_node_path9.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
10850
+ const instructions = (await (0, import_promises11.readFile)((0, import_node_path11.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
10616
10851
  return `${USER_INSTRUCTIONS_START_MARKER}
10617
10852
  ${instructions}
10618
10853
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -10690,9 +10925,9 @@ async function installLaunchAgent(config, dryRun) {
10690
10925
  if ((0, import_node_os6.platform)() !== "darwin") {
10691
10926
  throw new Error("launchd autostart is only supported on macOS.");
10692
10927
  }
10693
- const source = (0, import_node_path9.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
10928
+ const source = (0, import_node_path11.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
10694
10929
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
10695
- const rendered = renderTemplate(await (0, import_promises10.readFile)(source, "utf8"), config);
10930
+ const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config);
10696
10931
  if (dryRun) {
10697
10932
  console.log(`Would write ${destination}`);
10698
10933
  console.log(`Would run: launchctl unload ${destination}`);
@@ -10700,9 +10935,9 @@ async function installLaunchAgent(config, dryRun) {
10700
10935
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
10701
10936
  return;
10702
10937
  }
10703
- await ensureDirectory((0, import_node_path9.dirname)(destination), false);
10704
- await ensureDirectory((0, import_node_path9.dirname)(openVikingLogPath(config)), false);
10705
- await (0, import_promises10.writeFile)(destination, rendered, "utf8");
10938
+ await ensureDirectory((0, import_node_path11.dirname)(destination), false);
10939
+ await ensureDirectory((0, import_node_path11.dirname)(openVikingLogPath(config)), false);
10940
+ await (0, import_promises11.writeFile)(destination, rendered, "utf8");
10706
10941
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
10707
10942
  await maybeRun(false, "launchctl", ["load", destination]);
10708
10943
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
@@ -10765,7 +11000,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
10765
11000
  if (typeof parsed.default_user !== "string") {
10766
11001
  return false;
10767
11002
  }
10768
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path9.join)(config.agentContextHome, "data")) {
11003
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path11.join)(config.agentContextHome, "data")) {
10769
11004
  return false;
10770
11005
  }
10771
11006
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -10911,7 +11146,13 @@ async function main() {
10911
11146
  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) => {
10912
11147
  await runShareSync(getRuntimeConfig(program2), options);
10913
11148
  });
10914
- 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) => {
11149
+ 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").option(
11150
+ "--preview",
11151
+ "Print the exact bytes that would land in the shared git repo (after frontmatter strip and scrubber redaction) without writing, committing, or pushing"
11152
+ ).option(
11153
+ "--redact",
11154
+ "Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
11155
+ ).action(async (uri, options) => {
10915
11156
  await runSharePublish(getRuntimeConfig(program2), uri, options);
10916
11157
  });
10917
11158
  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) => {