threadnote 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3489,9 +3489,14 @@ var USER_INSTRUCTIONS_START_MARKER = "<!-- BEGIN THREADNOTE USER INSTRUCTIONS --
3489
3489
  var USER_INSTRUCTIONS_END_MARKER = "<!-- END THREADNOTE USER INSTRUCTIONS -->";
3490
3490
  var USER_MANIFEST_NAME = "seed-manifest.yaml";
3491
3491
  var USER_AGENT_INSTRUCTION_TARGETS = [
3492
- { label: "codex user instructions", path: "~/.codex/AGENTS.md" },
3493
- { label: "claude user instructions", path: "~/.claude/CLAUDE.md" },
3494
- { label: "cursor user rule", path: "~/.cursor/rules/threadnote.md" }
3492
+ { kind: "block", label: "codex user instructions", path: "~/.codex/AGENTS.md" },
3493
+ { kind: "block", label: "claude user instructions", path: "~/.claude/CLAUDE.md" },
3494
+ { kind: "block", label: "cursor user rule", path: "~/.cursor/rules/threadnote.md" },
3495
+ {
3496
+ kind: "file",
3497
+ label: "copilot user instructions",
3498
+ path: "~/.copilot/instructions/threadnote.instructions.md"
3499
+ }
3495
3500
  ];
3496
3501
  var DEFAULT_SEED_PATTERNS = [
3497
3502
  "AGENTS.md",
@@ -3981,6 +3986,7 @@ function errorMessage(err) {
3981
3986
  }
3982
3987
 
3983
3988
  // src/mcp.ts
3989
+ var import_node_fs2 = require("node:fs");
3984
3990
  var import_promises2 = require("node:fs/promises");
3985
3991
  var import_node_path2 = require("node:path");
3986
3992
  var import_node_os2 = require("node:os");
@@ -4010,6 +4016,15 @@ async function runMcpInstall(config, agent, options) {
4010
4016
  });
4011
4017
  return;
4012
4018
  }
4019
+ if (agent === "copilot") {
4020
+ await runCopilotMcpInstall(config, name, {
4021
+ apply,
4022
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
4023
+ nativeHttp,
4024
+ url
4025
+ });
4026
+ return;
4027
+ }
4013
4028
  const command = buildMcpInstallCommand(config, agent, name, {
4014
4029
  bearerTokenEnvVar: options.bearerTokenEnvVar,
4015
4030
  nativeHttp,
@@ -4059,6 +4074,34 @@ async function runCursorMcpInstall(config, name, options) {
4059
4074
  await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
4060
4075
  console.log(currentContent === void 0 ? `Wrote Cursor MCP config: ${path}` : `Updated Cursor MCP config: ${path}`);
4061
4076
  }
4077
+ async function runCopilotMcpInstall(config, name, options) {
4078
+ const path = copilotMcpConfigPath();
4079
+ const serverConfig = buildCopilotMcpServerConfig(config, {
4080
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
4081
+ nativeHttp: options.nativeHttp,
4082
+ url: options.url
4083
+ });
4084
+ const currentContent = await readFileIfExists(path);
4085
+ const nextContent = renderCopilotMcpConfig(currentContent, name, serverConfig);
4086
+ if (!options.apply) {
4087
+ console.log("Dry run. Re-run with --apply to modify GitHub Copilot MCP config.");
4088
+ printCopilotMcpSnippet(config, name, {
4089
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
4090
+ nativeHttp: options.nativeHttp,
4091
+ url: options.url
4092
+ });
4093
+ return;
4094
+ }
4095
+ if (currentContent === nextContent) {
4096
+ console.log(`Already configured: ${path}`);
4097
+ return;
4098
+ }
4099
+ await ensureDirectory((0, import_node_path2.dirname)(path), false);
4100
+ await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
4101
+ console.log(
4102
+ currentContent === void 0 ? `Wrote GitHub Copilot MCP config: ${path}` : `Updated GitHub Copilot MCP config: ${path}`
4103
+ );
4104
+ }
4062
4105
  async function removeMcpConfigs(value, dryRun) {
4063
4106
  const clients = await resolveMcpClients(value, "remove");
4064
4107
  if (clients.length === 0) {
@@ -4070,6 +4113,10 @@ async function removeMcpConfigs(value, dryRun) {
4070
4113
  await removeCursorMcpConfig(OPENVIKING_MCP_NAME, dryRun);
4071
4114
  continue;
4072
4115
  }
4116
+ if (client === "copilot") {
4117
+ await removeCopilotMcpConfig(OPENVIKING_MCP_NAME, dryRun);
4118
+ continue;
4119
+ }
4073
4120
  const command = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
4074
4121
  await maybeRun(dryRun, command.executable, command.args, { allowFailure: true, cwd: command.cwd });
4075
4122
  }
@@ -4090,11 +4137,19 @@ async function removeMcpSnippets(config, dryRun) {
4090
4137
  "MCP snippet",
4091
4138
  dryRun
4092
4139
  );
4140
+ await removePathIfExists(
4141
+ (0, import_node_path2.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.copilot.json`),
4142
+ "MCP snippet",
4143
+ dryRun
4144
+ );
4093
4145
  }
4094
4146
  function buildMcpInstallCommand(config, agent, name, options) {
4095
4147
  if (agent === "cursor") {
4096
4148
  throw new Error("Cursor MCP config is written directly to ~/.cursor/mcp.json.");
4097
4149
  }
4150
+ if (agent === "copilot") {
4151
+ throw new Error("GitHub Copilot MCP config is written directly to the VS Code user mcp.json file.");
4152
+ }
4098
4153
  const claudeCwd = getInvocationCwd();
4099
4154
  const claudeScope = options.scope ?? "user";
4100
4155
  if (!options.nativeHttp) {
@@ -4139,6 +4194,9 @@ function buildMcpRemoveCommand(agent, name) {
4139
4194
  if (agent === "cursor") {
4140
4195
  throw new Error("Cursor MCP config is removed directly from ~/.cursor/mcp.json.");
4141
4196
  }
4197
+ if (agent === "copilot") {
4198
+ throw new Error("GitHub Copilot MCP config is removed directly from the VS Code user mcp.json file.");
4199
+ }
4142
4200
  return agent === "codex" ? { executable: "codex", args: ["mcp", "remove", name] } : { executable: "claude", args: ["mcp", "remove", name], cwd: getInvocationCwd() };
4143
4201
  }
4144
4202
  function mcpEnvironment(config) {
@@ -4171,6 +4229,21 @@ function buildCursorMcpServerConfig(config, options) {
4171
4229
  env: mcpEnvironmentObject(config)
4172
4230
  };
4173
4231
  }
4232
+ function buildCopilotMcpServerConfig(config, options) {
4233
+ if (options.nativeHttp) {
4234
+ const server = { type: "http", url: options.url };
4235
+ if (options.bearerTokenEnvVar) {
4236
+ server.headers = { Authorization: `Bearer \${env:${options.bearerTokenEnvVar}}` };
4237
+ }
4238
+ return server;
4239
+ }
4240
+ return {
4241
+ args: [mcpAdapterCommand()[0]],
4242
+ command: "/usr/bin/env",
4243
+ env: mcpEnvironmentObject(config),
4244
+ type: "stdio"
4245
+ };
4246
+ }
4174
4247
  function renderCursorMcpConfig(currentContent, name, serverConfig) {
4175
4248
  const parsed = currentContent === void 0 ? {} : parseJsonConfigObject(currentContent);
4176
4249
  if (parsed === void 0) {
@@ -4186,6 +4259,21 @@ function renderCursorMcpConfig(currentContent, name, serverConfig) {
4186
4259
  return `${JSON.stringify(nextConfig, null, 2)}
4187
4260
  `;
4188
4261
  }
4262
+ function renderCopilotMcpConfig(currentContent, name, serverConfig) {
4263
+ const parsed = currentContent === void 0 ? {} : parseJsonConfigObject(currentContent);
4264
+ if (parsed === void 0) {
4265
+ throw new Error(`${copilotMcpConfigPath()} exists but is not a JSON object; not modifying it.`);
4266
+ }
4267
+ if (parsed.servers !== void 0 && !isJsonObject(parsed.servers)) {
4268
+ throw new Error(`${copilotMcpConfigPath()} has a non-object servers field; not modifying it.`);
4269
+ }
4270
+ const nextConfig = { ...parsed };
4271
+ const servers = isJsonObject(parsed.servers) ? { ...parsed.servers } : {};
4272
+ servers[name] = serverConfig;
4273
+ nextConfig.servers = servers;
4274
+ return `${JSON.stringify(nextConfig, null, 2)}
4275
+ `;
4276
+ }
4189
4277
  async function removeCursorMcpConfig(name, dryRun) {
4190
4278
  const path = cursorMcpConfigPath();
4191
4279
  const currentContent = await readFileIfExists(path);
@@ -4215,11 +4303,44 @@ async function removeCursorMcpConfig(name, dryRun) {
4215
4303
  await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
4216
4304
  console.log(`Updated Cursor MCP config: ${path}`);
4217
4305
  }
4306
+ async function removeCopilotMcpConfig(name, dryRun) {
4307
+ const path = copilotMcpConfigPath();
4308
+ const currentContent = await readFileIfExists(path);
4309
+ if (currentContent === void 0) {
4310
+ console.log(`Already absent: ${path}`);
4311
+ return;
4312
+ }
4313
+ const parsed = parseJsonConfigObject(currentContent);
4314
+ if (parsed === void 0) {
4315
+ console.log(`WARN ${path} exists but is not a JSON object; not modifying it.`);
4316
+ return;
4317
+ }
4318
+ if (!isJsonObject(parsed.servers) || parsed.servers[name] === void 0) {
4319
+ console.log(`No GitHub Copilot MCP config found: ${path}`);
4320
+ return;
4321
+ }
4322
+ const nextConfig = { ...parsed };
4323
+ const servers = { ...parsed.servers };
4324
+ delete servers[name];
4325
+ nextConfig.servers = servers;
4326
+ const nextContent = `${JSON.stringify(nextConfig, null, 2)}
4327
+ `;
4328
+ if (dryRun) {
4329
+ console.log(`Would update GitHub Copilot MCP config: ${path}`);
4330
+ return;
4331
+ }
4332
+ await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
4333
+ console.log(`Updated GitHub Copilot MCP config: ${path}`);
4334
+ }
4218
4335
  function printMcpSnippet(config, agent, name, options) {
4219
4336
  if (agent === "cursor") {
4220
4337
  printCursorMcpSnippet(config, name, { nativeHttp: options.nativeHttp, url: options.url });
4221
4338
  return;
4222
4339
  }
4340
+ if (agent === "copilot") {
4341
+ printCopilotMcpSnippet(config, name, { nativeHttp: options.nativeHttp, url: options.url });
4342
+ return;
4343
+ }
4223
4344
  const snippetPath = (0, import_node_path2.join)(config.agentContextHome, "mcp", `${name}.${agent}.${agent === "codex" ? "toml" : "txt"}`);
4224
4345
  const command = buildMcpInstallCommand(config, agent, name, {
4225
4346
  nativeHttp: options.nativeHttp,
@@ -4239,14 +4360,37 @@ function printCursorMcpSnippet(config, name, options) {
4239
4360
  Snippet (${snippetPath}; merge into ${cursorMcpConfigPath()}):
4240
4361
  ${snippet2}`);
4241
4362
  }
4363
+ function printCopilotMcpSnippet(config, name, options) {
4364
+ const snippetPath = (0, import_node_path2.join)(config.agentContextHome, "mcp", `${name}.copilot.json`);
4365
+ const snippet2 = JSON.stringify({ servers: { [name]: buildCopilotMcpServerConfig(config, options) } }, null, 2);
4366
+ console.log(`
4367
+ Snippet (${snippetPath}; merge into ${copilotMcpConfigPath()}):
4368
+ ${snippet2}`);
4369
+ }
4242
4370
  function cursorMcpConfigPath() {
4243
4371
  return expandPath("~/.cursor/mcp.json");
4244
4372
  }
4373
+ function copilotMcpConfigPath() {
4374
+ if (process.env.THREADNOTE_COPILOT_MCP_CONFIG) {
4375
+ return expandPath(process.env.THREADNOTE_COPILOT_MCP_CONFIG);
4376
+ }
4377
+ if ((0, import_node_os2.platform)() === "darwin") {
4378
+ const stablePath = expandPath("~/Library/Application Support/Code/User/mcp.json");
4379
+ const insidersPath = expandPath("~/Library/Application Support/Code - Insiders/User/mcp.json");
4380
+ return existsSyncDirectory((0, import_node_path2.dirname)(stablePath)) || !existsSyncDirectory((0, import_node_path2.dirname)(insidersPath)) ? stablePath : insidersPath;
4381
+ }
4382
+ if ((0, import_node_os2.platform)() === "win32") {
4383
+ const appData = process.env.APPDATA;
4384
+ return appData ? (0, import_node_path2.join)(appData, "Code", "User", "mcp.json") : expandPath("~/AppData/Roaming/Code/User/mcp.json");
4385
+ }
4386
+ const configHome = process.env.XDG_CONFIG_HOME ? expandPath(process.env.XDG_CONFIG_HOME) : expandPath("~/.config");
4387
+ return (0, import_node_path2.join)(configHome, "Code", "User", "mcp.json");
4388
+ }
4245
4389
  function parseAgentClient(value) {
4246
- if (value === "codex" || value === "claude" || value === "cursor") {
4390
+ if (value === "codex" || value === "claude" || value === "copilot" || value === "cursor") {
4247
4391
  return value;
4248
4392
  }
4249
- throw new Error(`Unsupported agent: ${value}. Expected codex, claude, or cursor.`);
4393
+ throw new Error(`Unsupported agent: ${value}. Expected codex, claude, copilot, or cursor.`);
4250
4394
  }
4251
4395
  function parseClaudeMcpScope(value) {
4252
4396
  if (value === "local" || value === "project" || value === "user") {
@@ -4261,7 +4405,7 @@ async function resolveMcpClients(value, action) {
4261
4405
  }
4262
4406
  let requested;
4263
4407
  if (normalized === "available" || normalized === "all") {
4264
- requested = ["codex", "claude", "cursor"];
4408
+ requested = ["codex", "claude", "cursor", "copilot"];
4265
4409
  } else {
4266
4410
  requested = normalized.split(",").map((part) => part.trim()).filter(Boolean).map(parseAgentClient);
4267
4411
  }
@@ -4277,6 +4421,16 @@ async function resolveMcpClients(value, action) {
4277
4421
  }
4278
4422
  continue;
4279
4423
  }
4424
+ if (client === "copilot") {
4425
+ if (!await isCopilotAvailable()) {
4426
+ console.log(`WARN VS Code/Copilot config not found; cannot ${action} copilot MCP config.`);
4427
+ continue;
4428
+ }
4429
+ if (!clients.includes(client)) {
4430
+ clients.push(client);
4431
+ }
4432
+ continue;
4433
+ }
4280
4434
  if (!await findExecutable([client])) {
4281
4435
  console.log(`WARN ${client} command not found; cannot ${action} ${client} MCP config.`);
4282
4436
  continue;
@@ -4296,9 +4450,28 @@ async function isCursorAvailable() {
4296
4450
  }
4297
4451
  return (0, import_node_os2.platform)() === "darwin" && await exists("/Applications/Cursor.app");
4298
4452
  }
4453
+ async function isCopilotAvailable() {
4454
+ if (process.env.THREADNOTE_COPILOT_MCP_CONFIG) {
4455
+ return true;
4456
+ }
4457
+ if (await exists((0, import_node_path2.dirname)(copilotMcpConfigPath()))) {
4458
+ return true;
4459
+ }
4460
+ if (await findExecutable(["code", "code-insiders"])) {
4461
+ return true;
4462
+ }
4463
+ return (0, import_node_os2.platform)() === "darwin" && await exists("/Applications/Visual Studio Code.app");
4464
+ }
4465
+ function existsSyncDirectory(path) {
4466
+ try {
4467
+ return (0, import_node_fs2.statSync)(path).isDirectory();
4468
+ } catch (_err) {
4469
+ return false;
4470
+ }
4471
+ }
4299
4472
 
4300
4473
  // src/runtime.ts
4301
- var import_node_fs2 = require("node:fs");
4474
+ var import_node_fs3 = require("node:fs");
4302
4475
  var import_node_os3 = require("node:os");
4303
4476
  var import_node_path3 = require("node:path");
4304
4477
  function getRuntimeConfig(program2, manifestOverride) {
@@ -4320,7 +4493,7 @@ function getRuntimeConfig(program2, manifestOverride) {
4320
4493
  }
4321
4494
  function defaultManifestPath(agentContextHome) {
4322
4495
  const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
4323
- return (0, import_node_fs2.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
4496
+ return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
4324
4497
  }
4325
4498
  function builtInExampleManifestPath() {
4326
4499
  return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
@@ -7209,9 +7382,9 @@ async function runRead(config, uri, options) {
7209
7382
  const ov = await openVikingCliForMode(options.dryRun === true);
7210
7383
  const result = await maybeRun(options.dryRun === true, ov, withIdentity(config, ["read", uri]));
7211
7384
  if (result && result.stdout.includes("[Directory overview is not ready]") && (uri.endsWith("/.overview.md") || uri.endsWith("/.abstract.md"))) {
7212
- const parentUri = parentVikingUri(uri);
7385
+ const parentUri2 = parentVikingUri(uri);
7213
7386
  console.log("\nThis is a generated summary placeholder. To read the underlying content, inspect leaf nodes:");
7214
- console.log(` threadnote list ${parentUri} --all --recursive`);
7387
+ console.log(` threadnote list ${parentUri2} --all --recursive`);
7215
7388
  }
7216
7389
  }
7217
7390
  async function runList(config, uri, options) {
@@ -7554,6 +7727,7 @@ function exactMemoryScopes(config, includeArchived) {
7554
7727
  `${userBase}/handoffs/active`,
7555
7728
  `${userBase}/incidents/active`,
7556
7729
  `${userBase}/events`,
7730
+ `${userBase}/shared`,
7557
7731
  `viking://agent/${uriSegment(config.agentId)}/memories`
7558
7732
  ];
7559
7733
  return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
@@ -8222,19 +8396,780 @@ function detectSecretMatches(content) {
8222
8396
  return matches;
8223
8397
  }
8224
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
+
8225
9160
  // src/lifecycle.ts
8226
9161
  var import_node_child_process2 = require("node:child_process");
9162
+ var import_node_fs5 = require("node:fs");
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
+
9167
+ // src/update.ts
8227
9168
  var import_node_fs4 = require("node:fs");
8228
- var import_promises8 = require("node:fs/promises");
9169
+ var import_promises7 = require("node:fs/promises");
8229
9170
  var import_node_os5 = require("node:os");
8230
9171
  var import_node_path7 = require("node:path");
8231
-
8232
- // src/update.ts
8233
- var import_node_fs3 = require("node:fs");
8234
- var import_promises6 = require("node:fs/promises");
8235
- var import_node_os4 = require("node:os");
8236
- var import_node_path6 = require("node:path");
8237
- var import_promises7 = require("node:readline/promises");
9172
+ var import_promises8 = require("node:readline/promises");
8238
9173
  var import_node_process = require("node:process");
8239
9174
  var NPM_PACKAGE_NAME = "threadnote";
8240
9175
  var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
@@ -8321,7 +9256,9 @@ async function runUpdate(config, options) {
8321
9256
  } else {
8322
9257
  console.log("Skipping post-update migration prompts because --no-post-update was provided.");
8323
9258
  }
8324
- console.log("Update complete. Restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.");
9259
+ console.log(
9260
+ "Update complete. Restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload."
9261
+ );
8325
9262
  }
8326
9263
  async function runPostUpdate(config, options) {
8327
9264
  if (!options.fromVersion || !options.toVersion) {
@@ -8380,7 +9317,7 @@ async function getUpdateInfo(config, options) {
8380
9317
  };
8381
9318
  }
8382
9319
  async function currentPackageVersion() {
8383
- const rawPackage = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), "package.json"), "utf8");
9320
+ const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
8384
9321
  const parsed = JSON.parse(rawPackage);
8385
9322
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8386
9323
  throw new Error("Could not read current threadnote package version.");
@@ -8430,11 +9367,11 @@ async function readFreshCache(config, registry) {
8430
9367
  }
8431
9368
  async function writeUpdateCache(config, cache) {
8432
9369
  await ensureDirectory(config.agentContextHome, false);
8433
- await (0, import_promises6.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
9370
+ await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
8434
9371
  `, { encoding: "utf8", mode: 384 });
8435
9372
  }
8436
9373
  function updateCachePath(config) {
8437
- return (0, import_node_path6.join)(config.agentContextHome, "update-check.json");
9374
+ return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
8438
9375
  }
8439
9376
  async function runApplicablePostUpdateMigrations(config, options) {
8440
9377
  const state = await readPostUpdateState(config);
@@ -8498,7 +9435,7 @@ async function applicablePostUpdateMigrations(config, options) {
8498
9435
  return applicable;
8499
9436
  }
8500
9437
  async function readPostUpdateMigrations() {
8501
- const raw = await readFileIfExists((0, import_node_path6.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
9438
+ const raw = await readFileIfExists((0, import_node_path7.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
8502
9439
  if (!raw) {
8503
9440
  return [];
8504
9441
  }
@@ -8537,7 +9474,7 @@ function printPostUpdateMigration(migration) {
8537
9474
  }
8538
9475
  }
8539
9476
  async function confirmPostUpdateMigration(prompt) {
8540
- const readline = (0, import_promises7.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
9477
+ const readline = (0, import_promises8.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
8541
9478
  try {
8542
9479
  const answer = (await readline.question(prompt)).trim().toLowerCase();
8543
9480
  return answer === "y" || answer === "yes";
@@ -8569,11 +9506,11 @@ async function readPostUpdateState(config) {
8569
9506
  }
8570
9507
  async function writePostUpdateState(config, state) {
8571
9508
  await ensureDirectory(config.agentContextHome, false);
8572
- await (0, import_promises6.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
9509
+ await (0, import_promises7.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
8573
9510
  `, { encoding: "utf8", mode: 384 });
8574
9511
  }
8575
9512
  function postUpdateStatePath(config) {
8576
- return (0, import_node_path6.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
9513
+ return (0, import_node_path7.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
8577
9514
  }
8578
9515
  async function resolveUpdateRuntime(runtime) {
8579
9516
  if (runtime !== "auto") {
@@ -8603,18 +9540,18 @@ async function runtimeThreadnoteBin(runtime) {
8603
9540
  if (runtime === "npm") {
8604
9541
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
8605
9542
  const prefix = result.stdout.trim();
8606
- return prefix ? (0, import_node_path6.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
9543
+ return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
8607
9544
  }
8608
9545
  if (runtime === "bun") {
8609
9546
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
8610
9547
  const binDir = result.stdout.trim();
8611
- return binDir ? (0, import_node_path6.join)(binDir, NPM_PACKAGE_NAME) : void 0;
9548
+ return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
8612
9549
  }
8613
- return (0, import_node_path6.join)(process.env.DENO_INSTALL ?? (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
9550
+ return (0, import_node_path7.join)(process.env.DENO_INSTALL ?? (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
8614
9551
  }
8615
9552
  async function isExecutable(path) {
8616
9553
  try {
8617
- await (0, import_promises6.access)(path, import_node_fs3.constants.X_OK);
9554
+ await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
8618
9555
  return true;
8619
9556
  } catch (_err) {
8620
9557
  return false;
@@ -8692,7 +9629,7 @@ function safeVersionNumber(value) {
8692
9629
  async function runDoctor(config, options) {
8693
9630
  const checks = [];
8694
9631
  checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
8695
- checks.push({ name: "platform", status: (0, import_node_os5.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os5.platform)() });
9632
+ checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
8696
9633
  checks.push(await commandCheck("node", ["--version"]));
8697
9634
  checks.push(await commandCheck("python3", ["--version"]));
8698
9635
  checks.push(await commandCheck("openviking-server", ["--help"]));
@@ -8705,9 +9642,9 @@ async function runDoctor(config, options) {
8705
9642
  checks.push(await commandShimCheck());
8706
9643
  checks.push(...await userAgentInstructionsChecks());
8707
9644
  checks.push(await manifestCheck(config.manifestPath));
8708
- checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
8709
- checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
8710
- checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
9645
+ checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
9646
+ checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
9647
+ checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
8711
9648
  checks.push(await healthCheck(config));
8712
9649
  for (const check of checks) {
8713
9650
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -8724,9 +9661,9 @@ async function runInstall(config, options) {
8724
9661
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
8725
9662
  const dryRun = options.dryRun === true;
8726
9663
  await ensureDirectory(config.agentContextHome, dryRun);
8727
- await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "logs"), dryRun);
8728
- await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "redacted"), dryRun);
8729
- await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "mcp"), dryRun);
9664
+ await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "logs"), dryRun);
9665
+ await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "redacted"), dryRun);
9666
+ await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "mcp"), dryRun);
8730
9667
  await installCommandShim(dryRun);
8731
9668
  await installUserAgentInstructions(dryRun);
8732
9669
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
@@ -8752,17 +9689,17 @@ async function runInstall(config, options) {
8752
9689
  }
8753
9690
  await writeTemplateIfMissing({
8754
9691
  config,
8755
- destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ov.conf"),
9692
+ destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ov.conf"),
8756
9693
  dryRun,
8757
9694
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
8758
- templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ov.conf.template.json")
9695
+ templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json")
8759
9696
  });
8760
9697
  await writeTemplateIfMissing({
8761
9698
  config,
8762
- destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ovcli.conf"),
9699
+ destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ovcli.conf"),
8763
9700
  dryRun,
8764
9701
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
8765
- templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ovcli.conf.template.json")
9702
+ templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json")
8766
9703
  });
8767
9704
  if (options.start !== false) {
8768
9705
  const healthy = await repairServerHealth(config, dryRun);
@@ -8812,7 +9749,7 @@ async function runUninstall(config, options) {
8812
9749
  }
8813
9750
  console.log("Uninstalling local Threadnote setup.");
8814
9751
  await runStop(config, { dryRun });
8815
- await removePathIfExists((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
9752
+ await removePathIfExists((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
8816
9753
  await removeLaunchAgent(dryRun);
8817
9754
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
8818
9755
  await removeMcpSnippets(config, dryRun);
@@ -8866,16 +9803,16 @@ async function repairManifest(config, dryRun) {
8866
9803
  console.log(output2.trimEnd());
8867
9804
  return;
8868
9805
  }
8869
- await ensureDirectory((0, import_node_path7.dirname)(config.manifestPath), false);
9806
+ await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
8870
9807
  const currentContent = await readFileIfExists(config.manifestPath);
8871
9808
  if (currentContent !== void 0) {
8872
9809
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
8873
- await (0, import_promises8.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
8874
- await (0, import_promises8.chmod)(backupPath, 384);
9810
+ await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
9811
+ await (0, import_promises9.chmod)(backupPath, 384);
8875
9812
  console.log(`Backup: ${backupPath}`);
8876
9813
  }
8877
- await (0, import_promises8.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
8878
- await (0, import_promises8.chmod)(config.manifestPath, 384);
9814
+ await (0, import_promises9.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
9815
+ await (0, import_promises9.chmod)(config.manifestPath, 384);
8879
9816
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
8880
9817
  }
8881
9818
  async function repairServerHealth(config, dryRun) {
@@ -8916,16 +9853,16 @@ async function runStart(config, options) {
8916
9853
  );
8917
9854
  }
8918
9855
  const logPath = openVikingLogPath(config);
8919
- await ensureDirectory((0, import_node_path7.dirname)(logPath), false);
9856
+ await ensureDirectory((0, import_node_path8.dirname)(logPath), false);
8920
9857
  if (options.foreground === true) {
8921
9858
  const result = await runInteractive(server, args);
8922
9859
  process.exitCode = result;
8923
9860
  return;
8924
9861
  }
8925
- const logFd = (0, import_node_fs4.openSync)(logPath, "a");
9862
+ const logFd = (0, import_node_fs5.openSync)(logPath, "a");
8926
9863
  const child = spawnDetachedServerWithLog(server, args, logFd);
8927
9864
  child.unref();
8928
- await (0, import_promises8.writeFile)((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
9865
+ await (0, import_promises9.writeFile)((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
8929
9866
  `, "utf8");
8930
9867
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
8931
9868
  if (health) {
@@ -8940,7 +9877,7 @@ function spawnDetachedServerWithLog(server, args, logFd) {
8940
9877
  try {
8941
9878
  return spawnDetachedServer(server, args, logFd);
8942
9879
  } finally {
8943
- (0, import_node_fs4.closeSync)(logFd);
9880
+ (0, import_node_fs5.closeSync)(logFd);
8944
9881
  }
8945
9882
  }
8946
9883
  function spawnDetachedServer(server, args, logFd) {
@@ -8951,14 +9888,14 @@ function spawnDetachedServer(server, args, logFd) {
8951
9888
  }
8952
9889
  async function runStop(config, options) {
8953
9890
  const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
8954
- if ((0, import_node_os5.platform)() === "darwin") {
9891
+ if ((0, import_node_os6.platform)() === "darwin") {
8955
9892
  if (options.dryRun === true || await exists(launchAgentPath)) {
8956
9893
  await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
8957
9894
  } else {
8958
9895
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
8959
9896
  }
8960
9897
  }
8961
- const pidPath = (0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid");
9898
+ const pidPath = (0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid");
8962
9899
  const pidText = await readFileIfExists(pidPath);
8963
9900
  if (!pidText) {
8964
9901
  console.log("No pid file found for detached OpenViking server.");
@@ -9054,7 +9991,7 @@ async function pythonSystemCertificatesCheck() {
9054
9991
  };
9055
9992
  }
9056
9993
  async function commandShimCheck() {
9057
- const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9994
+ const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9058
9995
  const content = await readFileIfExists(shimPath);
9059
9996
  if (content === void 0) {
9060
9997
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -9076,9 +10013,9 @@ async function commandShimCheck() {
9076
10013
  return { name: "threadnote shim", status: "ok", detail: shimPath };
9077
10014
  }
9078
10015
  async function userAgentInstructionsChecks() {
9079
- const expectedBlock = await renderUserAgentInstructionsBlock();
9080
10016
  return Promise.all(
9081
10017
  USER_AGENT_INSTRUCTION_TARGETS.map(async (target) => {
10018
+ const expectedInstructions = await renderUserAgentInstructions(target);
9082
10019
  const targetPath = expandPath(target.path);
9083
10020
  const content = await readFileIfExists(targetPath);
9084
10021
  if (content === void 0) {
@@ -9092,7 +10029,7 @@ async function userAgentInstructionsChecks() {
9092
10029
  detail: `${targetPath} missing threadnote block; install will add it`
9093
10030
  };
9094
10031
  }
9095
- if (existingBlock !== expectedBlock) {
10032
+ if (target.kind === "file" && content !== expectedInstructions || target.kind === "block" && existingBlock !== expectedInstructions) {
9096
10033
  return {
9097
10034
  name: target.label,
9098
10035
  status: "warn",
@@ -9120,11 +10057,11 @@ async function hasPythonModule(serverPath, moduleName) {
9120
10057
  async function siblingPythonForExecutable(executablePath) {
9121
10058
  let resolvedPath;
9122
10059
  try {
9123
- resolvedPath = await (0, import_promises8.realpath)(executablePath);
10060
+ resolvedPath = await (0, import_promises9.realpath)(executablePath);
9124
10061
  } catch (_err) {
9125
10062
  return void 0;
9126
10063
  }
9127
- const pythonPath = (0, import_node_path7.join)((0, import_node_path7.dirname)(resolvedPath), "python");
10064
+ const pythonPath = (0, import_node_path8.join)((0, import_node_path8.dirname)(resolvedPath), "python");
9128
10065
  return await exists(pythonPath) ? pythonPath : void 0;
9129
10066
  }
9130
10067
  async function manifestCheck(path) {
@@ -9246,38 +10183,38 @@ function printInstallNextSteps(options) {
9246
10183
  }
9247
10184
  async function writeTemplateIfMissing(options) {
9248
10185
  if (await exists(options.destinationPath)) {
9249
- const currentContent = await (0, import_promises8.readFile)(options.destinationPath, "utf8");
10186
+ const currentContent = await (0, import_promises9.readFile)(options.destinationPath, "utf8");
9250
10187
  if (options.shouldRepair?.(currentContent) !== true) {
9251
10188
  console.log(`Already exists: ${options.destinationPath}`);
9252
10189
  return;
9253
10190
  }
9254
- const rendered2 = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
10191
+ const rendered2 = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
9255
10192
  if (options.dryRun) {
9256
10193
  console.log(`Would repair generated config: ${options.destinationPath}`);
9257
10194
  return;
9258
10195
  }
9259
10196
  const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
9260
- await (0, import_promises8.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
9261
- await (0, import_promises8.chmod)(backupPath, 384);
9262
- await (0, import_promises8.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
9263
- await (0, import_promises8.chmod)(options.destinationPath, 384);
10197
+ await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10198
+ await (0, import_promises9.chmod)(backupPath, 384);
10199
+ await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
10200
+ await (0, import_promises9.chmod)(options.destinationPath, 384);
9264
10201
  console.log(`Repaired generated config: ${options.destinationPath}`);
9265
10202
  console.log(`Backup: ${backupPath}`);
9266
10203
  return;
9267
10204
  }
9268
- const rendered = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
10205
+ const rendered = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
9269
10206
  if (options.dryRun) {
9270
10207
  console.log(`Would write ${options.destinationPath}`);
9271
10208
  return;
9272
10209
  }
9273
- await ensureDirectory((0, import_node_path7.dirname)(options.destinationPath), false);
9274
- await (0, import_promises8.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
9275
- await (0, import_promises8.chmod)(options.destinationPath, 384);
10210
+ await ensureDirectory((0, import_node_path8.dirname)(options.destinationPath), false);
10211
+ await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
10212
+ await (0, import_promises9.chmod)(options.destinationPath, 384);
9276
10213
  console.log(`Wrote ${options.destinationPath}`);
9277
10214
  }
9278
10215
  async function installCommandShim(dryRun) {
9279
10216
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
9280
- const shimPath = (0, import_node_path7.join)(binDir, "threadnote");
10217
+ const shimPath = (0, import_node_path8.join)(binDir, "threadnote");
9281
10218
  const existingContent = await readFileIfExists(shimPath);
9282
10219
  if (existingContent && !isManagedCommandShim(existingContent)) {
9283
10220
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -9293,12 +10230,12 @@ async function installCommandShim(dryRun) {
9293
10230
  return;
9294
10231
  }
9295
10232
  await ensureDirectory(binDir, false);
9296
- await (0, import_promises8.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
9297
- await (0, import_promises8.chmod)(shimPath, 493);
10233
+ await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10234
+ await (0, import_promises9.chmod)(shimPath, 493);
9298
10235
  console.log(`Wrote command shim: ${shimPath}`);
9299
10236
  }
9300
10237
  async function removeCommandShim(dryRun) {
9301
- const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10238
+ const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9302
10239
  const content = await readFileIfExists(shimPath);
9303
10240
  if (content === void 0) {
9304
10241
  console.log(`Already absent: ${shimPath}`);
@@ -9311,11 +10248,15 @@ async function removeCommandShim(dryRun) {
9311
10248
  await removePath(shimPath, "command shim", dryRun);
9312
10249
  }
9313
10250
  async function installUserAgentInstructions(dryRun) {
9314
- const block = await renderUserAgentInstructionsBlock();
9315
10251
  for (const target of USER_AGENT_INSTRUCTION_TARGETS) {
10252
+ const instructions = await renderUserAgentInstructions(target);
9316
10253
  const targetPath = expandPath(target.path);
9317
10254
  const currentContent = await readFileIfExists(targetPath);
9318
- const nextContent = upsertManagedBlock(currentContent ?? "", block);
10255
+ if (target.kind === "file" && currentContent !== void 0 && extractManagedBlock(currentContent) === void 0) {
10256
+ console.log(`WARN ${targetPath} is not managed by threadnote; not modifying it`);
10257
+ continue;
10258
+ }
10259
+ const nextContent = target.kind === "file" ? instructions : upsertManagedBlock(currentContent ?? "", instructions);
9319
10260
  if (nextContent === void 0) {
9320
10261
  console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
9321
10262
  continue;
@@ -9328,8 +10269,8 @@ async function installUserAgentInstructions(dryRun) {
9328
10269
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
9329
10270
  continue;
9330
10271
  }
9331
- await ensureDirectory((0, import_node_path7.dirname)(targetPath), false);
9332
- await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10272
+ await ensureDirectory((0, import_node_path8.dirname)(targetPath), false);
10273
+ await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
9333
10274
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
9334
10275
  }
9335
10276
  }
@@ -9341,6 +10282,14 @@ async function removeUserAgentInstructions(dryRun) {
9341
10282
  console.log(`Already absent: ${targetPath}`);
9342
10283
  continue;
9343
10284
  }
10285
+ if (target.kind === "file") {
10286
+ if (extractManagedBlock(currentContent) === void 0) {
10287
+ console.log(`WARN ${targetPath} is not managed by threadnote; not removing it`);
10288
+ continue;
10289
+ }
10290
+ await removePath(targetPath, target.label, dryRun);
10291
+ continue;
10292
+ }
9344
10293
  const nextContent = removeManagedBlock(currentContent);
9345
10294
  if (nextContent === void 0) {
9346
10295
  console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
@@ -9358,12 +10307,27 @@ async function removeUserAgentInstructions(dryRun) {
9358
10307
  console.log(`Would update ${targetPath}`);
9359
10308
  continue;
9360
10309
  }
9361
- await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10310
+ await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
9362
10311
  console.log(`Updated ${targetPath}`);
9363
10312
  }
9364
10313
  }
10314
+ async function renderUserAgentInstructions(target) {
10315
+ const block = await renderUserAgentInstructionsBlock();
10316
+ if (target.kind === "block") {
10317
+ return block;
10318
+ }
10319
+ return [
10320
+ "---",
10321
+ "name: Threadnote",
10322
+ "description: Shared local context and handoffs through Threadnote",
10323
+ 'applyTo: "**"',
10324
+ "---",
10325
+ "",
10326
+ block
10327
+ ].join("\n");
10328
+ }
9365
10329
  async function renderUserAgentInstructionsBlock() {
9366
- const instructions = (await (0, import_promises8.readFile)((0, import_node_path7.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
10330
+ const instructions = (await (0, import_promises9.readFile)((0, import_node_path8.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
9367
10331
  return `${USER_INSTRUCTIONS_START_MARKER}
9368
10332
  ${instructions}
9369
10333
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -9438,12 +10402,12 @@ function isManagedCommandShim(content) {
9438
10402
  return content.includes(SHIM_MARKER);
9439
10403
  }
9440
10404
  async function installLaunchAgent(config, dryRun) {
9441
- if ((0, import_node_os5.platform)() !== "darwin") {
10405
+ if ((0, import_node_os6.platform)() !== "darwin") {
9442
10406
  throw new Error("launchd autostart is only supported on macOS.");
9443
10407
  }
9444
- const source = (0, import_node_path7.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
10408
+ const source = (0, import_node_path8.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
9445
10409
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
9446
- const rendered = renderTemplate(await (0, import_promises8.readFile)(source, "utf8"), config);
10410
+ const rendered = renderTemplate(await (0, import_promises9.readFile)(source, "utf8"), config);
9447
10411
  if (dryRun) {
9448
10412
  console.log(`Would write ${destination}`);
9449
10413
  console.log(`Would run: launchctl unload ${destination}`);
@@ -9451,9 +10415,9 @@ async function installLaunchAgent(config, dryRun) {
9451
10415
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
9452
10416
  return;
9453
10417
  }
9454
- await ensureDirectory((0, import_node_path7.dirname)(destination), false);
9455
- await ensureDirectory((0, import_node_path7.dirname)(openVikingLogPath(config)), false);
9456
- await (0, import_promises8.writeFile)(destination, rendered, "utf8");
10418
+ await ensureDirectory((0, import_node_path8.dirname)(destination), false);
10419
+ await ensureDirectory((0, import_node_path8.dirname)(openVikingLogPath(config)), false);
10420
+ await (0, import_promises9.writeFile)(destination, rendered, "utf8");
9457
10421
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
9458
10422
  await maybeRun(false, "launchctl", ["load", destination]);
9459
10423
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
@@ -9516,7 +10480,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
9516
10480
  if (typeof parsed.default_user !== "string") {
9517
10481
  return false;
9518
10482
  }
9519
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path7.join)(config.agentContextHome, "data")) {
10483
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path8.join)(config.agentContextHome, "data")) {
9520
10484
  return false;
9521
10485
  }
9522
10486
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -9557,7 +10521,7 @@ async function main() {
9557
10521
  });
9558
10522
  program2.command("repair").description("Repair local OpenViking install, config files, server health, shim, manifest, and MCP config").option("--dry-run", "Print the repair actions without making changes").option(
9559
10523
  "--mcp <clients>",
9560
- "MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
10524
+ "MCP clients to repair: available, all, none, codex, claude, cursor, copilot, or comma-separated list",
9561
10525
  "available"
9562
10526
  ).option("--no-start", "Do not start OpenViking if health is failing").option("--no-post-update", "Skip post-update migration prompts after repair").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).action(async (options) => {
9563
10527
  const config = getRuntimeConfig(program2);
@@ -9574,7 +10538,7 @@ async function main() {
9574
10538
  });
9575
10539
  program2.command("uninstall").description("Remove Threadnote setup and optionally erase local memories").option("--dry-run", "Print uninstall actions without making changes").option(
9576
10540
  "--mcp <clients>",
9577
- "MCP clients to remove: available, all, none, codex, claude, cursor, or comma-separated list",
10541
+ "MCP clients to remove: available, all, none, codex, claude, cursor, copilot, or comma-separated list",
9578
10542
  "available"
9579
10543
  ).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
9580
10544
  await runUninstall(getRuntimeConfig(program2), options);
@@ -9588,10 +10552,10 @@ async function main() {
9588
10552
  program2.command("seed-skills").description("Seed Codex, Claude, and repo-local SKILL.md files as a searchable catalog").option("--dry-run", "Print skill files and ov commands without importing").option("--manifest <path>", "Manifest path for repo-local skill discovery").option("--native", "Use native OpenViking skill ingestion; requires a working VLM config").action(async (options) => {
9589
10553
  await runSeedSkills(getRuntimeConfig(program2, options.manifest), options);
9590
10554
  });
9591
- program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, or cursor").option("--apply", "Actually modify the selected agent config").option("--name <name>", "MCP server name", OPENVIKING_MCP_NAME).option("--native-http", "Install OpenViking native HTTP MCP endpoint instead of the local stdio adapter").option("--scope <scope>", "Claude MCP config scope: user, local, or project", parseClaudeMcpScope, "user").option("--url <url>", "OpenViking native HTTP MCP URL").option("--bearer-token-env-var <name>", "Environment variable containing the local API key").action(async (agent, options) => {
10555
+ program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, cursor, or copilot").option("--apply", "Actually modify the selected agent config").option("--name <name>", "MCP server name", OPENVIKING_MCP_NAME).option("--native-http", "Install OpenViking native HTTP MCP endpoint instead of the local stdio adapter").option("--scope <scope>", "Claude MCP config scope: user, local, or project", parseClaudeMcpScope, "user").option("--url <url>", "OpenViking native HTTP MCP URL").option("--bearer-token-env-var <name>", "Environment variable containing the local API key").action(async (agent, options) => {
9592
10556
  await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
9593
10557
  });
9594
- program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind, "durable").option("--project <name>", "Project/repo/topic namespace for lifecycle-aware storage").option("--replace <uri>", "Supersede an existing viking:// memory after the new memory is stored").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--status <status>", "active, archived, or superseded", parseMemoryStatus, "active").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").option("--topic <name>", "Stable topic name; active memories with the same project/topic update one file").action(async (options) => {
10558
+ program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind, "durable").option("--project <name>", "Project/repo/topic namespace for lifecycle-aware storage").option("--replace <uri>", "Supersede an existing viking:// memory after the new memory is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--status <status>", "active, archived, or superseded", parseMemoryStatus, "active").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").option("--topic <name>", "Stable topic name; active memories with the same project/topic update one file").action(async (options) => {
9595
10559
  await runRemember(getRuntimeConfig(program2), options);
9596
10560
  });
9597
10561
  program2.command("migrate-memories").description("Migrate legacy session-only Threadnote memories into durable memory files").option("--all-accounts", "Scan all local OpenViking accounts under THREADNOTE_HOME").option("--dry-run", "Print migration actions without writing memories").option("--limit <count>", "Maximum number of memories to migrate").option(
@@ -9614,7 +10578,7 @@ async function main() {
9614
10578
  program2.command("list").alias("ls").description("List a viking:// directory").argument("[uri]", "viking:// URI to list", "viking://").option("-a, --all", "Show hidden files such as .abstract.md and .overview.md").option("--dry-run", "Print ov command without listing").option("-n, --node-limit <count>", "Maximum number of nodes to list").option("-r, --recursive", "List subdirectories recursively").option("-s, --simple", "Print only paths").action(async (uri, options) => {
9615
10579
  await runList(getRuntimeConfig(program2), uri, options);
9616
10580
  });
9617
- program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
10581
+ program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
9618
10582
  await runHandoff(getRuntimeConfig(program2), options);
9619
10583
  });
9620
10584
  program2.command("archive").description("Move a memory into the archived lifecycle tree, then remove the original after the archive is stored").argument("<uri>", "viking:// memory URI to archive").option("--dry-run", "Print archive content and ov commands without changing anything").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind).option("--project <name>", "Override inferred project/repo namespace").option("--topic <name>", "Override inferred topic").action(async (uri, options) => {
@@ -9623,6 +10587,31 @@ async function main() {
9623
10587
  program2.command("forget").description("Remove a viking:// URI from local OpenViking context").argument("<uri>", "viking:// URI to remove").option("--dry-run", "Print ov command without deleting").action(async (uri, options) => {
9624
10588
  await runForget(getRuntimeConfig(program2), uri, options);
9625
10589
  });
10590
+ const share = program2.command("share").description("Share durable memories with teammates through a git-backed repository");
10591
+ share.command("init").description("Configure a shared memories repo for a team (clones the remote into the local memory tree)").argument("<remote-url>", "git remote URL of the shared memories repo").option("--team <name>", 'Team name; defaults to "default"').option("--set-default", "Mark this team as the default for future share commands").option(
10592
+ "--no-push",
10593
+ "Do not push the auto-generated .gitignore housekeeping commit. Does not affect future publishes."
10594
+ ).option("--dry-run", "Print actions without running them").action(async (remoteUrl, options) => {
10595
+ await runShareInit(getRuntimeConfig(program2), remoteUrl, options);
10596
+ });
10597
+ share.command("status").description("Show git status and ahead/behind counts for a shared team").option("--team <name>", "Team name; defaults to the configured default team").option("--dry-run", "Print git commands without running them").action(async (options) => {
10598
+ await runShareStatus(getRuntimeConfig(program2), options);
10599
+ });
10600
+ share.command("sync").description("Pull, reindex, and push the shared memories repo for a team").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message when auto-committing local edits").option("--no-auto-commit", "Refuse to sync if there are uncommitted local changes").option("--no-push", "Skip the push step after pulling and reindexing").option("--dry-run", "Print actions without running them").action(async (options) => {
10601
+ await runShareSync(getRuntimeConfig(program2), options);
10602
+ });
10603
+ share.command("publish").description("Move a personal memory into the shared team namespace, commit and push").argument("<viking-uri>", "viking:// memory URI to publish").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").action(async (uri, options) => {
10604
+ await runSharePublish(getRuntimeConfig(program2), uri, options);
10605
+ });
10606
+ share.command("unpublish").description("Pull a shared memory back into the personal namespace, commit removal and push").argument("<viking-uri>", "viking:// memory URI inside a team shared subtree").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").action(async (uri, options) => {
10607
+ await runShareUnpublish(getRuntimeConfig(program2), uri, options);
10608
+ });
10609
+ share.command("list").description("List configured shared teams").option("--dry-run", "Print without side effects (no-op for list)").action(async (options) => {
10610
+ await runShareList(getRuntimeConfig(program2), options);
10611
+ });
10612
+ share.command("remove").description("Forget a configured team and optionally delete its worktree/gitdir").option("--team <name>", "Team name; defaults to the configured default team").option("--keep-files", "Keep the worktree and gitdir on disk; only forget the team entry").option("--dry-run", "Print actions without running them").action(async (options) => {
10613
+ await runShareRemove(getRuntimeConfig(program2), options);
10614
+ });
9626
10615
  program2.command("export-pack").description("Export local OpenViking context to an .ovpack").option("--dry-run", "Print ov command without exporting").option("--path <path>", "Output .ovpack path").option("--uri <uri>", "Source viking:// URI to export", "viking://").action(async (options) => {
9627
10616
  await runExportPack(getRuntimeConfig(program2), options);
9628
10617
  });