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 +112 -79
- package/dist/threadnote.cjs +276 -42
- package/docs/rollout.md +1 -1
- package/docs/security.md +1 -0
- package/docs/troubleshooting.md +55 -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,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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
82
|
+
Apply after review:
|
|
54
83
|
|
|
55
|
-
|
|
84
|
+
```bash
|
|
85
|
+
threadnote mcp-install codex --apply
|
|
86
|
+
threadnote mcp-install claude --apply
|
|
87
|
+
```
|
|
56
88
|
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
npm run build
|
|
62
|
-
npm run doctor -- --dry-run
|
|
63
|
-
npm run threadnote -- install
|
|
95
|
+
threadnote repair
|
|
64
96
|
```
|
|
65
97
|
|
|
66
|
-
`
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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.
|
package/dist/threadnote.cjs
CHANGED
|
@@ -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
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
6469
|
+
await (0, import_promises10.writeFile)((0, import_node_path4.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
6429
6470
|
`, "utf8");
|
|
6430
|
-
|
|
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
|
-
|
|
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,
|
|
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",
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
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,
|
|
7034
|
+
await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
6924
7035
|
await (0, import_promises2.chmod)(backupPath, 384);
|
|
6925
|
-
await (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
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"
|