threadnote 0.2.0 → 0.2.2
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 +109 -101
- package/dist/threadnote.cjs +131 -22
- package/docs/troubleshooting.md +51 -0
- package/package.json +1 -1
- package/scripts/install.sh +5 -1
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
|
|
74
|
-
threadnote seed-skills --dry-run
|
|
68
|
+
threadnote doctor --dry-run
|
|
75
69
|
```
|
|
76
70
|
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
Create or update the local manifest with the repos you care about:
|
|
172
121
|
|
|
173
122
|
```bash
|
|
174
|
-
threadnote
|
|
123
|
+
threadnote init-manifest --repo ~/src/my-service --repo ~/work/mobile-app
|
|
175
124
|
```
|
|
176
125
|
|
|
177
|
-
|
|
126
|
+
Review what will be imported, then seed curated repo guidance:
|
|
178
127
|
|
|
179
128
|
```bash
|
|
180
|
-
threadnote
|
|
129
|
+
threadnote seed --dry-run
|
|
130
|
+
threadnote seed
|
|
181
131
|
```
|
|
182
132
|
|
|
183
|
-
|
|
133
|
+
Optionally seed shared and repo-local skills:
|
|
184
134
|
|
|
185
135
|
```bash
|
|
186
|
-
threadnote
|
|
136
|
+
threadnote seed-skills --dry-run
|
|
137
|
+
threadnote seed-skills
|
|
187
138
|
```
|
|
188
139
|
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
##
|
|
169
|
+
## Source Checkout
|
|
198
170
|
|
|
199
|
-
|
|
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
|
|
174
|
+
npm install
|
|
205
175
|
npm run build
|
|
206
|
-
npm
|
|
207
|
-
npm
|
|
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
|
|
package/dist/threadnote.cjs
CHANGED
|
@@ -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,23 @@ 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}`);
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6297
|
+
const localEmbeddingMissing = await hasLocalEmbeddingDependency(serverPath) === false;
|
|
6298
|
+
const pythonSystemCertificatesMissing = await hasPythonSystemCertificatesPatch(serverPath) === false;
|
|
6299
|
+
if (localEmbeddingMissing) {
|
|
6300
|
+
const repairReasons = [];
|
|
6301
|
+
repairReasons.push("local embedding extra is missing");
|
|
6302
|
+
if (pythonSystemCertificatesMissing) {
|
|
6303
|
+
repairReasons.push("Python system certificate bridge is missing");
|
|
6304
|
+
}
|
|
6305
|
+
console.log(`OpenViking install needs repair: ${repairReasons.join("; ")}.`);
|
|
6306
|
+
await runInstallCommands(config, options.packageManager, true, options.dryRun === true);
|
|
6307
|
+
} else if (pythonSystemCertificatesMissing) {
|
|
6308
|
+
console.log("OpenViking install needs repair: Python system certificate bridge is missing.");
|
|
6309
|
+
const installCommand = await getPythonSystemCertificatesInstallCommand(serverPath);
|
|
6295
6310
|
await maybeRun(options.dryRun === true, installCommand.executable, installCommand.args);
|
|
6296
6311
|
}
|
|
6297
6312
|
} else {
|
|
6298
|
-
|
|
6299
|
-
await maybeRun(options.dryRun === true, installCommand.executable, installCommand.args);
|
|
6313
|
+
await runInstallCommands(config, options.packageManager, false, options.dryRun === true);
|
|
6300
6314
|
}
|
|
6301
6315
|
await writeTemplateIfMissing({
|
|
6302
6316
|
config,
|
|
@@ -6312,9 +6326,9 @@ async function runInstall(config, options) {
|
|
|
6312
6326
|
shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
6313
6327
|
templatePath: (0, import_node_path4.join)(toolRoot(), "config", "ovcli.conf.template.json")
|
|
6314
6328
|
});
|
|
6315
|
-
console.log("Install complete. Run
|
|
6316
|
-
console.log(" threadnote doctor");
|
|
6329
|
+
console.log("Install complete. Run start, then doctor:");
|
|
6317
6330
|
console.log(" threadnote start");
|
|
6331
|
+
console.log(" threadnote doctor");
|
|
6318
6332
|
}
|
|
6319
6333
|
async function runRepair(config, options) {
|
|
6320
6334
|
const dryRun = options.dryRun === true;
|
|
@@ -6436,8 +6450,9 @@ async function runStart(config, options) {
|
|
|
6436
6450
|
return;
|
|
6437
6451
|
}
|
|
6438
6452
|
const existingHealth = await readOpenVikingHealthIfAvailable(config, 500);
|
|
6453
|
+
const healthUrl = openVikingHealthUrl(config);
|
|
6439
6454
|
if (existingHealth) {
|
|
6440
|
-
console.log(`OpenViking is already healthy at
|
|
6455
|
+
console.log(`OpenViking is already healthy at ${healthUrl}`);
|
|
6441
6456
|
return;
|
|
6442
6457
|
}
|
|
6443
6458
|
if (await isTcpPortOpen(config.host, config.port, 500)) {
|
|
@@ -6445,19 +6460,26 @@ async function runStart(config, options) {
|
|
|
6445
6460
|
`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
6461
|
);
|
|
6447
6462
|
}
|
|
6448
|
-
|
|
6463
|
+
const logPath = openVikingLogPath(config);
|
|
6464
|
+
await ensureDirectory((0, import_node_path2.dirname)(logPath), false);
|
|
6449
6465
|
if (options.foreground === true) {
|
|
6450
6466
|
const result = await runInteractive(server, args);
|
|
6451
6467
|
process.exitCode = result;
|
|
6452
6468
|
return;
|
|
6453
6469
|
}
|
|
6454
|
-
const logPath = (0, import_node_path4.join)(config.agentContextHome, "logs", "server.log");
|
|
6455
6470
|
const logFd = (0, import_node_fs3.openSync)(logPath, "a");
|
|
6456
6471
|
const child = spawnDetachedServerWithLog(server, args, logFd);
|
|
6457
6472
|
child.unref();
|
|
6458
6473
|
await (0, import_promises10.writeFile)((0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
6459
6474
|
`, "utf8");
|
|
6460
|
-
|
|
6475
|
+
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
6476
|
+
if (health) {
|
|
6477
|
+
console.log(`Started OpenViking with pid ${child.pid}. Health OK at ${healthUrl}. Logs: ${logPath}`);
|
|
6478
|
+
return;
|
|
6479
|
+
}
|
|
6480
|
+
throw new Error(
|
|
6481
|
+
`Started OpenViking with pid ${child.pid}, but ${healthUrl} did not become healthy within ${START_HEALTH_TIMEOUT_MS / 1e3}s. Logs: ${logPath}`
|
|
6482
|
+
);
|
|
6461
6483
|
}
|
|
6462
6484
|
function spawnDetachedServerWithLog(server, args, logFd) {
|
|
6463
6485
|
try {
|
|
@@ -6824,6 +6846,21 @@ async function localEmbeddingCheck() {
|
|
|
6824
6846
|
detail: "llama_cpp missing; install will repair with openviking[local-embed]"
|
|
6825
6847
|
};
|
|
6826
6848
|
}
|
|
6849
|
+
async function pythonSystemCertificatesCheck() {
|
|
6850
|
+
const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
6851
|
+
if (!serverPath) {
|
|
6852
|
+
return { name: "python system certs", status: "warn", detail: "openviking-server missing" };
|
|
6853
|
+
}
|
|
6854
|
+
const hasDependency = await hasPythonSystemCertificatesPatch(serverPath);
|
|
6855
|
+
if (hasDependency === void 0) {
|
|
6856
|
+
return { name: "python system certs", status: "warn", detail: "could not inspect tool Python" };
|
|
6857
|
+
}
|
|
6858
|
+
return hasDependency ? { name: "python system certs", status: "ok", detail: `${PYTHON_SYSTEM_CERTS_MODULE} import works` } : {
|
|
6859
|
+
name: "python system certs",
|
|
6860
|
+
status: "warn",
|
|
6861
|
+
detail: `${PYTHON_SYSTEM_CERTS_MODULE} missing; install will repair corporate TLS support`
|
|
6862
|
+
};
|
|
6863
|
+
}
|
|
6827
6864
|
async function commandShimCheck() {
|
|
6828
6865
|
const shimPath = (0, import_node_path4.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
6829
6866
|
const content = await readFileIfExists(shimPath);
|
|
@@ -6867,11 +6904,17 @@ async function userAgentInstructionsChecks() {
|
|
|
6867
6904
|
);
|
|
6868
6905
|
}
|
|
6869
6906
|
async function hasLocalEmbeddingDependency(serverPath) {
|
|
6907
|
+
return hasPythonModule(serverPath, "llama_cpp");
|
|
6908
|
+
}
|
|
6909
|
+
async function hasPythonSystemCertificatesPatch(serverPath) {
|
|
6910
|
+
return hasPythonModule(serverPath, PYTHON_SYSTEM_CERTS_MODULE);
|
|
6911
|
+
}
|
|
6912
|
+
async function hasPythonModule(serverPath, moduleName) {
|
|
6870
6913
|
const pythonPath = await siblingPythonForExecutable(serverPath);
|
|
6871
6914
|
if (!pythonPath) {
|
|
6872
6915
|
return void 0;
|
|
6873
6916
|
}
|
|
6874
|
-
const result = await runCommand(pythonPath, ["-c",
|
|
6917
|
+
const result = await runCommand(pythonPath, ["-c", `import ${moduleName}`], { allowFailure: true });
|
|
6875
6918
|
return result.exitCode === 0;
|
|
6876
6919
|
}
|
|
6877
6920
|
async function siblingPythonForExecutable(executablePath) {
|
|
@@ -6904,7 +6947,7 @@ async function healthCheck(config) {
|
|
|
6904
6947
|
}
|
|
6905
6948
|
}
|
|
6906
6949
|
async function readOpenVikingHealth(config, timeoutMs) {
|
|
6907
|
-
return httpGetText(
|
|
6950
|
+
return httpGetText(openVikingHealthUrl(config), timeoutMs);
|
|
6908
6951
|
}
|
|
6909
6952
|
async function readOpenVikingHealthIfAvailable(config, timeoutMs) {
|
|
6910
6953
|
try {
|
|
@@ -6913,24 +6956,76 @@ async function readOpenVikingHealthIfAvailable(config, timeoutMs) {
|
|
|
6913
6956
|
return void 0;
|
|
6914
6957
|
}
|
|
6915
6958
|
}
|
|
6916
|
-
|
|
6959
|
+
function openVikingHealthUrl(config) {
|
|
6960
|
+
return `http://${config.host}:${config.port}/health`;
|
|
6961
|
+
}
|
|
6962
|
+
function openVikingLogPath(config) {
|
|
6963
|
+
return (0, import_node_path4.join)(config.agentContextHome, "logs", "server.log");
|
|
6964
|
+
}
|
|
6965
|
+
async function waitForOpenVikingHealth(config, timeoutMs) {
|
|
6966
|
+
const deadline = Date.now() + timeoutMs;
|
|
6967
|
+
while (Date.now() <= deadline) {
|
|
6968
|
+
const requestTimeoutMs = Math.max(100, Math.min(1e3, deadline - Date.now()));
|
|
6969
|
+
const health = await readOpenVikingHealthIfAvailable(config, requestTimeoutMs);
|
|
6970
|
+
if (health) {
|
|
6971
|
+
return health;
|
|
6972
|
+
}
|
|
6973
|
+
const remainingMs = deadline - Date.now();
|
|
6974
|
+
if (remainingMs <= 0) {
|
|
6975
|
+
break;
|
|
6976
|
+
}
|
|
6977
|
+
await sleep(Math.min(START_HEALTH_POLL_INTERVAL_MS, remainingMs));
|
|
6978
|
+
}
|
|
6979
|
+
return void 0;
|
|
6980
|
+
}
|
|
6981
|
+
async function runInstallCommands(config, preferred, force, dryRun) {
|
|
6982
|
+
const installCommands = await getInstallCommands(config, preferred, force);
|
|
6983
|
+
for (const installCommand of installCommands) {
|
|
6984
|
+
await maybeRun(dryRun, installCommand.executable, installCommand.args);
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
async function getPythonSystemCertificatesInstallCommand(serverPath) {
|
|
6988
|
+
const pythonPath = await siblingPythonForExecutable(serverPath);
|
|
6989
|
+
if (!pythonPath) {
|
|
6990
|
+
throw new Error(`Could not find the OpenViking Python environment for ${serverPath}`);
|
|
6991
|
+
}
|
|
6992
|
+
const uvPath = await findExecutable(["uv"]);
|
|
6993
|
+
if (uvPath) {
|
|
6994
|
+
return {
|
|
6995
|
+
executable: uvPath,
|
|
6996
|
+
args: ["pip", "install", "--native-tls", "--python", pythonPath, PYTHON_SYSTEM_CERTS_PACKAGE]
|
|
6997
|
+
};
|
|
6998
|
+
}
|
|
6999
|
+
return { executable: pythonPath, args: ["-m", "pip", "install", PYTHON_SYSTEM_CERTS_PACKAGE] };
|
|
7000
|
+
}
|
|
7001
|
+
async function getInstallCommands(config, preferred, force) {
|
|
6917
7002
|
const packageSpec = `${OPENVIKING_PACKAGE_NAME}==${config.openVikingVersion}`;
|
|
6918
7003
|
const manager = preferred ?? await detectPackageManager();
|
|
6919
7004
|
if (manager === "pipx") {
|
|
6920
|
-
return
|
|
7005
|
+
return [
|
|
7006
|
+
{ executable: "pipx", args: force ? ["install", "--force", packageSpec] : ["install", packageSpec] },
|
|
7007
|
+
{
|
|
7008
|
+
executable: "pipx",
|
|
7009
|
+
args: force ? ["inject", "--force", "openviking", PYTHON_SYSTEM_CERTS_PACKAGE] : ["inject", "openviking", PYTHON_SYSTEM_CERTS_PACKAGE]
|
|
7010
|
+
}
|
|
7011
|
+
];
|
|
6921
7012
|
}
|
|
6922
7013
|
if (manager === "uv") {
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
7014
|
+
const uvArgs = ["tool", "install", "--native-tls", "--with", PYTHON_SYSTEM_CERTS_PACKAGE];
|
|
7015
|
+
return [
|
|
7016
|
+
{
|
|
7017
|
+
executable: "uv",
|
|
7018
|
+
args: force ? [...uvArgs, "--force", packageSpec] : [...uvArgs, packageSpec]
|
|
7019
|
+
}
|
|
7020
|
+
];
|
|
6927
7021
|
}
|
|
6928
7022
|
const pipArgs = ["-m", "pip", "install", "--user"];
|
|
6929
7023
|
if (force) {
|
|
6930
7024
|
pipArgs.push("--upgrade", "--force-reinstall");
|
|
6931
7025
|
}
|
|
7026
|
+
pipArgs.push(PYTHON_SYSTEM_CERTS_PACKAGE);
|
|
6932
7027
|
pipArgs.push(packageSpec);
|
|
6933
|
-
return { executable: "python3", args: pipArgs };
|
|
7028
|
+
return [{ executable: "python3", args: pipArgs }];
|
|
6934
7029
|
}
|
|
6935
7030
|
async function detectPackageManager() {
|
|
6936
7031
|
if (await findExecutable(["pipx"])) {
|
|
@@ -7149,11 +7244,20 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
7149
7244
|
return;
|
|
7150
7245
|
}
|
|
7151
7246
|
await ensureDirectory((0, import_node_path2.dirname)(destination), false);
|
|
7247
|
+
await ensureDirectory((0, import_node_path2.dirname)(openVikingLogPath(config)), false);
|
|
7152
7248
|
await (0, import_promises10.writeFile)(destination, rendered, "utf8");
|
|
7153
7249
|
await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
|
|
7154
7250
|
await maybeRun(false, "launchctl", ["load", destination]);
|
|
7155
7251
|
await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
|
|
7156
|
-
|
|
7252
|
+
const healthUrl = openVikingHealthUrl(config);
|
|
7253
|
+
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
7254
|
+
if (health) {
|
|
7255
|
+
console.log(`Installed and started ${LAUNCHD_LABEL}. Health OK at ${healthUrl}`);
|
|
7256
|
+
return;
|
|
7257
|
+
}
|
|
7258
|
+
throw new Error(
|
|
7259
|
+
`Installed and started ${LAUNCHD_LABEL}, but ${healthUrl} did not become healthy within ${START_HEALTH_TIMEOUT_MS / 1e3}s. Logs: ${openVikingLogPath(config)}`
|
|
7260
|
+
);
|
|
7157
7261
|
}
|
|
7158
7262
|
async function removeLaunchAgent(dryRun) {
|
|
7159
7263
|
const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
@@ -7310,7 +7414,7 @@ function buildMcpInstallCommand(config, agent, name, options) {
|
|
|
7310
7414
|
if (agent === "codex") {
|
|
7311
7415
|
return {
|
|
7312
7416
|
executable: "codex",
|
|
7313
|
-
args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", ...command]
|
|
7417
|
+
args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", "/usr/bin/env", ...command]
|
|
7314
7418
|
};
|
|
7315
7419
|
}
|
|
7316
7420
|
return {
|
|
@@ -7813,6 +7917,11 @@ async function httpGetText(url, timeoutMs) {
|
|
|
7813
7917
|
request.on("error", rejectPromise);
|
|
7814
7918
|
});
|
|
7815
7919
|
}
|
|
7920
|
+
async function sleep(ms) {
|
|
7921
|
+
return new Promise((resolvePromise) => {
|
|
7922
|
+
setTimeout(resolvePromise, ms);
|
|
7923
|
+
});
|
|
7924
|
+
}
|
|
7816
7925
|
async function readHttpStatus(url, timeoutMs) {
|
|
7817
7926
|
return new Promise((resolvePromise) => {
|
|
7818
7927
|
const request = (0, import_node_http.get)(url, (response) => {
|
package/docs/troubleshooting.md
CHANGED
|
@@ -10,6 +10,46 @@ 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
|
+
|
|
45
|
+
If an older Threadnote release tries to reinstall all of OpenViking and fails while fetching packages such as `openai`,
|
|
46
|
+
install the certificate bridge directly into the existing OpenViking environment:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv pip install --native-tls --python "$(dirname "$(realpath "$(which openviking-server)")")/python" pip-system-certs
|
|
50
|
+
threadnote start
|
|
51
|
+
```
|
|
52
|
+
|
|
13
53
|
## Local Embedding Extra Missing
|
|
14
54
|
|
|
15
55
|
The default OpenViking config uses the local embedding backend. If the server log says `llama-cpp-python` is missing,
|
|
@@ -23,6 +63,14 @@ The installer repairs this by installing `openviking[local-embed]`.
|
|
|
23
63
|
|
|
24
64
|
## Server Health Fails
|
|
25
65
|
|
|
66
|
+
If `doctor` reports `WARN openviking health: connect ECONNREFUSED 127.0.0.1:1933`, the local server is not running.
|
|
67
|
+
Start it and recheck:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
threadnote start
|
|
71
|
+
threadnote doctor --dry-run
|
|
72
|
+
```
|
|
73
|
+
|
|
26
74
|
Check whether the server is running:
|
|
27
75
|
|
|
28
76
|
```bash
|
|
@@ -35,6 +83,9 @@ For detached starts, logs are written to:
|
|
|
35
83
|
~/.openviking/logs/server.log
|
|
36
84
|
```
|
|
37
85
|
|
|
86
|
+
If `start` reports that OpenViking did not become healthy, open that log. Certificate failures during the first embedding
|
|
87
|
+
model download are covered above.
|
|
88
|
+
|
|
38
89
|
## Port Already In Use
|
|
39
90
|
|
|
40
91
|
The default bind address is `127.0.0.1:1933`. This does not conflict with projects serving `localhost:80`,
|
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -125,4 +125,8 @@ if ! have threadnote; then
|
|
|
125
125
|
fi
|
|
126
126
|
|
|
127
127
|
say ""
|
|
128
|
-
say "Threadnote is installed.
|
|
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"
|