threadnote 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -89,6 +89,8 @@ The bundled `config/seed-manifest.example.yaml` is only an example. Each develop
89
89
  current checkout.
90
90
  - `start`: starts `openviking-server` on `127.0.0.1:1933`.
91
91
  - `stop`: stops the detached server pid or macOS LaunchAgent.
92
+ - `uninstall`: removes Threadnote shims, MCP config, launchd config, and managed user instructions. Memories are
93
+ preserved by default; pass `--erase-memories` to delete `THREADNOTE_HOME`.
92
94
  - `init-manifest`: creates or updates `~/.openviking/seed-manifest.yaml` from one or more developer repo roots.
93
95
  - `seed`: imports curated repo guidance and docs from the manifest.
94
96
  - `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog. Use
@@ -164,6 +166,29 @@ If a future OpenViking build exposes a healthy native endpoint, install it expli
164
166
  threadnote mcp-install claude --native-http --apply
165
167
  ```
166
168
 
169
+ ## Uninstall
170
+
171
+ Preview removal:
172
+
173
+ ```bash
174
+ threadnote uninstall --dry-run
175
+ ```
176
+
177
+ Remove Threadnote setup while keeping local OpenViking memories:
178
+
179
+ ```bash
180
+ threadnote uninstall
181
+ ```
182
+
183
+ Erase local memories too:
184
+
185
+ ```bash
186
+ threadnote uninstall --erase-memories
187
+ ```
188
+
189
+ `uninstall` removes only Threadnote-managed files and agent config. The npm, Bun, or Deno package remains installed;
190
+ remove that with the package manager you used to install it.
191
+
167
192
  ## Notes
168
193
 
169
194
  OpenViking itself is not vendored here. This prototype installs and controls a local service and should get legal and
@@ -3491,11 +3491,12 @@ var import_promises6 = require("node:fs/promises");
3491
3491
  var import_promises7 = require("node:fs/promises");
3492
3492
  var import_node_path5 = require("node:path");
3493
3493
  var import_node_path6 = require("node:path");
3494
+ var import_promises8 = require("node:fs/promises");
3494
3495
  var import_node_path7 = require("node:path");
3495
3496
  var import_node_child_process = require("node:child_process");
3496
- var import_promises8 = require("node:fs/promises");
3497
- var import_node_os3 = require("node:os");
3498
3497
  var import_promises9 = require("node:fs/promises");
3498
+ var import_node_os3 = require("node:os");
3499
+ var import_promises10 = require("node:fs/promises");
3499
3500
 
3500
3501
  // node_modules/.deno/js-yaml@4.1.1/node_modules/js-yaml/dist/js-yaml.mjs
3501
3502
  function isNothing(subject) {
@@ -6178,6 +6179,13 @@ async function main() {
6178
6179
  program2.command("stop").description("Stop the local OpenViking server or LaunchAgent").option("--dry-run", "Print the stop actions without running them").action(async (options) => {
6179
6180
  await runStop(getRuntimeConfig(program2), options);
6180
6181
  });
6182
+ program2.command("uninstall").description("Remove Threadnote setup and optionally erase local memories").option("--dry-run", "Print uninstall actions without making changes").option(
6183
+ "--mcp <clients>",
6184
+ "MCP clients to remove: available, all, none, codex, claude, or comma-separated list",
6185
+ "available"
6186
+ ).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
6187
+ await runUninstall(getRuntimeConfig(program2), options);
6188
+ });
6181
6189
  program2.command("seed").description("Seed curated context from the manifest; never indexes whole repos by default").option("--dry-run", "Print files and ov commands without importing").option("--manifest <path>", "Manifest path for this seed run").action(async (options) => {
6182
6190
  await runSeed(getRuntimeConfig(program2, options.manifest), options);
6183
6191
  });
@@ -6318,7 +6326,7 @@ async function runRepair(config, options) {
6318
6326
  } else {
6319
6327
  console.log("Skipping server health repair because --no-start was provided.");
6320
6328
  }
6321
- const mcpClients = await resolveRepairMcpClients(options.mcp ?? "available");
6329
+ const mcpClients = await resolveMcpClients(options.mcp ?? "available", "repair");
6322
6330
  if (mcpClients.length === 0) {
6323
6331
  console.log("Skipping MCP config repair.");
6324
6332
  } else {
@@ -6330,6 +6338,28 @@ async function runRepair(config, options) {
6330
6338
  console.log("\nPost-repair doctor:");
6331
6339
  await runDoctor(config, { dryRun, strict: false });
6332
6340
  }
6341
+ async function runUninstall(config, options) {
6342
+ const dryRun = options.dryRun === true;
6343
+ if (options.eraseMemories === true && options.preserveMemories === true) {
6344
+ throw new Error("Use either --erase-memories or --preserve-memories, not both.");
6345
+ }
6346
+ console.log("Uninstalling local Threadnote setup.");
6347
+ await runStop(config, { dryRun });
6348
+ await removePathIfExists((0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
6349
+ await removeLaunchAgent(dryRun);
6350
+ await removeMcpConfigs(options.mcp ?? "available", dryRun);
6351
+ await removeMcpSnippets(config, dryRun);
6352
+ await removeCommandShim(dryRun);
6353
+ await removeUserAgentInstructions(dryRun);
6354
+ if (options.eraseMemories === true) {
6355
+ await eraseThreadnoteHome(config.agentContextHome, dryRun);
6356
+ } else {
6357
+ console.log(`Preserving local memories and OpenViking home: ${config.agentContextHome}`);
6358
+ console.log("Use --erase-memories to delete this directory during uninstall.");
6359
+ }
6360
+ console.log("Uninstall complete.");
6361
+ console.log("The package remains installed. Remove it with your package manager if desired.");
6362
+ }
6333
6363
  async function repairManifest(config, dryRun) {
6334
6364
  try {
6335
6365
  await readSeedManifest(config.manifestPath);
@@ -6373,11 +6403,11 @@ async function repairManifest(config, dryRun) {
6373
6403
  const currentContent = await readFileIfExists(config.manifestPath);
6374
6404
  if (currentContent !== void 0) {
6375
6405
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
6376
- await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
6406
+ await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
6377
6407
  await (0, import_promises2.chmod)(backupPath, 384);
6378
6408
  console.log(`Backup: ${backupPath}`);
6379
6409
  }
6380
- await (0, import_promises9.writeFile)(config.manifestPath, output, { encoding: "utf8", mode: 384 });
6410
+ await (0, import_promises10.writeFile)(config.manifestPath, output, { encoding: "utf8", mode: 384 });
6381
6411
  await (0, import_promises2.chmod)(config.manifestPath, 384);
6382
6412
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
6383
6413
  }
@@ -6425,7 +6455,7 @@ async function runStart(config, options) {
6425
6455
  const logFd = (0, import_node_fs3.openSync)(logPath, "a");
6426
6456
  const child = spawnDetachedServerWithLog(server, args, logFd);
6427
6457
  child.unref();
6428
- await (0, import_promises9.writeFile)((0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
6458
+ await (0, import_promises10.writeFile)((0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
6429
6459
  `, "utf8");
6430
6460
  console.log(`Started OpenViking with pid ${child.pid}. Logs: ${logPath}`);
6431
6461
  }
@@ -6445,7 +6475,11 @@ function spawnDetachedServer(server, args, logFd) {
6445
6475
  async function runStop(config, options) {
6446
6476
  const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
6447
6477
  if ((0, import_node_os2.platform)() === "darwin") {
6448
- await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
6478
+ if (options.dryRun === true || await exists(launchAgentPath)) {
6479
+ await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
6480
+ } else {
6481
+ console.log(`No LaunchAgent found: ${launchAgentPath}`);
6482
+ }
6449
6483
  }
6450
6484
  const pidPath = (0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid");
6451
6485
  const pidText = await readFileIfExists(pidPath);
@@ -6541,7 +6575,7 @@ async function runInitManifest(config, options) {
6541
6575
  return;
6542
6576
  }
6543
6577
  await ensureDirectory((0, import_node_path2.dirname)(manifestPath), false);
6544
- await (0, import_promises9.writeFile)(manifestPath, output, { encoding: "utf8", mode: 384 });
6578
+ await (0, import_promises10.writeFile)(manifestPath, output, { encoding: "utf8", mode: 384 });
6545
6579
  await (0, import_promises2.chmod)(manifestPath, 384);
6546
6580
  console.log(`Wrote manifest: ${manifestPath}`);
6547
6581
  console.log("Seed with:");
@@ -6920,9 +6954,9 @@ async function writeTemplateIfMissing(options) {
6920
6954
  return;
6921
6955
  }
6922
6956
  const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
6923
- await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
6957
+ await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
6924
6958
  await (0, import_promises2.chmod)(backupPath, 384);
6925
- await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
6959
+ await (0, import_promises10.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
6926
6960
  await (0, import_promises2.chmod)(options.destinationPath, 384);
6927
6961
  console.log(`Repaired generated config: ${options.destinationPath}`);
6928
6962
  console.log(`Backup: ${backupPath}`);
@@ -6934,7 +6968,7 @@ async function writeTemplateIfMissing(options) {
6934
6968
  return;
6935
6969
  }
6936
6970
  await ensureDirectory((0, import_node_path2.dirname)(options.destinationPath), false);
6937
- await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
6971
+ await (0, import_promises10.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
6938
6972
  await (0, import_promises2.chmod)(options.destinationPath, 384);
6939
6973
  console.log(`Wrote ${options.destinationPath}`);
6940
6974
  }
@@ -6956,10 +6990,23 @@ async function installCommandShim(dryRun) {
6956
6990
  return;
6957
6991
  }
6958
6992
  await ensureDirectory(binDir, false);
6959
- await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
6993
+ await (0, import_promises10.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
6960
6994
  await (0, import_promises2.chmod)(shimPath, 493);
6961
6995
  console.log(`Wrote command shim: ${shimPath}`);
6962
6996
  }
6997
+ async function removeCommandShim(dryRun) {
6998
+ const shimPath = (0, import_node_path4.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
6999
+ const content = await readFileIfExists(shimPath);
7000
+ if (content === void 0) {
7001
+ console.log(`Already absent: ${shimPath}`);
7002
+ return;
7003
+ }
7004
+ if (!isManagedCommandShim(content)) {
7005
+ console.log(`WARN not removing unmanaged command shim: ${shimPath}`);
7006
+ return;
7007
+ }
7008
+ await removePath(shimPath, "command shim", dryRun);
7009
+ }
6963
7010
  async function installUserAgentInstructions(dryRun) {
6964
7011
  const block = await renderUserAgentInstructionsBlock();
6965
7012
  for (const target of USER_AGENT_INSTRUCTION_TARGETS) {
@@ -6979,10 +7026,39 @@ async function installUserAgentInstructions(dryRun) {
6979
7026
  continue;
6980
7027
  }
6981
7028
  await ensureDirectory((0, import_node_path2.dirname)(targetPath), false);
6982
- await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
7029
+ await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
6983
7030
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
6984
7031
  }
6985
7032
  }
7033
+ async function removeUserAgentInstructions(dryRun) {
7034
+ for (const target of USER_AGENT_INSTRUCTION_TARGETS) {
7035
+ const targetPath = expandPath(target.path);
7036
+ const currentContent = await readFileIfExists(targetPath);
7037
+ if (currentContent === void 0) {
7038
+ console.log(`Already absent: ${targetPath}`);
7039
+ continue;
7040
+ }
7041
+ const nextContent = removeManagedBlock(currentContent);
7042
+ if (nextContent === void 0) {
7043
+ console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
7044
+ continue;
7045
+ }
7046
+ if (nextContent === currentContent) {
7047
+ console.log(`No threadnote block found: ${targetPath}`);
7048
+ continue;
7049
+ }
7050
+ if (nextContent.trim().length === 0) {
7051
+ await removePath(targetPath, target.label, dryRun);
7052
+ continue;
7053
+ }
7054
+ if (dryRun) {
7055
+ console.log(`Would update ${targetPath}`);
7056
+ continue;
7057
+ }
7058
+ await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
7059
+ console.log(`Updated ${targetPath}`);
7060
+ }
7061
+ }
6986
7062
  async function renderUserAgentInstructionsBlock() {
6987
7063
  const instructions = (await (0, import_promises5.readFile)((0, import_node_path4.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
6988
7064
  return `${USER_INSTRUCTIONS_START_MARKER}
@@ -7010,6 +7086,20 @@ function upsertManagedBlock(content, block) {
7010
7086
  }
7011
7087
  return joinMarkdownSections([content.trimEnd(), block]);
7012
7088
  }
7089
+ function removeManagedBlock(content) {
7090
+ const startIndex = content.indexOf(USER_INSTRUCTIONS_START_MARKER);
7091
+ const endIndex = content.indexOf(USER_INSTRUCTIONS_END_MARKER);
7092
+ if (startIndex === -1 !== (endIndex === -1) || endIndex < startIndex) {
7093
+ return void 0;
7094
+ }
7095
+ if (startIndex === -1) {
7096
+ return content;
7097
+ }
7098
+ const before = content.slice(0, startIndex).trimEnd();
7099
+ const after = content.slice(endIndex + USER_INSTRUCTIONS_END_MARKER.length).trimStart();
7100
+ const nextContent = joinMarkdownSections([before, after]);
7101
+ return nextContent.trim().length > 0 ? nextContent : "";
7102
+ }
7013
7103
  function joinMarkdownSections(sections) {
7014
7104
  return `${sections.filter((section) => section.length > 0).join("\n\n")}
7015
7105
  `;
@@ -7059,12 +7149,44 @@ async function installLaunchAgent(config, dryRun) {
7059
7149
  return;
7060
7150
  }
7061
7151
  await ensureDirectory((0, import_node_path2.dirname)(destination), false);
7062
- await (0, import_promises9.writeFile)(destination, rendered, "utf8");
7152
+ await (0, import_promises10.writeFile)(destination, rendered, "utf8");
7063
7153
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
7064
7154
  await maybeRun(false, "launchctl", ["load", destination]);
7065
7155
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
7066
7156
  console.log(`Installed and started ${LAUNCHD_LABEL}`);
7067
7157
  }
7158
+ async function removeLaunchAgent(dryRun) {
7159
+ const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
7160
+ const content = await readFileIfExists(launchAgentPath);
7161
+ if (content === void 0) {
7162
+ console.log(`Already absent: ${launchAgentPath}`);
7163
+ return;
7164
+ }
7165
+ if (!content.includes(LAUNCHD_LABEL) || !content.includes(OPENVIKING_SERVER_COMMAND)) {
7166
+ console.log(`WARN not removing unmanaged LaunchAgent: ${launchAgentPath}`);
7167
+ return;
7168
+ }
7169
+ await removePath(launchAgentPath, "LaunchAgent", dryRun);
7170
+ }
7171
+ async function removeMcpConfigs(value, dryRun) {
7172
+ const clients = await resolveMcpClients(value, "remove");
7173
+ if (clients.length === 0) {
7174
+ console.log("Skipping MCP config removal.");
7175
+ return;
7176
+ }
7177
+ for (const client of clients) {
7178
+ const command = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
7179
+ await maybeRun(dryRun, command.executable, command.args, { allowFailure: true, cwd: command.cwd });
7180
+ }
7181
+ }
7182
+ async function removeMcpSnippets(config, dryRun) {
7183
+ await removePathIfExists((0, import_node_path4.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.codex.toml`), "MCP snippet", dryRun);
7184
+ await removePathIfExists((0, import_node_path4.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.claude.txt`), "MCP snippet", dryRun);
7185
+ }
7186
+ async function eraseThreadnoteHome(path, dryRun) {
7187
+ assertSafeThreadnoteHomeForErase(path);
7188
+ await removePathIfExists(path, "THREADNOTE_HOME and all memories", dryRun);
7189
+ }
7068
7190
  function openVikingServerArgs(config) {
7069
7191
  return ["--config", (0, import_node_path4.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
7070
7192
  }
@@ -7123,7 +7245,7 @@ async function prepareSeedFile(config, candidate, dryRun) {
7123
7245
  return redactedPath;
7124
7246
  }
7125
7247
  await ensureDirectory((0, import_node_path2.dirname)(redactedPath), false);
7126
- await (0, import_promises9.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
7248
+ await (0, import_promises10.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
7127
7249
  await (0, import_promises2.chmod)(redactedPath, 384);
7128
7250
  return redactedPath;
7129
7251
  }
@@ -7263,7 +7385,7 @@ async function storeMemory(config, memory, dryRun) {
7263
7385
  console.log(formatShellCommand(ov, withIdentity(config, ["add-memory", memory])));
7264
7386
  return;
7265
7387
  }
7266
- await (0, import_promises9.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
7388
+ await (0, import_promises10.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
7267
7389
  await (0, import_promises2.chmod)(memoryPath, 384);
7268
7390
  await maybeRun(false, ov, withIdentity(config, ["add-memory", memory]));
7269
7391
  }
@@ -7764,14 +7886,14 @@ async function exists(path) {
7764
7886
  }
7765
7887
  async function isFile(path) {
7766
7888
  try {
7767
- return (await (0, import_promises8.stat)(path)).isFile();
7889
+ return (await (0, import_promises9.stat)(path)).isFile();
7768
7890
  } catch (_err) {
7769
7891
  return false;
7770
7892
  }
7771
7893
  }
7772
7894
  async function isDirectory(path) {
7773
7895
  try {
7774
- return (await (0, import_promises8.stat)(path)).isDirectory();
7896
+ return (await (0, import_promises9.stat)(path)).isDirectory();
7775
7897
  } catch (_err) {
7776
7898
  return false;
7777
7899
  }
@@ -7783,6 +7905,21 @@ async function readFileIfExists(path) {
7783
7905
  return void 0;
7784
7906
  }
7785
7907
  }
7908
+ async function removePathIfExists(path, label, dryRun) {
7909
+ if (!await exists(path)) {
7910
+ console.log(`Already absent: ${path}`);
7911
+ return;
7912
+ }
7913
+ await removePath(path, label, dryRun);
7914
+ }
7915
+ async function removePath(path, label, dryRun) {
7916
+ if (dryRun) {
7917
+ console.log(`Would remove ${label}: ${path}`);
7918
+ return;
7919
+ }
7920
+ await (0, import_promises8.rm)(path, { force: true, recursive: true });
7921
+ console.log(`Removed ${label}: ${path}`);
7922
+ }
7786
7923
  function renderTemplate(template, config) {
7787
7924
  return template.replaceAll("{{THREADNOTE_HOME}}", config.agentContextHome).replaceAll("{{OPENVIKING_ACCOUNT}}", config.account).replaceAll("{{OPENVIKING_AGENT_ID}}", config.agentId).replaceAll("{{OPENVIKING_HOST}}", config.host).replaceAll("{{OPENVIKING_PORT}}", String(config.port)).replaceAll("{{OPENVIKING_USER}}", config.user);
7788
7925
  }
@@ -7818,7 +7955,7 @@ function parseClaudeMcpScope(value) {
7818
7955
  }
7819
7956
  throw new Error(`Invalid Claude MCP scope: ${value}. Expected local, project, or user.`);
7820
7957
  }
7821
- async function resolveRepairMcpClients(value) {
7958
+ async function resolveMcpClients(value, action) {
7822
7959
  const normalized = value.trim().toLowerCase();
7823
7960
  if (normalized === "none" || normalized === "false" || normalized === "off") {
7824
7961
  return [];
@@ -7832,7 +7969,7 @@ async function resolveRepairMcpClients(value) {
7832
7969
  const clients = [];
7833
7970
  for (const client of requested) {
7834
7971
  if (!await findExecutable([client])) {
7835
- console.log(`WARN ${client} command not found; cannot repair ${client} MCP config.`);
7972
+ console.log(`WARN ${client} command not found; cannot ${action} ${client} MCP config.`);
7836
7973
  continue;
7837
7974
  }
7838
7975
  if (!clients.includes(client)) {
@@ -7858,6 +7995,12 @@ function expandPath(path) {
7858
7995
  }
7859
7996
  return (0, import_node_path3.isAbsolute)(path) ? path : (0, import_node_path6.resolve)(getInvocationCwd(), path);
7860
7997
  }
7998
+ function assertSafeThreadnoteHomeForErase(path) {
7999
+ const resolvedPath = (0, import_node_path6.resolve)(path);
8000
+ if (resolvedPath === "/" || resolvedPath === (0, import_node_os.homedir)() || resolvedPath === (0, import_node_path2.dirname)((0, import_node_os.homedir)())) {
8001
+ throw new Error(`Refusing to erase unsafe THREADNOTE_HOME: ${resolvedPath}`);
8002
+ }
8003
+ }
7861
8004
  function portablePath(path) {
7862
8005
  const home = (0, import_node_os.homedir)();
7863
8006
  const resolvedPath = (0, import_node_path6.resolve)(path);
package/docs/rollout.md CHANGED
@@ -21,4 +21,4 @@ Start with a local-only pilot.
21
21
  - Codex and Claude can both store and recall a shared handoff.
22
22
  - Seeding curated guidance does not import known secret patterns.
23
23
  - Fresh agents can recall repo testing guidance and discover relevant skills.
24
- - Removing OpenViking leaves agent configs in a known state.
24
+ - `uninstall --dry-run` previews removal, and `uninstall` leaves memories intact unless `--erase-memories` is explicit.
package/docs/security.md CHANGED
@@ -17,6 +17,7 @@
17
17
  - `mcp-install` requires `--apply` before it changes Codex or Claude config.
18
18
  - `install` updates user-level Codex and Claude instruction files through a managed Markdown block. Existing personal
19
19
  instructions outside that block are preserved.
20
+ - `uninstall` preserves local memories by default. `--erase-memories` is required before deleting `THREADNOTE_HOME`.
20
21
  - Config files created under `THREADNOTE_HOME` are written with user-only permissions.
21
22
 
22
23
  ## Rollout Requirements
@@ -105,3 +105,15 @@ This is expected. Run with `--apply` after reviewing the command:
105
105
  ```bash
106
106
  threadnote mcp-install codex --apply
107
107
  ```
108
+
109
+ ## Uninstall Without Losing Memories
110
+
111
+ Run:
112
+
113
+ ```bash
114
+ threadnote uninstall --dry-run
115
+ threadnote uninstall
116
+ ```
117
+
118
+ By default, uninstall removes Threadnote-managed shims, MCP config, launchd config, and user instruction blocks while
119
+ preserving `THREADNOTE_HOME`. To delete local OpenViking data too, pass `--erase-memories`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",