threadnote 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,8 +4,24 @@
4
4
  It is intentionally scoped to curated docs, memories, skills, and handoffs. It is not a source-navigation replacement,
5
5
  and it does not index whole repositories by default.
6
6
 
7
+ ## Real-World Uses
8
+
9
+ Want to continue work in a fresh agent session? `threadnote install` adds user-level Codex and Claude instructions so
10
+ new agents automatically recall recent handoffs and relevant memories before they start changing code.
11
+
12
+ Implemented a feature a while ago and need to pick it up again? Ask the agent to recall the feature, branch, or repo.
13
+ Threadnote returns auditable `viking://` pointers that the agent can read before deciding what still matters.
14
+
15
+ Switching between Codex and Claude? Install the MCP adapter for both. The user-level instructions tell agents to store a
16
+ handoff before they pause, so the next agent can search the same local memory layer instead of reconstructing context
17
+ from chat history.
18
+
19
+ Found a durable workflow fact, like how a repo runs tests or where release notes live? Ask the agent to remember it.
20
+ Threadnote keeps that memory local and searchable without editing unrelated repo files.
21
+
7
22
  ## Safety Model
8
23
 
24
+ - Machine writes stay **locally** under `THREADNOTE_HOME`, which defaults to `~/.openviking`.
9
25
  - Curated manifests only: seed commands import only paths listed in `config/seed-manifest.example.yaml` or an explicit
10
26
  per-developer manifest.
11
27
  - Ignore rules: `.threadnoteignore` excludes build output, binary artifacts, local auth files, env files, and logs.
@@ -15,7 +31,6 @@ and it does not index whole repositories by default.
15
31
  - User instructions: `install` upserts a managed Threadnote block in `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md`
16
32
  without replacing existing personal instructions.
17
33
  - Agent config changes are explicit: `mcp-install` prints commands and snippets by default; use `--apply` to run them.
18
- - Machine writes stay under `THREADNOTE_HOME`, which defaults to `~/.openviking`.
19
34
 
20
35
  ## Install
21
36
 
@@ -28,6 +43,15 @@ curl -fsSL https://raw.githubusercontent.com/Kashkovsky/threadnote/main/scripts/
28
43
  This installs the published package from npmjs and runs `threadnote install`. It does not use npm `postinstall`,
29
44
  because setup writes local machine config and should be an explicit action.
30
45
 
46
+ Start the local server and confirm it is healthy:
47
+
48
+ ```bash
49
+ threadnote start
50
+ threadnote doctor --dry-run
51
+ ```
52
+
53
+ If install or health checks fail, see `docs/troubleshooting.md`.
54
+
31
55
  To force a runtime:
32
56
 
33
57
  ```bash
@@ -40,45 +64,82 @@ Or install manually:
40
64
  ```bash
41
65
  npm install --global threadnote
42
66
  threadnote install
67
+ threadnote start
68
+ threadnote doctor --dry-run
43
69
  ```
44
70
 
45
- For a one-off check before installing globally:
71
+ ### MCP
72
+
73
+ Make the agents you use aware of Threadnote. Use only the MCP install lines for agents you actually use. Open a fresh Codex or Claude session after installing MCP so the new server registration is loaded.
74
+
75
+ Dry-run examples:
46
76
 
47
77
  ```bash
48
- npm exec --yes threadnote@latest -- doctor --dry-run
49
- bunx threadnote@latest doctor --dry-run
50
- deno run --allow-read --allow-env --allow-run --allow-net npm:threadnote@latest doctor --dry-run
78
+ threadnote mcp-install codex
79
+ threadnote mcp-install claude
51
80
  ```
52
81
 
53
- Avoid using `npm exec` for `threadnote install`; durable shims and MCP launchers should point at a stable global installation, not npm's temporary package cache.
82
+ Apply after review:
54
83
 
55
- ## Source Checkout
84
+ ```bash
85
+ threadnote mcp-install codex --apply
86
+ threadnote mcp-install claude --apply
87
+ ```
56
88
 
57
- For local development from this repo:
89
+ Claude installs at `user` scope by default so the same OpenViking MCP server is available from any repo or worktree.
90
+ Use `--scope local` or `--scope project` only when you intentionally want repo-scoped Claude MCP config.
91
+
92
+ If the package or checkout that originally installed `threadnote` has moved, run repair:
58
93
 
59
94
  ```bash
60
- npm install
61
- npm run build
62
- npm run doctor -- --dry-run
63
- npm run threadnote -- install
95
+ threadnote repair
64
96
  ```
65
97
 
66
- `install` writes a small command shim to `~/.local/bin/threadnote` by default and upserts user-level agent guidance in
67
- `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md`. After that, use the short command from any repo or working directory:
98
+ This rewrites the `threadnote` shim and reinstalls the stdio MCP adapter for available agents so launcher paths point
99
+ at the current checkout.
100
+
101
+ The default install uses the bundled stdio MCP adapter, because OpenViking `0.3.12` does not expose the native `/mcp`
102
+ HTTP route:
103
+
104
+ ```bash
105
+ codex mcp add threadnote -- threadnote-mcp-server
106
+ claude mcp add threadnote -- threadnote-mcp-server
107
+ ```
108
+
109
+ If a future OpenViking build exposes a healthy native endpoint, install it explicitly:
110
+
111
+ ```bash
112
+ threadnote mcp-install claude --native-http --apply
113
+ ```
114
+
115
+ ### Seed Local Repos
116
+
117
+ Memories and handoffs work before seeding. Seed local repos when you want agents to recall repo guidance, docs, and
118
+ skills without reopening the same files by hand.
119
+
120
+ Create or update the local manifest with the repos you care about:
68
121
 
69
122
  ```bash
70
- threadnote doctor --dry-run
71
123
  threadnote init-manifest --repo ~/src/my-service --repo ~/work/mobile-app
72
- threadnote start
124
+ ```
125
+
126
+ Review what will be imported, then seed curated repo guidance:
127
+
128
+ ```bash
73
129
  threadnote seed --dry-run
130
+ threadnote seed
131
+ ```
132
+
133
+ Optionally seed shared and repo-local skills:
134
+
135
+ ```bash
74
136
  threadnote seed-skills --dry-run
137
+ threadnote seed-skills
75
138
  ```
76
139
 
77
- If `~/.local/bin` is not on your `PATH`, either add it or set `THREADNOTE_BIN_DIR` before running `install`.
78
- After reviewing dry-run output, remove `--dry-run` for the operation you want to perform.
140
+ When you add another local repo later, rerun `init-manifest --repo <path>` and seed again.
79
141
 
80
- The bundled `config/seed-manifest.example.yaml` is only an example. Each developer should create a local manifest at
81
- `~/.openviking/seed-manifest.yaml` with `threadnote init-manifest`; repo paths can be anywhere.
142
+ This is it! Start working with your agents as usual. The agent will automatically recall relevant memories and store the new ones. If you want to force it to recall/handoff something, just ask explicitly.
82
143
 
83
144
  ## Commands
84
145
 
@@ -89,6 +150,8 @@ The bundled `config/seed-manifest.example.yaml` is only an example. Each develop
89
150
  current checkout.
90
151
  - `start`: starts `openviking-server` on `127.0.0.1:1933`.
91
152
  - `stop`: stops the detached server pid or macOS LaunchAgent.
153
+ - `uninstall`: removes Threadnote shims, MCP config, launchd config, and managed user instructions. Memories are
154
+ preserved by default; pass `--erase-memories` to delete `THREADNOTE_HOME`.
92
155
  - `init-manifest`: creates or updates `~/.openviking/seed-manifest.yaml` from one or more developer repo roots.
93
156
  - `seed`: imports curated repo guidance and docs from the manifest.
94
157
  - `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog. Use
@@ -103,6 +166,34 @@ The bundled `config/seed-manifest.example.yaml` is only an example. Each develop
103
166
  - `forget`: removes a `viking://` URI.
104
167
  - `export-pack` / `import-pack`: moves local context through `.ovpack` files.
105
168
 
169
+ ## Source Checkout
170
+
171
+ For local development from this repo:
172
+
173
+ ```bash
174
+ npm install
175
+ npm run build
176
+ npm run doctor -- --dry-run
177
+ npm run threadnote -- install
178
+ ```
179
+
180
+ `install` writes a small command shim to `~/.local/bin/threadnote` by default and upserts user-level agent guidance in
181
+ `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md`. After that, use the short command from any repo or working directory:
182
+
183
+ ```bash
184
+ threadnote doctor --dry-run
185
+ threadnote init-manifest --repo ~/src/my-service --repo ~/work/mobile-app
186
+ threadnote start
187
+ threadnote seed --dry-run
188
+ threadnote seed-skills --dry-run
189
+ ```
190
+
191
+ If `~/.local/bin` is not on your `PATH`, either add it or set `THREADNOTE_BIN_DIR` before running `install`.
192
+ After reviewing dry-run output, remove `--dry-run` for the operation you want to perform.
193
+
194
+ The bundled `config/seed-manifest.example.yaml` is only an example. Each developer should create a local manifest at
195
+ `~/.openviking/seed-manifest.yaml` with `threadnote init-manifest`; repo paths can be anywhere.
196
+
106
197
  ## Configuration
107
198
 
108
199
  Environment variables:
@@ -122,65 +213,7 @@ Local projects using `localhost:80` or `localhost:443` do not conflict with Open
122
213
  only occurs when another process already owns the same host and port. If that happens, choose a different
123
214
  `THREADNOTE_PORT`.
124
215
 
125
- ## MCP
126
-
127
- Dry-run examples:
128
-
129
- ```bash
130
- threadnote mcp-install codex
131
- threadnote mcp-install claude
132
- ```
133
-
134
- Apply after review:
135
-
136
- ```bash
137
- threadnote mcp-install codex --apply
138
- threadnote mcp-install claude --apply
139
- ```
140
-
141
- Claude installs at `user` scope by default so the same OpenViking MCP server is available from any repo or worktree.
142
- Use `--scope local` or `--scope project` only when you intentionally want repo-scoped Claude MCP config.
143
-
144
- If the package or checkout that originally installed `threadnote` has moved, run repair:
145
-
146
- ```bash
147
- threadnote repair
148
- ```
149
-
150
- This rewrites the `threadnote` shim and reinstalls the stdio MCP adapter for available agents so launcher paths point
151
- at the current checkout.
152
-
153
- The default install uses the bundled stdio MCP adapter, because OpenViking `0.3.12` does not expose the native `/mcp`
154
- HTTP route:
155
-
156
- ```bash
157
- codex mcp add threadnote -- threadnote-mcp-server
158
- claude mcp add threadnote -- threadnote-mcp-server
159
- ```
160
-
161
- If a future OpenViking build exposes a healthy native endpoint, install it explicitly:
162
-
163
- ```bash
164
- threadnote mcp-install claude --native-http --apply
165
- ```
166
-
167
- ## Notes
168
-
169
- OpenViking itself is not vendored here. This prototype installs and controls a local service and should get legal and
170
- security review before broader rollout.
171
-
172
- ## Publishing
173
-
174
- The npm package ships bundled CommonJS `.cjs` entrypoints in `dist/`; `tsx` is only used by `npm run dev` for
175
- source-checkout development. There is no published TypeScript runner and no runtime npm dependency tree. The generated
176
- user shim tries Node, Bun, then Deno.
177
-
178
- ```bash
179
- npm run typecheck
180
- npm run build
181
- npm pack --dry-run
182
- npm publish
183
- ```
216
+ ## Misc
184
217
 
185
218
  See `docs/migration.md` for switching an existing repo workflow to `threadnote` without deleting canonical
186
219
  `AGENTS.md`, `CLAUDE.md`, `.claude/`, or `.agents/` files.
@@ -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) {
@@ -6130,8 +6131,12 @@ var DEFAULT_AGENT_ID = "threadnote";
6130
6131
  var OPENVIKING_PACKAGE_NAME = "openviking[local-embed]";
6131
6132
  var OPENVIKING_SERVER_COMMAND = "openviking-server";
6132
6133
  var OPENVIKING_MCP_NAME = "threadnote";
6134
+ var PYTHON_SYSTEM_CERTS_MODULE = "pip_system_certs";
6135
+ var PYTHON_SYSTEM_CERTS_PACKAGE = "pip-system-certs";
6133
6136
  var LAUNCHD_LABEL = "io.threadnote.openviking";
6134
6137
  var MAX_SECRET_MATCHES_TO_PRINT = 5;
6138
+ var START_HEALTH_POLL_INTERVAL_MS = 500;
6139
+ var START_HEALTH_TIMEOUT_MS = 15e3;
6135
6140
  var SHIM_MARKER = "Generated by threadnote";
6136
6141
  var USER_INSTRUCTIONS_START_MARKER = "<!-- BEGIN THREADNOTE USER INSTRUCTIONS -->";
6137
6142
  var USER_INSTRUCTIONS_END_MARKER = "<!-- END THREADNOTE USER INSTRUCTIONS -->";
@@ -6178,6 +6183,13 @@ async function main() {
6178
6183
  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
6184
  await runStop(getRuntimeConfig(program2), options);
6180
6185
  });
6186
+ program2.command("uninstall").description("Remove Threadnote setup and optionally erase local memories").option("--dry-run", "Print uninstall actions without making changes").option(
6187
+ "--mcp <clients>",
6188
+ "MCP clients to remove: available, all, none, codex, claude, or comma-separated list",
6189
+ "available"
6190
+ ).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
6191
+ await runUninstall(getRuntimeConfig(program2), options);
6192
+ });
6181
6193
  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
6194
  await runSeed(getRuntimeConfig(program2, options.manifest), options);
6183
6195
  });
@@ -6249,6 +6261,7 @@ async function runDoctor(config, options) {
6249
6261
  checks.push(await commandCheck("openviking-server", ["--help"]));
6250
6262
  checks.push(await firstCommandCheck("openviking cli", ["ov", "openviking"], ["--help"]));
6251
6263
  checks.push(await localEmbeddingCheck());
6264
+ checks.push(await pythonSystemCertificatesCheck());
6252
6265
  checks.push(await firstCommandCheck("python installer", ["pipx", "uv", "pip3"], ["--version"]));
6253
6266
  checks.push(await commandPresenceCheck("codex", ["--version"]));
6254
6267
  checks.push(await commandPresenceCheck("claude", ["--version"]));
@@ -6281,14 +6294,19 @@ async function runInstall(config, options) {
6281
6294
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
6282
6295
  if (serverPath) {
6283
6296
  console.log(`OpenViking server already installed: ${serverPath}`);
6297
+ const repairReasons = [];
6284
6298
  if (await hasLocalEmbeddingDependency(serverPath) === false) {
6285
- console.log("OpenViking local embedding extra is missing; repairing install.");
6286
- const installCommand = await getInstallCommand(config, options.packageManager, true);
6287
- await maybeRun(options.dryRun === true, installCommand.executable, installCommand.args);
6299
+ repairReasons.push("local embedding extra is missing");
6300
+ }
6301
+ if (await hasPythonSystemCertificatesPatch(serverPath) === false) {
6302
+ repairReasons.push("Python system certificate bridge is missing");
6303
+ }
6304
+ if (repairReasons.length > 0) {
6305
+ console.log(`OpenViking install needs repair: ${repairReasons.join("; ")}.`);
6306
+ await runInstallCommands(config, options.packageManager, true, options.dryRun === true);
6288
6307
  }
6289
6308
  } else {
6290
- const installCommand = await getInstallCommand(config, options.packageManager, false);
6291
- await maybeRun(options.dryRun === true, installCommand.executable, installCommand.args);
6309
+ await runInstallCommands(config, options.packageManager, false, options.dryRun === true);
6292
6310
  }
6293
6311
  await writeTemplateIfMissing({
6294
6312
  config,
@@ -6304,9 +6322,9 @@ async function runInstall(config, options) {
6304
6322
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
6305
6323
  templatePath: (0, import_node_path4.join)(toolRoot(), "config", "ovcli.conf.template.json")
6306
6324
  });
6307
- console.log("Install complete. Run doctor, then start:");
6308
- console.log(" threadnote doctor");
6325
+ console.log("Install complete. Run start, then doctor:");
6309
6326
  console.log(" threadnote start");
6327
+ console.log(" threadnote doctor");
6310
6328
  }
6311
6329
  async function runRepair(config, options) {
6312
6330
  const dryRun = options.dryRun === true;
@@ -6318,7 +6336,7 @@ async function runRepair(config, options) {
6318
6336
  } else {
6319
6337
  console.log("Skipping server health repair because --no-start was provided.");
6320
6338
  }
6321
- const mcpClients = await resolveRepairMcpClients(options.mcp ?? "available");
6339
+ const mcpClients = await resolveMcpClients(options.mcp ?? "available", "repair");
6322
6340
  if (mcpClients.length === 0) {
6323
6341
  console.log("Skipping MCP config repair.");
6324
6342
  } else {
@@ -6330,6 +6348,28 @@ async function runRepair(config, options) {
6330
6348
  console.log("\nPost-repair doctor:");
6331
6349
  await runDoctor(config, { dryRun, strict: false });
6332
6350
  }
6351
+ async function runUninstall(config, options) {
6352
+ const dryRun = options.dryRun === true;
6353
+ if (options.eraseMemories === true && options.preserveMemories === true) {
6354
+ throw new Error("Use either --erase-memories or --preserve-memories, not both.");
6355
+ }
6356
+ console.log("Uninstalling local Threadnote setup.");
6357
+ await runStop(config, { dryRun });
6358
+ await removePathIfExists((0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
6359
+ await removeLaunchAgent(dryRun);
6360
+ await removeMcpConfigs(options.mcp ?? "available", dryRun);
6361
+ await removeMcpSnippets(config, dryRun);
6362
+ await removeCommandShim(dryRun);
6363
+ await removeUserAgentInstructions(dryRun);
6364
+ if (options.eraseMemories === true) {
6365
+ await eraseThreadnoteHome(config.agentContextHome, dryRun);
6366
+ } else {
6367
+ console.log(`Preserving local memories and OpenViking home: ${config.agentContextHome}`);
6368
+ console.log("Use --erase-memories to delete this directory during uninstall.");
6369
+ }
6370
+ console.log("Uninstall complete.");
6371
+ console.log("The package remains installed. Remove it with your package manager if desired.");
6372
+ }
6333
6373
  async function repairManifest(config, dryRun) {
6334
6374
  try {
6335
6375
  await readSeedManifest(config.manifestPath);
@@ -6373,11 +6413,11 @@ async function repairManifest(config, dryRun) {
6373
6413
  const currentContent = await readFileIfExists(config.manifestPath);
6374
6414
  if (currentContent !== void 0) {
6375
6415
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
6376
- await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
6416
+ await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
6377
6417
  await (0, import_promises2.chmod)(backupPath, 384);
6378
6418
  console.log(`Backup: ${backupPath}`);
6379
6419
  }
6380
- await (0, import_promises9.writeFile)(config.manifestPath, output, { encoding: "utf8", mode: 384 });
6420
+ await (0, import_promises10.writeFile)(config.manifestPath, output, { encoding: "utf8", mode: 384 });
6381
6421
  await (0, import_promises2.chmod)(config.manifestPath, 384);
6382
6422
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
6383
6423
  }
@@ -6406,8 +6446,9 @@ async function runStart(config, options) {
6406
6446
  return;
6407
6447
  }
6408
6448
  const existingHealth = await readOpenVikingHealthIfAvailable(config, 500);
6449
+ const healthUrl = openVikingHealthUrl(config);
6409
6450
  if (existingHealth) {
6410
- console.log(`OpenViking is already healthy at http://${config.host}:${config.port}/health`);
6451
+ console.log(`OpenViking is already healthy at ${healthUrl}`);
6411
6452
  return;
6412
6453
  }
6413
6454
  if (await isTcpPortOpen(config.host, config.port, 500)) {
@@ -6415,19 +6456,26 @@ async function runStart(config, options) {
6415
6456
  `Port ${config.host}:${config.port} is already in use, but it is not a healthy OpenViking server. Set THREADNOTE_PORT or pass --port to use a different port.`
6416
6457
  );
6417
6458
  }
6418
- await ensureDirectory((0, import_node_path4.join)(config.agentContextHome, "logs"), false);
6459
+ const logPath = openVikingLogPath(config);
6460
+ await ensureDirectory((0, import_node_path2.dirname)(logPath), false);
6419
6461
  if (options.foreground === true) {
6420
6462
  const result = await runInteractive(server, args);
6421
6463
  process.exitCode = result;
6422
6464
  return;
6423
6465
  }
6424
- const logPath = (0, import_node_path4.join)(config.agentContextHome, "logs", "server.log");
6425
6466
  const logFd = (0, import_node_fs3.openSync)(logPath, "a");
6426
6467
  const child = spawnDetachedServerWithLog(server, args, logFd);
6427
6468
  child.unref();
6428
- await (0, import_promises9.writeFile)((0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
6469
+ await (0, import_promises10.writeFile)((0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
6429
6470
  `, "utf8");
6430
- console.log(`Started OpenViking with pid ${child.pid}. Logs: ${logPath}`);
6471
+ const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
6472
+ if (health) {
6473
+ console.log(`Started OpenViking with pid ${child.pid}. Health OK at ${healthUrl}. Logs: ${logPath}`);
6474
+ return;
6475
+ }
6476
+ throw new Error(
6477
+ `Started OpenViking with pid ${child.pid}, but ${healthUrl} did not become healthy within ${START_HEALTH_TIMEOUT_MS / 1e3}s. Logs: ${logPath}`
6478
+ );
6431
6479
  }
6432
6480
  function spawnDetachedServerWithLog(server, args, logFd) {
6433
6481
  try {
@@ -6445,7 +6493,11 @@ function spawnDetachedServer(server, args, logFd) {
6445
6493
  async function runStop(config, options) {
6446
6494
  const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
6447
6495
  if ((0, import_node_os2.platform)() === "darwin") {
6448
- await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
6496
+ if (options.dryRun === true || await exists(launchAgentPath)) {
6497
+ await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
6498
+ } else {
6499
+ console.log(`No LaunchAgent found: ${launchAgentPath}`);
6500
+ }
6449
6501
  }
6450
6502
  const pidPath = (0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid");
6451
6503
  const pidText = await readFileIfExists(pidPath);
@@ -6541,7 +6593,7 @@ async function runInitManifest(config, options) {
6541
6593
  return;
6542
6594
  }
6543
6595
  await ensureDirectory((0, import_node_path2.dirname)(manifestPath), false);
6544
- await (0, import_promises9.writeFile)(manifestPath, output, { encoding: "utf8", mode: 384 });
6596
+ await (0, import_promises10.writeFile)(manifestPath, output, { encoding: "utf8", mode: 384 });
6545
6597
  await (0, import_promises2.chmod)(manifestPath, 384);
6546
6598
  console.log(`Wrote manifest: ${manifestPath}`);
6547
6599
  console.log("Seed with:");
@@ -6790,6 +6842,21 @@ async function localEmbeddingCheck() {
6790
6842
  detail: "llama_cpp missing; install will repair with openviking[local-embed]"
6791
6843
  };
6792
6844
  }
6845
+ async function pythonSystemCertificatesCheck() {
6846
+ const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
6847
+ if (!serverPath) {
6848
+ return { name: "python system certs", status: "warn", detail: "openviking-server missing" };
6849
+ }
6850
+ const hasDependency = await hasPythonSystemCertificatesPatch(serverPath);
6851
+ if (hasDependency === void 0) {
6852
+ return { name: "python system certs", status: "warn", detail: "could not inspect tool Python" };
6853
+ }
6854
+ return hasDependency ? { name: "python system certs", status: "ok", detail: `${PYTHON_SYSTEM_CERTS_MODULE} import works` } : {
6855
+ name: "python system certs",
6856
+ status: "warn",
6857
+ detail: `${PYTHON_SYSTEM_CERTS_MODULE} missing; install will repair corporate TLS support`
6858
+ };
6859
+ }
6793
6860
  async function commandShimCheck() {
6794
6861
  const shimPath = (0, import_node_path4.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
6795
6862
  const content = await readFileIfExists(shimPath);
@@ -6833,11 +6900,17 @@ async function userAgentInstructionsChecks() {
6833
6900
  );
6834
6901
  }
6835
6902
  async function hasLocalEmbeddingDependency(serverPath) {
6903
+ return hasPythonModule(serverPath, "llama_cpp");
6904
+ }
6905
+ async function hasPythonSystemCertificatesPatch(serverPath) {
6906
+ return hasPythonModule(serverPath, PYTHON_SYSTEM_CERTS_MODULE);
6907
+ }
6908
+ async function hasPythonModule(serverPath, moduleName) {
6836
6909
  const pythonPath = await siblingPythonForExecutable(serverPath);
6837
6910
  if (!pythonPath) {
6838
6911
  return void 0;
6839
6912
  }
6840
- const result = await runCommand(pythonPath, ["-c", "import llama_cpp"], { allowFailure: true });
6913
+ const result = await runCommand(pythonPath, ["-c", `import ${moduleName}`], { allowFailure: true });
6841
6914
  return result.exitCode === 0;
6842
6915
  }
6843
6916
  async function siblingPythonForExecutable(executablePath) {
@@ -6870,7 +6943,7 @@ async function healthCheck(config) {
6870
6943
  }
6871
6944
  }
6872
6945
  async function readOpenVikingHealth(config, timeoutMs) {
6873
- return httpGetText(`http://${config.host}:${config.port}/health`, timeoutMs);
6946
+ return httpGetText(openVikingHealthUrl(config), timeoutMs);
6874
6947
  }
6875
6948
  async function readOpenVikingHealthIfAvailable(config, timeoutMs) {
6876
6949
  try {
@@ -6879,24 +6952,62 @@ async function readOpenVikingHealthIfAvailable(config, timeoutMs) {
6879
6952
  return void 0;
6880
6953
  }
6881
6954
  }
6882
- async function getInstallCommand(config, preferred, force) {
6955
+ function openVikingHealthUrl(config) {
6956
+ return `http://${config.host}:${config.port}/health`;
6957
+ }
6958
+ function openVikingLogPath(config) {
6959
+ return (0, import_node_path4.join)(config.agentContextHome, "logs", "server.log");
6960
+ }
6961
+ async function waitForOpenVikingHealth(config, timeoutMs) {
6962
+ const deadline = Date.now() + timeoutMs;
6963
+ while (Date.now() <= deadline) {
6964
+ const requestTimeoutMs = Math.max(100, Math.min(1e3, deadline - Date.now()));
6965
+ const health = await readOpenVikingHealthIfAvailable(config, requestTimeoutMs);
6966
+ if (health) {
6967
+ return health;
6968
+ }
6969
+ const remainingMs = deadline - Date.now();
6970
+ if (remainingMs <= 0) {
6971
+ break;
6972
+ }
6973
+ await sleep(Math.min(START_HEALTH_POLL_INTERVAL_MS, remainingMs));
6974
+ }
6975
+ return void 0;
6976
+ }
6977
+ async function runInstallCommands(config, preferred, force, dryRun) {
6978
+ const installCommands = await getInstallCommands(config, preferred, force);
6979
+ for (const installCommand of installCommands) {
6980
+ await maybeRun(dryRun, installCommand.executable, installCommand.args);
6981
+ }
6982
+ }
6983
+ async function getInstallCommands(config, preferred, force) {
6883
6984
  const packageSpec = `${OPENVIKING_PACKAGE_NAME}==${config.openVikingVersion}`;
6884
6985
  const manager = preferred ?? await detectPackageManager();
6885
6986
  if (manager === "pipx") {
6886
- return { executable: "pipx", args: force ? ["install", "--force", packageSpec] : ["install", packageSpec] };
6987
+ return [
6988
+ { executable: "pipx", args: force ? ["install", "--force", packageSpec] : ["install", packageSpec] },
6989
+ {
6990
+ executable: "pipx",
6991
+ args: force ? ["inject", "--force", "openviking", PYTHON_SYSTEM_CERTS_PACKAGE] : ["inject", "openviking", PYTHON_SYSTEM_CERTS_PACKAGE]
6992
+ }
6993
+ ];
6887
6994
  }
6888
6995
  if (manager === "uv") {
6889
- return {
6890
- executable: "uv",
6891
- args: force ? ["tool", "install", "--force", packageSpec] : ["tool", "install", packageSpec]
6892
- };
6996
+ const uvArgs = ["tool", "install", "--native-tls", "--with", PYTHON_SYSTEM_CERTS_PACKAGE];
6997
+ return [
6998
+ {
6999
+ executable: "uv",
7000
+ args: force ? [...uvArgs, "--force", packageSpec] : [...uvArgs, packageSpec]
7001
+ }
7002
+ ];
6893
7003
  }
6894
7004
  const pipArgs = ["-m", "pip", "install", "--user"];
6895
7005
  if (force) {
6896
7006
  pipArgs.push("--upgrade", "--force-reinstall");
6897
7007
  }
7008
+ pipArgs.push(PYTHON_SYSTEM_CERTS_PACKAGE);
6898
7009
  pipArgs.push(packageSpec);
6899
- return { executable: "python3", args: pipArgs };
7010
+ return [{ executable: "python3", args: pipArgs }];
6900
7011
  }
6901
7012
  async function detectPackageManager() {
6902
7013
  if (await findExecutable(["pipx"])) {
@@ -6920,9 +7031,9 @@ async function writeTemplateIfMissing(options) {
6920
7031
  return;
6921
7032
  }
6922
7033
  const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
6923
- await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
7034
+ await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
6924
7035
  await (0, import_promises2.chmod)(backupPath, 384);
6925
- await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
7036
+ await (0, import_promises10.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
6926
7037
  await (0, import_promises2.chmod)(options.destinationPath, 384);
6927
7038
  console.log(`Repaired generated config: ${options.destinationPath}`);
6928
7039
  console.log(`Backup: ${backupPath}`);
@@ -6934,7 +7045,7 @@ async function writeTemplateIfMissing(options) {
6934
7045
  return;
6935
7046
  }
6936
7047
  await ensureDirectory((0, import_node_path2.dirname)(options.destinationPath), false);
6937
- await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
7048
+ await (0, import_promises10.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
6938
7049
  await (0, import_promises2.chmod)(options.destinationPath, 384);
6939
7050
  console.log(`Wrote ${options.destinationPath}`);
6940
7051
  }
@@ -6956,10 +7067,23 @@ async function installCommandShim(dryRun) {
6956
7067
  return;
6957
7068
  }
6958
7069
  await ensureDirectory(binDir, false);
6959
- await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
7070
+ await (0, import_promises10.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
6960
7071
  await (0, import_promises2.chmod)(shimPath, 493);
6961
7072
  console.log(`Wrote command shim: ${shimPath}`);
6962
7073
  }
7074
+ async function removeCommandShim(dryRun) {
7075
+ const shimPath = (0, import_node_path4.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
7076
+ const content = await readFileIfExists(shimPath);
7077
+ if (content === void 0) {
7078
+ console.log(`Already absent: ${shimPath}`);
7079
+ return;
7080
+ }
7081
+ if (!isManagedCommandShim(content)) {
7082
+ console.log(`WARN not removing unmanaged command shim: ${shimPath}`);
7083
+ return;
7084
+ }
7085
+ await removePath(shimPath, "command shim", dryRun);
7086
+ }
6963
7087
  async function installUserAgentInstructions(dryRun) {
6964
7088
  const block = await renderUserAgentInstructionsBlock();
6965
7089
  for (const target of USER_AGENT_INSTRUCTION_TARGETS) {
@@ -6979,10 +7103,39 @@ async function installUserAgentInstructions(dryRun) {
6979
7103
  continue;
6980
7104
  }
6981
7105
  await ensureDirectory((0, import_node_path2.dirname)(targetPath), false);
6982
- await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
7106
+ await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
6983
7107
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
6984
7108
  }
6985
7109
  }
7110
+ async function removeUserAgentInstructions(dryRun) {
7111
+ for (const target of USER_AGENT_INSTRUCTION_TARGETS) {
7112
+ const targetPath = expandPath(target.path);
7113
+ const currentContent = await readFileIfExists(targetPath);
7114
+ if (currentContent === void 0) {
7115
+ console.log(`Already absent: ${targetPath}`);
7116
+ continue;
7117
+ }
7118
+ const nextContent = removeManagedBlock(currentContent);
7119
+ if (nextContent === void 0) {
7120
+ console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
7121
+ continue;
7122
+ }
7123
+ if (nextContent === currentContent) {
7124
+ console.log(`No threadnote block found: ${targetPath}`);
7125
+ continue;
7126
+ }
7127
+ if (nextContent.trim().length === 0) {
7128
+ await removePath(targetPath, target.label, dryRun);
7129
+ continue;
7130
+ }
7131
+ if (dryRun) {
7132
+ console.log(`Would update ${targetPath}`);
7133
+ continue;
7134
+ }
7135
+ await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
7136
+ console.log(`Updated ${targetPath}`);
7137
+ }
7138
+ }
6986
7139
  async function renderUserAgentInstructionsBlock() {
6987
7140
  const instructions = (await (0, import_promises5.readFile)((0, import_node_path4.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
6988
7141
  return `${USER_INSTRUCTIONS_START_MARKER}
@@ -7010,6 +7163,20 @@ function upsertManagedBlock(content, block) {
7010
7163
  }
7011
7164
  return joinMarkdownSections([content.trimEnd(), block]);
7012
7165
  }
7166
+ function removeManagedBlock(content) {
7167
+ const startIndex = content.indexOf(USER_INSTRUCTIONS_START_MARKER);
7168
+ const endIndex = content.indexOf(USER_INSTRUCTIONS_END_MARKER);
7169
+ if (startIndex === -1 !== (endIndex === -1) || endIndex < startIndex) {
7170
+ return void 0;
7171
+ }
7172
+ if (startIndex === -1) {
7173
+ return content;
7174
+ }
7175
+ const before = content.slice(0, startIndex).trimEnd();
7176
+ const after = content.slice(endIndex + USER_INSTRUCTIONS_END_MARKER.length).trimStart();
7177
+ const nextContent = joinMarkdownSections([before, after]);
7178
+ return nextContent.trim().length > 0 ? nextContent : "";
7179
+ }
7013
7180
  function joinMarkdownSections(sections) {
7014
7181
  return `${sections.filter((section) => section.length > 0).join("\n\n")}
7015
7182
  `;
@@ -7059,11 +7226,52 @@ async function installLaunchAgent(config, dryRun) {
7059
7226
  return;
7060
7227
  }
7061
7228
  await ensureDirectory((0, import_node_path2.dirname)(destination), false);
7062
- await (0, import_promises9.writeFile)(destination, rendered, "utf8");
7229
+ await ensureDirectory((0, import_node_path2.dirname)(openVikingLogPath(config)), false);
7230
+ await (0, import_promises10.writeFile)(destination, rendered, "utf8");
7063
7231
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
7064
7232
  await maybeRun(false, "launchctl", ["load", destination]);
7065
7233
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
7066
- console.log(`Installed and started ${LAUNCHD_LABEL}`);
7234
+ const healthUrl = openVikingHealthUrl(config);
7235
+ const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
7236
+ if (health) {
7237
+ console.log(`Installed and started ${LAUNCHD_LABEL}. Health OK at ${healthUrl}`);
7238
+ return;
7239
+ }
7240
+ throw new Error(
7241
+ `Installed and started ${LAUNCHD_LABEL}, but ${healthUrl} did not become healthy within ${START_HEALTH_TIMEOUT_MS / 1e3}s. Logs: ${openVikingLogPath(config)}`
7242
+ );
7243
+ }
7244
+ async function removeLaunchAgent(dryRun) {
7245
+ const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
7246
+ const content = await readFileIfExists(launchAgentPath);
7247
+ if (content === void 0) {
7248
+ console.log(`Already absent: ${launchAgentPath}`);
7249
+ return;
7250
+ }
7251
+ if (!content.includes(LAUNCHD_LABEL) || !content.includes(OPENVIKING_SERVER_COMMAND)) {
7252
+ console.log(`WARN not removing unmanaged LaunchAgent: ${launchAgentPath}`);
7253
+ return;
7254
+ }
7255
+ await removePath(launchAgentPath, "LaunchAgent", dryRun);
7256
+ }
7257
+ async function removeMcpConfigs(value, dryRun) {
7258
+ const clients = await resolveMcpClients(value, "remove");
7259
+ if (clients.length === 0) {
7260
+ console.log("Skipping MCP config removal.");
7261
+ return;
7262
+ }
7263
+ for (const client of clients) {
7264
+ const command = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
7265
+ await maybeRun(dryRun, command.executable, command.args, { allowFailure: true, cwd: command.cwd });
7266
+ }
7267
+ }
7268
+ async function removeMcpSnippets(config, dryRun) {
7269
+ await removePathIfExists((0, import_node_path4.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.codex.toml`), "MCP snippet", dryRun);
7270
+ await removePathIfExists((0, import_node_path4.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.claude.txt`), "MCP snippet", dryRun);
7271
+ }
7272
+ async function eraseThreadnoteHome(path, dryRun) {
7273
+ assertSafeThreadnoteHomeForErase(path);
7274
+ await removePathIfExists(path, "THREADNOTE_HOME and all memories", dryRun);
7067
7275
  }
7068
7276
  function openVikingServerArgs(config) {
7069
7277
  return ["--config", (0, import_node_path4.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
@@ -7123,7 +7331,7 @@ async function prepareSeedFile(config, candidate, dryRun) {
7123
7331
  return redactedPath;
7124
7332
  }
7125
7333
  await ensureDirectory((0, import_node_path2.dirname)(redactedPath), false);
7126
- await (0, import_promises9.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
7334
+ await (0, import_promises10.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
7127
7335
  await (0, import_promises2.chmod)(redactedPath, 384);
7128
7336
  return redactedPath;
7129
7337
  }
@@ -7188,7 +7396,7 @@ function buildMcpInstallCommand(config, agent, name, options) {
7188
7396
  if (agent === "codex") {
7189
7397
  return {
7190
7398
  executable: "codex",
7191
- args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", ...command]
7399
+ args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", "/usr/bin/env", ...command]
7192
7400
  };
7193
7401
  }
7194
7402
  return {
@@ -7263,7 +7471,7 @@ async function storeMemory(config, memory, dryRun) {
7263
7471
  console.log(formatShellCommand(ov, withIdentity(config, ["add-memory", memory])));
7264
7472
  return;
7265
7473
  }
7266
- await (0, import_promises9.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
7474
+ await (0, import_promises10.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
7267
7475
  await (0, import_promises2.chmod)(memoryPath, 384);
7268
7476
  await maybeRun(false, ov, withIdentity(config, ["add-memory", memory]));
7269
7477
  }
@@ -7691,6 +7899,11 @@ async function httpGetText(url, timeoutMs) {
7691
7899
  request.on("error", rejectPromise);
7692
7900
  });
7693
7901
  }
7902
+ async function sleep(ms) {
7903
+ return new Promise((resolvePromise) => {
7904
+ setTimeout(resolvePromise, ms);
7905
+ });
7906
+ }
7694
7907
  async function readHttpStatus(url, timeoutMs) {
7695
7908
  return new Promise((resolvePromise) => {
7696
7909
  const request = (0, import_node_http.get)(url, (response) => {
@@ -7764,14 +7977,14 @@ async function exists(path) {
7764
7977
  }
7765
7978
  async function isFile(path) {
7766
7979
  try {
7767
- return (await (0, import_promises8.stat)(path)).isFile();
7980
+ return (await (0, import_promises9.stat)(path)).isFile();
7768
7981
  } catch (_err) {
7769
7982
  return false;
7770
7983
  }
7771
7984
  }
7772
7985
  async function isDirectory(path) {
7773
7986
  try {
7774
- return (await (0, import_promises8.stat)(path)).isDirectory();
7987
+ return (await (0, import_promises9.stat)(path)).isDirectory();
7775
7988
  } catch (_err) {
7776
7989
  return false;
7777
7990
  }
@@ -7783,6 +7996,21 @@ async function readFileIfExists(path) {
7783
7996
  return void 0;
7784
7997
  }
7785
7998
  }
7999
+ async function removePathIfExists(path, label, dryRun) {
8000
+ if (!await exists(path)) {
8001
+ console.log(`Already absent: ${path}`);
8002
+ return;
8003
+ }
8004
+ await removePath(path, label, dryRun);
8005
+ }
8006
+ async function removePath(path, label, dryRun) {
8007
+ if (dryRun) {
8008
+ console.log(`Would remove ${label}: ${path}`);
8009
+ return;
8010
+ }
8011
+ await (0, import_promises8.rm)(path, { force: true, recursive: true });
8012
+ console.log(`Removed ${label}: ${path}`);
8013
+ }
7786
8014
  function renderTemplate(template, config) {
7787
8015
  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
8016
  }
@@ -7818,7 +8046,7 @@ function parseClaudeMcpScope(value) {
7818
8046
  }
7819
8047
  throw new Error(`Invalid Claude MCP scope: ${value}. Expected local, project, or user.`);
7820
8048
  }
7821
- async function resolveRepairMcpClients(value) {
8049
+ async function resolveMcpClients(value, action) {
7822
8050
  const normalized = value.trim().toLowerCase();
7823
8051
  if (normalized === "none" || normalized === "false" || normalized === "off") {
7824
8052
  return [];
@@ -7832,7 +8060,7 @@ async function resolveRepairMcpClients(value) {
7832
8060
  const clients = [];
7833
8061
  for (const client of requested) {
7834
8062
  if (!await findExecutable([client])) {
7835
- console.log(`WARN ${client} command not found; cannot repair ${client} MCP config.`);
8063
+ console.log(`WARN ${client} command not found; cannot ${action} ${client} MCP config.`);
7836
8064
  continue;
7837
8065
  }
7838
8066
  if (!clients.includes(client)) {
@@ -7858,6 +8086,12 @@ function expandPath(path) {
7858
8086
  }
7859
8087
  return (0, import_node_path3.isAbsolute)(path) ? path : (0, import_node_path6.resolve)(getInvocationCwd(), path);
7860
8088
  }
8089
+ function assertSafeThreadnoteHomeForErase(path) {
8090
+ const resolvedPath = (0, import_node_path6.resolve)(path);
8091
+ if (resolvedPath === "/" || resolvedPath === (0, import_node_os.homedir)() || resolvedPath === (0, import_node_path2.dirname)((0, import_node_os.homedir)())) {
8092
+ throw new Error(`Refusing to erase unsafe THREADNOTE_HOME: ${resolvedPath}`);
8093
+ }
8094
+ }
7861
8095
  function portablePath(path) {
7862
8096
  const home = (0, import_node_os.homedir)();
7863
8097
  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
@@ -10,6 +10,38 @@ threadnote install
10
10
 
11
11
  The installer prefers `pipx`, then `uv`, then `python3 -m pip install --user`.
12
12
 
13
+ ## `uv` Fails With `UnknownIssuer`
14
+
15
+ Some corporate machines trust PyPI through certificates installed in the system keychain. Threadnote passes
16
+ `--native-tls` when it uses `uv` so those system certificates are loaded.
17
+
18
+ If an older install still fails with `invalid peer certificate: UnknownIssuer`, retry with:
19
+
20
+ ```bash
21
+ UV_NATIVE_TLS=1 threadnote install
22
+ ```
23
+
24
+ Or use a different Python installer:
25
+
26
+ ```bash
27
+ threadnote install --package-manager pipx
28
+ threadnote install --package-manager pip
29
+ ```
30
+
31
+ ## Model Download Fails With `CERTIFICATE_VERIFY_FAILED`
32
+
33
+ On first start, OpenViking may download the local embedding model from Hugging Face. If `~/.openviking/logs/server.log`
34
+ shows `SSLCertVerificationError`, `self-signed certificate in certificate chain`, or `Failed to download local embedding
35
+ model`, repair the OpenViking Python environment and start again:
36
+
37
+ ```bash
38
+ threadnote repair --package-manager uv
39
+ threadnote start
40
+ ```
41
+
42
+ Threadnote installs `pip-system-certs` into the OpenViking environment so Python `requests` can use certificates trusted
43
+ by the operating system.
44
+
13
45
  ## Local Embedding Extra Missing
14
46
 
15
47
  The default OpenViking config uses the local embedding backend. If the server log says `llama-cpp-python` is missing,
@@ -23,6 +55,14 @@ The installer repairs this by installing `openviking[local-embed]`.
23
55
 
24
56
  ## Server Health Fails
25
57
 
58
+ If `doctor` reports `WARN openviking health: connect ECONNREFUSED 127.0.0.1:1933`, the local server is not running.
59
+ Start it and recheck:
60
+
61
+ ```bash
62
+ threadnote start
63
+ threadnote doctor --dry-run
64
+ ```
65
+
26
66
  Check whether the server is running:
27
67
 
28
68
  ```bash
@@ -35,6 +75,9 @@ For detached starts, logs are written to:
35
75
  ~/.openviking/logs/server.log
36
76
  ```
37
77
 
78
+ If `start` reports that OpenViking did not become healthy, open that log. Certificate failures during the first embedding
79
+ model download are covered above.
80
+
38
81
  ## Port Already In Use
39
82
 
40
83
  The default bind address is `127.0.0.1:1933`. This does not conflict with projects serving `localhost:80`,
@@ -105,3 +148,15 @@ This is expected. Run with `--apply` after reviewing the command:
105
148
  ```bash
106
149
  threadnote mcp-install codex --apply
107
150
  ```
151
+
152
+ ## Uninstall Without Losing Memories
153
+
154
+ Run:
155
+
156
+ ```bash
157
+ threadnote uninstall --dry-run
158
+ threadnote uninstall
159
+ ```
160
+
161
+ By default, uninstall removes Threadnote-managed shims, MCP config, launchd config, and user instruction blocks while
162
+ 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.1",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",
@@ -125,4 +125,8 @@ if ! have threadnote; then
125
125
  fi
126
126
 
127
127
  say ""
128
- say "Threadnote is installed. Run: threadnote doctor --dry-run"
128
+ say "Threadnote is installed. Next:"
129
+ say " threadnote start"
130
+ say " threadnote doctor --dry-run"
131
+ say " threadnote mcp-install codex --apply # if you use Codex"
132
+ say " threadnote mcp-install claude --apply # if you use Claude"