threadnote 0.2.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,91 +64,13 @@ Or install manually:
40
64
  ```bash
41
65
  npm install --global threadnote
42
66
  threadnote install
43
- ```
44
-
45
- For a one-off check before installing globally:
46
-
47
- ```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
51
- ```
52
-
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.
54
-
55
- ## Source Checkout
56
-
57
- For local development from this repo:
58
-
59
- ```bash
60
- npm install
61
- npm run build
62
- npm run doctor -- --dry-run
63
- npm run threadnote -- install
64
- ```
65
-
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:
68
-
69
- ```bash
70
- threadnote doctor --dry-run
71
- threadnote init-manifest --repo ~/src/my-service --repo ~/work/mobile-app
72
67
  threadnote start
73
- threadnote seed --dry-run
74
- threadnote seed-skills --dry-run
68
+ threadnote doctor --dry-run
75
69
  ```
76
70
 
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.
79
-
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.
82
-
83
- ## Commands
84
-
85
- - `doctor`: checks prerequisites, the generated command shim, manifest shape, templates, and local OpenViking health.
86
- - `install`: installs `openviking[local-embed]==0.3.12` if missing, creates `~/.openviking` config files if absent,
87
- writes the command shim, and upserts user-level agent instructions.
88
- - `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude MCP configs from the
89
- current checkout.
90
- - `start`: starts `openviking-server` on `127.0.0.1:1933`.
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`.
94
- - `init-manifest`: creates or updates `~/.openviking/seed-manifest.yaml` from one or more developer repo roots.
95
- - `seed`: imports curated repo guidance and docs from the manifest.
96
- - `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog. Use
97
- `seed-skills --native` only after configuring a working VLM provider.
98
- - `mcp-install codex|claude`: installs or prints OpenViking MCP configuration for Codex or Claude.
99
- - `remember`: stores a durable memory.
100
- - `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
101
- `skills for api service`; use `--uri` or `--no-infer-scope` to override.
102
- - `read`: reads a `viking://` URI returned by `recall` or `list`.
103
- - `list` / `ls`: lists a `viking://` directory.
104
- - `handoff`: stores current git state and next-step notes as a durable handoff.
105
- - `forget`: removes a `viking://` URI.
106
- - `export-pack` / `import-pack`: moves local context through `.ovpack` files.
107
-
108
- ## Configuration
109
-
110
- Environment variables:
111
-
112
- - `THREADNOTE_HOME`: local state directory, default `~/.openviking`.
113
- - `THREADNOTE_MANIFEST`: seed manifest path. Defaults to `~/.openviking/seed-manifest.yaml` if present, otherwise
114
- the bundled example manifest.
115
- - `THREADNOTE_ACCOUNT`: OpenViking account header/config value, default `local`.
116
- - `THREADNOTE_USER`: OpenViking user value, default local username.
117
- - `THREADNOTE_AGENT_ID`: shared agent identity, default `threadnote`.
118
- - `THREADNOTE_OPENVIKING_VERSION`: package version to install, default `0.3.12`.
119
- - `THREADNOTE_BIN_DIR`: directory for the `threadnote` shim, default `~/.local/bin`.
120
- - `THREADNOTE_HOST`: local bind host, default `127.0.0.1`.
121
- - `THREADNOTE_PORT`: local bind port, default `1933`.
122
-
123
- Local projects using `localhost:80` or `localhost:443` do not conflict with OpenViking on `127.0.0.1:1933`. A conflict
124
- only occurs when another process already owns the same host and port. If that happens, choose a different
125
- `THREADNOTE_PORT`.
71
+ ### MCP
126
72
 
127
- ## MCP
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.
128
74
 
129
75
  Dry-run examples:
130
76
 
@@ -166,47 +112,109 @@ If a future OpenViking build exposes a healthy native endpoint, install it expli
166
112
  threadnote mcp-install claude --native-http --apply
167
113
  ```
168
114
 
169
- ## Uninstall
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.
170
119
 
171
- Preview removal:
120
+ Create or update the local manifest with the repos you care about:
172
121
 
173
122
  ```bash
174
- threadnote uninstall --dry-run
123
+ threadnote init-manifest --repo ~/src/my-service --repo ~/work/mobile-app
175
124
  ```
176
125
 
177
- Remove Threadnote setup while keeping local OpenViking memories:
126
+ Review what will be imported, then seed curated repo guidance:
178
127
 
179
128
  ```bash
180
- threadnote uninstall
129
+ threadnote seed --dry-run
130
+ threadnote seed
181
131
  ```
182
132
 
183
- Erase local memories too:
133
+ Optionally seed shared and repo-local skills:
184
134
 
185
135
  ```bash
186
- threadnote uninstall --erase-memories
136
+ threadnote seed-skills --dry-run
137
+ threadnote seed-skills
187
138
  ```
188
139
 
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.
140
+ When you add another local repo later, rerun `init-manifest --repo <path>` and seed again.
191
141
 
192
- ## Notes
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.
143
+
144
+ ## Commands
193
145
 
194
- OpenViking itself is not vendored here. This prototype installs and controls a local service and should get legal and
195
- security review before broader rollout.
146
+ - `doctor`: checks prerequisites, the generated command shim, manifest shape, templates, and local OpenViking health.
147
+ - `install`: installs `openviking[local-embed]==0.3.12` if missing, creates `~/.openviking` config files if absent,
148
+ writes the command shim, and upserts user-level agent instructions.
149
+ - `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude MCP configs from the
150
+ current checkout.
151
+ - `start`: starts `openviking-server` on `127.0.0.1:1933`.
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`.
155
+ - `init-manifest`: creates or updates `~/.openviking/seed-manifest.yaml` from one or more developer repo roots.
156
+ - `seed`: imports curated repo guidance and docs from the manifest.
157
+ - `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog. Use
158
+ `seed-skills --native` only after configuring a working VLM provider.
159
+ - `mcp-install codex|claude`: installs or prints OpenViking MCP configuration for Codex or Claude.
160
+ - `remember`: stores a durable memory.
161
+ - `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
162
+ `skills for api service`; use `--uri` or `--no-infer-scope` to override.
163
+ - `read`: reads a `viking://` URI returned by `recall` or `list`.
164
+ - `list` / `ls`: lists a `viking://` directory.
165
+ - `handoff`: stores current git state and next-step notes as a durable handoff.
166
+ - `forget`: removes a `viking://` URI.
167
+ - `export-pack` / `import-pack`: moves local context through `.ovpack` files.
196
168
 
197
- ## Publishing
169
+ ## Source Checkout
198
170
 
199
- The npm package ships bundled CommonJS `.cjs` entrypoints in `dist/`; `tsx` is only used by `npm run dev` for
200
- source-checkout development. There is no published TypeScript runner and no runtime npm dependency tree. The generated
201
- user shim tries Node, Bun, then Deno.
171
+ For local development from this repo:
202
172
 
203
173
  ```bash
204
- npm run typecheck
174
+ npm install
205
175
  npm run build
206
- npm pack --dry-run
207
- npm publish
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
208
189
  ```
209
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
+
197
+ ## Configuration
198
+
199
+ Environment variables:
200
+
201
+ - `THREADNOTE_HOME`: local state directory, default `~/.openviking`.
202
+ - `THREADNOTE_MANIFEST`: seed manifest path. Defaults to `~/.openviking/seed-manifest.yaml` if present, otherwise
203
+ the bundled example manifest.
204
+ - `THREADNOTE_ACCOUNT`: OpenViking account header/config value, default `local`.
205
+ - `THREADNOTE_USER`: OpenViking user value, default local username.
206
+ - `THREADNOTE_AGENT_ID`: shared agent identity, default `threadnote`.
207
+ - `THREADNOTE_OPENVIKING_VERSION`: package version to install, default `0.3.12`.
208
+ - `THREADNOTE_BIN_DIR`: directory for the `threadnote` shim, default `~/.local/bin`.
209
+ - `THREADNOTE_HOST`: local bind host, default `127.0.0.1`.
210
+ - `THREADNOTE_PORT`: local bind port, default `1933`.
211
+
212
+ Local projects using `localhost:80` or `localhost:443` do not conflict with OpenViking on `127.0.0.1:1933`. A conflict
213
+ only occurs when another process already owns the same host and port. If that happens, choose a different
214
+ `THREADNOTE_PORT`.
215
+
216
+ ## Misc
217
+
210
218
  See `docs/migration.md` for switching an existing repo workflow to `threadnote` without deleting canonical
211
219
  `AGENTS.md`, `CLAUDE.md`, `.claude/`, or `.agents/` files.
212
220
 
@@ -6131,8 +6131,12 @@ var DEFAULT_AGENT_ID = "threadnote";
6131
6131
  var OPENVIKING_PACKAGE_NAME = "openviking[local-embed]";
6132
6132
  var OPENVIKING_SERVER_COMMAND = "openviking-server";
6133
6133
  var OPENVIKING_MCP_NAME = "threadnote";
6134
+ var PYTHON_SYSTEM_CERTS_MODULE = "pip_system_certs";
6135
+ var PYTHON_SYSTEM_CERTS_PACKAGE = "pip-system-certs";
6134
6136
  var LAUNCHD_LABEL = "io.threadnote.openviking";
6135
6137
  var MAX_SECRET_MATCHES_TO_PRINT = 5;
6138
+ var START_HEALTH_POLL_INTERVAL_MS = 500;
6139
+ var START_HEALTH_TIMEOUT_MS = 15e3;
6136
6140
  var SHIM_MARKER = "Generated by threadnote";
6137
6141
  var USER_INSTRUCTIONS_START_MARKER = "<!-- BEGIN THREADNOTE USER INSTRUCTIONS -->";
6138
6142
  var USER_INSTRUCTIONS_END_MARKER = "<!-- END THREADNOTE USER INSTRUCTIONS -->";
@@ -6257,6 +6261,7 @@ async function runDoctor(config, options) {
6257
6261
  checks.push(await commandCheck("openviking-server", ["--help"]));
6258
6262
  checks.push(await firstCommandCheck("openviking cli", ["ov", "openviking"], ["--help"]));
6259
6263
  checks.push(await localEmbeddingCheck());
6264
+ checks.push(await pythonSystemCertificatesCheck());
6260
6265
  checks.push(await firstCommandCheck("python installer", ["pipx", "uv", "pip3"], ["--version"]));
6261
6266
  checks.push(await commandPresenceCheck("codex", ["--version"]));
6262
6267
  checks.push(await commandPresenceCheck("claude", ["--version"]));
@@ -6289,14 +6294,19 @@ async function runInstall(config, options) {
6289
6294
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
6290
6295
  if (serverPath) {
6291
6296
  console.log(`OpenViking server already installed: ${serverPath}`);
6297
+ const repairReasons = [];
6292
6298
  if (await hasLocalEmbeddingDependency(serverPath) === false) {
6293
- console.log("OpenViking local embedding extra is missing; repairing install.");
6294
- const installCommand = await getInstallCommand(config, options.packageManager, true);
6295
- 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);
6296
6307
  }
6297
6308
  } else {
6298
- const installCommand = await getInstallCommand(config, options.packageManager, false);
6299
- await maybeRun(options.dryRun === true, installCommand.executable, installCommand.args);
6309
+ await runInstallCommands(config, options.packageManager, false, options.dryRun === true);
6300
6310
  }
6301
6311
  await writeTemplateIfMissing({
6302
6312
  config,
@@ -6312,9 +6322,9 @@ async function runInstall(config, options) {
6312
6322
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
6313
6323
  templatePath: (0, import_node_path4.join)(toolRoot(), "config", "ovcli.conf.template.json")
6314
6324
  });
6315
- console.log("Install complete. Run doctor, then start:");
6316
- console.log(" threadnote doctor");
6325
+ console.log("Install complete. Run start, then doctor:");
6317
6326
  console.log(" threadnote start");
6327
+ console.log(" threadnote doctor");
6318
6328
  }
6319
6329
  async function runRepair(config, options) {
6320
6330
  const dryRun = options.dryRun === true;
@@ -6436,8 +6446,9 @@ async function runStart(config, options) {
6436
6446
  return;
6437
6447
  }
6438
6448
  const existingHealth = await readOpenVikingHealthIfAvailable(config, 500);
6449
+ const healthUrl = openVikingHealthUrl(config);
6439
6450
  if (existingHealth) {
6440
- console.log(`OpenViking is already healthy at http://${config.host}:${config.port}/health`);
6451
+ console.log(`OpenViking is already healthy at ${healthUrl}`);
6441
6452
  return;
6442
6453
  }
6443
6454
  if (await isTcpPortOpen(config.host, config.port, 500)) {
@@ -6445,19 +6456,26 @@ async function runStart(config, options) {
6445
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.`
6446
6457
  );
6447
6458
  }
6448
- 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);
6449
6461
  if (options.foreground === true) {
6450
6462
  const result = await runInteractive(server, args);
6451
6463
  process.exitCode = result;
6452
6464
  return;
6453
6465
  }
6454
- const logPath = (0, import_node_path4.join)(config.agentContextHome, "logs", "server.log");
6455
6466
  const logFd = (0, import_node_fs3.openSync)(logPath, "a");
6456
6467
  const child = spawnDetachedServerWithLog(server, args, logFd);
6457
6468
  child.unref();
6458
6469
  await (0, import_promises10.writeFile)((0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
6459
6470
  `, "utf8");
6460
- 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
+ );
6461
6479
  }
6462
6480
  function spawnDetachedServerWithLog(server, args, logFd) {
6463
6481
  try {
@@ -6824,6 +6842,21 @@ async function localEmbeddingCheck() {
6824
6842
  detail: "llama_cpp missing; install will repair with openviking[local-embed]"
6825
6843
  };
6826
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
+ }
6827
6860
  async function commandShimCheck() {
6828
6861
  const shimPath = (0, import_node_path4.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
6829
6862
  const content = await readFileIfExists(shimPath);
@@ -6867,11 +6900,17 @@ async function userAgentInstructionsChecks() {
6867
6900
  );
6868
6901
  }
6869
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) {
6870
6909
  const pythonPath = await siblingPythonForExecutable(serverPath);
6871
6910
  if (!pythonPath) {
6872
6911
  return void 0;
6873
6912
  }
6874
- const result = await runCommand(pythonPath, ["-c", "import llama_cpp"], { allowFailure: true });
6913
+ const result = await runCommand(pythonPath, ["-c", `import ${moduleName}`], { allowFailure: true });
6875
6914
  return result.exitCode === 0;
6876
6915
  }
6877
6916
  async function siblingPythonForExecutable(executablePath) {
@@ -6904,7 +6943,7 @@ async function healthCheck(config) {
6904
6943
  }
6905
6944
  }
6906
6945
  async function readOpenVikingHealth(config, timeoutMs) {
6907
- return httpGetText(`http://${config.host}:${config.port}/health`, timeoutMs);
6946
+ return httpGetText(openVikingHealthUrl(config), timeoutMs);
6908
6947
  }
6909
6948
  async function readOpenVikingHealthIfAvailable(config, timeoutMs) {
6910
6949
  try {
@@ -6913,24 +6952,62 @@ async function readOpenVikingHealthIfAvailable(config, timeoutMs) {
6913
6952
  return void 0;
6914
6953
  }
6915
6954
  }
6916
- 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) {
6917
6984
  const packageSpec = `${OPENVIKING_PACKAGE_NAME}==${config.openVikingVersion}`;
6918
6985
  const manager = preferred ?? await detectPackageManager();
6919
6986
  if (manager === "pipx") {
6920
- 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
+ ];
6921
6994
  }
6922
6995
  if (manager === "uv") {
6923
- return {
6924
- executable: "uv",
6925
- args: force ? ["tool", "install", "--force", packageSpec] : ["tool", "install", packageSpec]
6926
- };
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
+ ];
6927
7003
  }
6928
7004
  const pipArgs = ["-m", "pip", "install", "--user"];
6929
7005
  if (force) {
6930
7006
  pipArgs.push("--upgrade", "--force-reinstall");
6931
7007
  }
7008
+ pipArgs.push(PYTHON_SYSTEM_CERTS_PACKAGE);
6932
7009
  pipArgs.push(packageSpec);
6933
- return { executable: "python3", args: pipArgs };
7010
+ return [{ executable: "python3", args: pipArgs }];
6934
7011
  }
6935
7012
  async function detectPackageManager() {
6936
7013
  if (await findExecutable(["pipx"])) {
@@ -7149,11 +7226,20 @@ async function installLaunchAgent(config, dryRun) {
7149
7226
  return;
7150
7227
  }
7151
7228
  await ensureDirectory((0, import_node_path2.dirname)(destination), false);
7229
+ await ensureDirectory((0, import_node_path2.dirname)(openVikingLogPath(config)), false);
7152
7230
  await (0, import_promises10.writeFile)(destination, rendered, "utf8");
7153
7231
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
7154
7232
  await maybeRun(false, "launchctl", ["load", destination]);
7155
7233
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
7156
- 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
+ );
7157
7243
  }
7158
7244
  async function removeLaunchAgent(dryRun) {
7159
7245
  const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
@@ -7310,7 +7396,7 @@ function buildMcpInstallCommand(config, agent, name, options) {
7310
7396
  if (agent === "codex") {
7311
7397
  return {
7312
7398
  executable: "codex",
7313
- args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", ...command]
7399
+ args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", "/usr/bin/env", ...command]
7314
7400
  };
7315
7401
  }
7316
7402
  return {
@@ -7813,6 +7899,11 @@ async function httpGetText(url, timeoutMs) {
7813
7899
  request.on("error", rejectPromise);
7814
7900
  });
7815
7901
  }
7902
+ async function sleep(ms) {
7903
+ return new Promise((resolvePromise) => {
7904
+ setTimeout(resolvePromise, ms);
7905
+ });
7906
+ }
7816
7907
  async function readHttpStatus(url, timeoutMs) {
7817
7908
  return new Promise((resolvePromise) => {
7818
7909
  const request = (0, import_node_http.get)(url, (response) => {
@@ -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`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.2.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"