threadnote 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/threadnote.cjs +468 -157
- package/docs/.nojekyll +0 -0
- package/docs/index.html +1532 -0
- package/package.json +1 -1
package/dist/threadnote.cjs
CHANGED
|
@@ -3498,6 +3498,12 @@ var USER_AGENT_INSTRUCTION_TARGETS = [
|
|
|
3498
3498
|
path: "~/.copilot/instructions/threadnote.instructions.md"
|
|
3499
3499
|
}
|
|
3500
3500
|
];
|
|
3501
|
+
var CLAUDE_SETTINGS_PATH = "~/.claude/settings.json";
|
|
3502
|
+
var THREADNOTE_HOOK_MARKER = "_threadnote";
|
|
3503
|
+
var THREADNOTE_HOOK_MARKER_VALUE = "managed";
|
|
3504
|
+
var HOOK_PRE_COMPACT_COMMAND = "threadnote pre-compact-hook";
|
|
3505
|
+
var HOOK_SESSION_START_COMMAND = "threadnote session-start-hook";
|
|
3506
|
+
var HOOK_AUTO_PRECOMPACT_TOPIC = "auto-precompact";
|
|
3501
3507
|
var DEFAULT_SEED_PATTERNS = [
|
|
3502
3508
|
"AGENTS.md",
|
|
3503
3509
|
"CLAUDE.md",
|
|
@@ -3515,6 +3521,16 @@ var DEFAULT_SEED_PATTERNS = [
|
|
|
3515
3521
|
"docs/**/*.md"
|
|
3516
3522
|
];
|
|
3517
3523
|
|
|
3524
|
+
// src/hooks.ts
|
|
3525
|
+
var import_promises5 = require("node:fs/promises");
|
|
3526
|
+
var import_node_path5 = require("node:path");
|
|
3527
|
+
|
|
3528
|
+
// src/mcp.ts
|
|
3529
|
+
var import_node_fs2 = require("node:fs");
|
|
3530
|
+
var import_promises2 = require("node:fs/promises");
|
|
3531
|
+
var import_node_path2 = require("node:path");
|
|
3532
|
+
var import_node_os2 = require("node:os");
|
|
3533
|
+
|
|
3518
3534
|
// src/utils.ts
|
|
3519
3535
|
var import_node_child_process = require("node:child_process");
|
|
3520
3536
|
var import_node_crypto = require("node:crypto");
|
|
@@ -3986,10 +4002,6 @@ function errorMessage(err) {
|
|
|
3986
4002
|
}
|
|
3987
4003
|
|
|
3988
4004
|
// src/mcp.ts
|
|
3989
|
-
var import_node_fs2 = require("node:fs");
|
|
3990
|
-
var import_promises2 = require("node:fs/promises");
|
|
3991
|
-
var import_node_path2 = require("node:path");
|
|
3992
|
-
var import_node_os2 = require("node:os");
|
|
3993
4005
|
async function runMcpInstall(config, agent, options) {
|
|
3994
4006
|
const name = options.name ?? OPENVIKING_MCP_NAME;
|
|
3995
4007
|
const url = options.url ?? `http://${config.host}:${config.port}/mcp`;
|
|
@@ -4470,50 +4482,6 @@ function existsSyncDirectory(path) {
|
|
|
4470
4482
|
}
|
|
4471
4483
|
}
|
|
4472
4484
|
|
|
4473
|
-
// src/runtime.ts
|
|
4474
|
-
var import_node_fs3 = require("node:fs");
|
|
4475
|
-
var import_node_os3 = require("node:os");
|
|
4476
|
-
var import_node_path3 = require("node:path");
|
|
4477
|
-
function getRuntimeConfig(program2, manifestOverride) {
|
|
4478
|
-
const options = program2.opts();
|
|
4479
|
-
const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
|
|
4480
|
-
const manifestPath = expandPath(
|
|
4481
|
-
manifestOverride ?? options.manifest ?? process.env.THREADNOTE_MANIFEST ?? defaultManifestPath(threadnoteHome)
|
|
4482
|
-
);
|
|
4483
|
-
return {
|
|
4484
|
-
account: process.env.THREADNOTE_ACCOUNT ?? DEFAULT_ACCOUNT,
|
|
4485
|
-
agentContextHome: threadnoteHome,
|
|
4486
|
-
agentId: process.env.THREADNOTE_AGENT_ID ?? DEFAULT_AGENT_ID,
|
|
4487
|
-
host: options.host ?? process.env.THREADNOTE_HOST ?? DEFAULT_HOST,
|
|
4488
|
-
manifestPath,
|
|
4489
|
-
openVikingVersion: process.env.THREADNOTE_OPENVIKING_VERSION ?? DEFAULT_OPENVIKING_VERSION,
|
|
4490
|
-
port: options.port ?? parsePort(process.env.THREADNOTE_PORT ?? String(DEFAULT_PORT)),
|
|
4491
|
-
user: process.env.THREADNOTE_USER ?? (0, import_node_os3.userInfo)().username
|
|
4492
|
-
};
|
|
4493
|
-
}
|
|
4494
|
-
function defaultManifestPath(agentContextHome) {
|
|
4495
|
-
const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
|
|
4496
|
-
return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
|
|
4497
|
-
}
|
|
4498
|
-
function builtInExampleManifestPath() {
|
|
4499
|
-
return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
|
|
4500
|
-
}
|
|
4501
|
-
function openVikingHealthUrl(config) {
|
|
4502
|
-
return `http://${config.host}:${config.port}/health`;
|
|
4503
|
-
}
|
|
4504
|
-
function openVikingLogPath(config) {
|
|
4505
|
-
return (0, import_node_path3.join)(config.agentContextHome, "logs", "server.log");
|
|
4506
|
-
}
|
|
4507
|
-
function openVikingServerArgs(config) {
|
|
4508
|
-
return ["--config", (0, import_node_path3.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
|
|
4509
|
-
}
|
|
4510
|
-
function withIdentity(config, args) {
|
|
4511
|
-
return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
|
|
4512
|
-
}
|
|
4513
|
-
function renderTemplate(template, config) {
|
|
4514
|
-
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);
|
|
4515
|
-
}
|
|
4516
|
-
|
|
4517
4485
|
// src/memory.ts
|
|
4518
4486
|
var import_promises4 = require("node:fs/promises");
|
|
4519
4487
|
var import_node_path4 = require("node:path");
|
|
@@ -7205,6 +7173,50 @@ function readStringArray(object, key) {
|
|
|
7205
7173
|
return value;
|
|
7206
7174
|
}
|
|
7207
7175
|
|
|
7176
|
+
// src/runtime.ts
|
|
7177
|
+
var import_node_fs3 = require("node:fs");
|
|
7178
|
+
var import_node_os3 = require("node:os");
|
|
7179
|
+
var import_node_path3 = require("node:path");
|
|
7180
|
+
function getRuntimeConfig(program2, manifestOverride) {
|
|
7181
|
+
const options = program2.opts();
|
|
7182
|
+
const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
|
|
7183
|
+
const manifestPath = expandPath(
|
|
7184
|
+
manifestOverride ?? options.manifest ?? process.env.THREADNOTE_MANIFEST ?? defaultManifestPath(threadnoteHome)
|
|
7185
|
+
);
|
|
7186
|
+
return {
|
|
7187
|
+
account: process.env.THREADNOTE_ACCOUNT ?? DEFAULT_ACCOUNT,
|
|
7188
|
+
agentContextHome: threadnoteHome,
|
|
7189
|
+
agentId: process.env.THREADNOTE_AGENT_ID ?? DEFAULT_AGENT_ID,
|
|
7190
|
+
host: options.host ?? process.env.THREADNOTE_HOST ?? DEFAULT_HOST,
|
|
7191
|
+
manifestPath,
|
|
7192
|
+
openVikingVersion: process.env.THREADNOTE_OPENVIKING_VERSION ?? DEFAULT_OPENVIKING_VERSION,
|
|
7193
|
+
port: options.port ?? parsePort(process.env.THREADNOTE_PORT ?? String(DEFAULT_PORT)),
|
|
7194
|
+
user: process.env.THREADNOTE_USER ?? (0, import_node_os3.userInfo)().username
|
|
7195
|
+
};
|
|
7196
|
+
}
|
|
7197
|
+
function defaultManifestPath(agentContextHome) {
|
|
7198
|
+
const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
|
|
7199
|
+
return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
|
|
7200
|
+
}
|
|
7201
|
+
function builtInExampleManifestPath() {
|
|
7202
|
+
return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
|
|
7203
|
+
}
|
|
7204
|
+
function openVikingHealthUrl(config) {
|
|
7205
|
+
return `http://${config.host}:${config.port}/health`;
|
|
7206
|
+
}
|
|
7207
|
+
function openVikingLogPath(config) {
|
|
7208
|
+
return (0, import_node_path3.join)(config.agentContextHome, "logs", "server.log");
|
|
7209
|
+
}
|
|
7210
|
+
function openVikingServerArgs(config) {
|
|
7211
|
+
return ["--config", (0, import_node_path3.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
|
|
7212
|
+
}
|
|
7213
|
+
function withIdentity(config, args) {
|
|
7214
|
+
return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
|
|
7215
|
+
}
|
|
7216
|
+
function renderTemplate(template, config) {
|
|
7217
|
+
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);
|
|
7218
|
+
}
|
|
7219
|
+
|
|
7208
7220
|
// src/memory.ts
|
|
7209
7221
|
function parseMemoryKind(value) {
|
|
7210
7222
|
if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
|
|
@@ -8083,9 +8095,201 @@ function formatBlock(value, emptyValue) {
|
|
|
8083
8095
|
return trimmed.split("\n").map((line) => `- ${line}`).join("\n");
|
|
8084
8096
|
}
|
|
8085
8097
|
|
|
8098
|
+
// src/hooks.ts
|
|
8099
|
+
var MANAGED_HOOKS = [
|
|
8100
|
+
{
|
|
8101
|
+
event: "PreCompact",
|
|
8102
|
+
command: HOOK_PRE_COMPACT_COMMAND,
|
|
8103
|
+
description: "Auto-store a handoff snapshot before context compaction so the next turn can recall it."
|
|
8104
|
+
},
|
|
8105
|
+
{
|
|
8106
|
+
event: "SessionStart",
|
|
8107
|
+
command: HOOK_SESSION_START_COMMAND,
|
|
8108
|
+
description: "Pre-load the latest handoff for the current repo into the new session context."
|
|
8109
|
+
}
|
|
8110
|
+
];
|
|
8111
|
+
async function runHooksInstall(config, agent, options) {
|
|
8112
|
+
const apply = options.apply === true && options.dryRun !== true;
|
|
8113
|
+
const remove = options.remove === true;
|
|
8114
|
+
switch (agent) {
|
|
8115
|
+
case "claude":
|
|
8116
|
+
await runClaudeHooksInstall({ apply, remove });
|
|
8117
|
+
return;
|
|
8118
|
+
case "codex":
|
|
8119
|
+
printCodexHooksNotice(remove);
|
|
8120
|
+
return;
|
|
8121
|
+
case "cursor":
|
|
8122
|
+
printNoHooksSupported("cursor", remove);
|
|
8123
|
+
return;
|
|
8124
|
+
case "copilot":
|
|
8125
|
+
printNoHooksSupported("copilot", remove);
|
|
8126
|
+
return;
|
|
8127
|
+
}
|
|
8128
|
+
}
|
|
8129
|
+
async function runClaudeHooksInstall(options) {
|
|
8130
|
+
const path = expandPath(CLAUDE_SETTINGS_PATH);
|
|
8131
|
+
const existingRaw = await exists(path) ? await (0, import_promises5.readFile)(path, "utf8") : "{}";
|
|
8132
|
+
const parsed = parseJsonConfigObject(existingRaw) ?? {};
|
|
8133
|
+
const next = options.remove ? withoutThreadnoteHooks(parsed) : withThreadnoteHooks(parsed);
|
|
8134
|
+
const before = JSON.stringify(parsed);
|
|
8135
|
+
const after = JSON.stringify(next);
|
|
8136
|
+
if (before === after) {
|
|
8137
|
+
console.log(`Claude hooks already ${options.remove ? "absent" : "managed"} in ${path}.`);
|
|
8138
|
+
return;
|
|
8139
|
+
}
|
|
8140
|
+
console.log(`${options.apply ? "Updating" : "Would update"} ${path}:`);
|
|
8141
|
+
for (const entry of MANAGED_HOOKS) {
|
|
8142
|
+
console.log(` ${options.remove ? "-" : "+"} ${entry.event}: ${entry.command}`);
|
|
8143
|
+
}
|
|
8144
|
+
if (!options.apply) {
|
|
8145
|
+
console.log("\nRe-run with --apply to actually modify the file.");
|
|
8146
|
+
return;
|
|
8147
|
+
}
|
|
8148
|
+
await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
8149
|
+
const serialized = `${JSON.stringify(next, void 0, 2)}
|
|
8150
|
+
`;
|
|
8151
|
+
await (0, import_promises5.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
|
|
8152
|
+
await (0, import_promises5.chmod)(path, 384);
|
|
8153
|
+
console.log(`${options.remove ? "Removed" : "Installed"} threadnote-managed Claude hooks.`);
|
|
8154
|
+
}
|
|
8155
|
+
function withThreadnoteHooks(input2) {
|
|
8156
|
+
const hooks = ensureMutableObject(input2.hooks);
|
|
8157
|
+
for (const entry of MANAGED_HOOKS) {
|
|
8158
|
+
const list = ensureMutableArray(hooks[entry.event]).filter((item) => !isManagedThreadnoteEntry(item));
|
|
8159
|
+
list.push({
|
|
8160
|
+
[THREADNOTE_HOOK_MARKER]: THREADNOTE_HOOK_MARKER_VALUE,
|
|
8161
|
+
matcher: "",
|
|
8162
|
+
hooks: [{ type: "command", command: entry.command }]
|
|
8163
|
+
});
|
|
8164
|
+
hooks[entry.event] = list;
|
|
8165
|
+
}
|
|
8166
|
+
return { ...input2, hooks };
|
|
8167
|
+
}
|
|
8168
|
+
function withoutThreadnoteHooks(input2) {
|
|
8169
|
+
if (!isJsonObject(input2.hooks)) {
|
|
8170
|
+
return input2;
|
|
8171
|
+
}
|
|
8172
|
+
const hooks = {};
|
|
8173
|
+
for (const [event, value] of Object.entries(input2.hooks)) {
|
|
8174
|
+
if (!Array.isArray(value)) {
|
|
8175
|
+
hooks[event] = value;
|
|
8176
|
+
continue;
|
|
8177
|
+
}
|
|
8178
|
+
const filtered = value.filter((item) => !isManagedThreadnoteEntry(item));
|
|
8179
|
+
if (filtered.length > 0) {
|
|
8180
|
+
hooks[event] = filtered;
|
|
8181
|
+
}
|
|
8182
|
+
}
|
|
8183
|
+
if (Object.keys(hooks).length === 0) {
|
|
8184
|
+
const next = { ...input2 };
|
|
8185
|
+
delete next.hooks;
|
|
8186
|
+
return next;
|
|
8187
|
+
}
|
|
8188
|
+
return { ...input2, hooks };
|
|
8189
|
+
}
|
|
8190
|
+
function isManagedThreadnoteEntry(value) {
|
|
8191
|
+
return isJsonObject(value) && value[THREADNOTE_HOOK_MARKER] === THREADNOTE_HOOK_MARKER_VALUE;
|
|
8192
|
+
}
|
|
8193
|
+
function ensureMutableObject(value) {
|
|
8194
|
+
return isJsonObject(value) ? { ...value } : {};
|
|
8195
|
+
}
|
|
8196
|
+
function ensureMutableArray(value) {
|
|
8197
|
+
return Array.isArray(value) ? [...value] : [];
|
|
8198
|
+
}
|
|
8199
|
+
function printCodexHooksNotice(remove) {
|
|
8200
|
+
if (remove) {
|
|
8201
|
+
console.log("Codex CLI does not expose a managed hook surface today, so there is nothing to remove.");
|
|
8202
|
+
return;
|
|
8203
|
+
}
|
|
8204
|
+
console.log(
|
|
8205
|
+
[
|
|
8206
|
+
"Codex CLI does not currently expose lifecycle hooks (no PreCompact or SessionStart analog).",
|
|
8207
|
+
"Threadnote already installs Codex user instructions at ~/.codex/AGENTS.md; that remains the active guidance surface.",
|
|
8208
|
+
"If a future Codex release adds hook events, threadnote will pick them up via `install-hooks codex`."
|
|
8209
|
+
].join("\n")
|
|
8210
|
+
);
|
|
8211
|
+
}
|
|
8212
|
+
function printNoHooksSupported(agent, remove) {
|
|
8213
|
+
if (remove) {
|
|
8214
|
+
console.log(`${agent} does not expose a hook surface; nothing to remove.`);
|
|
8215
|
+
return;
|
|
8216
|
+
}
|
|
8217
|
+
console.log(
|
|
8218
|
+
[
|
|
8219
|
+
`${agent} does not expose agent-mode hooks today.`,
|
|
8220
|
+
"Threadnote already installs user-level instructions for this agent; that remains the active guidance surface.",
|
|
8221
|
+
"Hooks support will be added if the agent gains a hook surface upstream."
|
|
8222
|
+
].join("\n")
|
|
8223
|
+
);
|
|
8224
|
+
}
|
|
8225
|
+
async function hasManagedClaudeHooks() {
|
|
8226
|
+
const path = expandPath(CLAUDE_SETTINGS_PATH);
|
|
8227
|
+
if (!await exists(path)) {
|
|
8228
|
+
return false;
|
|
8229
|
+
}
|
|
8230
|
+
const raw = await (0, import_promises5.readFile)(path, "utf8");
|
|
8231
|
+
const parsed = parseJsonConfigObject(raw);
|
|
8232
|
+
if (!parsed || !isJsonObject(parsed.hooks)) {
|
|
8233
|
+
return false;
|
|
8234
|
+
}
|
|
8235
|
+
for (const value of Object.values(parsed.hooks)) {
|
|
8236
|
+
if (!Array.isArray(value)) {
|
|
8237
|
+
continue;
|
|
8238
|
+
}
|
|
8239
|
+
if (value.some((item) => isManagedThreadnoteEntry(item))) {
|
|
8240
|
+
return true;
|
|
8241
|
+
}
|
|
8242
|
+
}
|
|
8243
|
+
return false;
|
|
8244
|
+
}
|
|
8245
|
+
async function runPreCompactHook(config, options = {}) {
|
|
8246
|
+
try {
|
|
8247
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
8248
|
+
const project = repoRoot ? (0, import_node_path5.basename)(repoRoot) : "general";
|
|
8249
|
+
await runHandoff(config, {
|
|
8250
|
+
blockers: "- none recorded",
|
|
8251
|
+
dryRun: options.dryRun === true,
|
|
8252
|
+
nextStep: "Continue from this auto-snapshot. A manual `threadnote handoff` will produce a richer write-up if you have more context.",
|
|
8253
|
+
project,
|
|
8254
|
+
sourceAgentClient: "claude",
|
|
8255
|
+
task: "Auto-snapshot captured at Claude PreCompact (deterministic safety net before context compaction).",
|
|
8256
|
+
tests: "- not recorded (auto-snapshot)",
|
|
8257
|
+
topic: HOOK_AUTO_PRECOMPACT_TOPIC
|
|
8258
|
+
});
|
|
8259
|
+
} catch (err) {
|
|
8260
|
+
process.stderr.write(
|
|
8261
|
+
`threadnote pre-compact-hook: snapshot skipped (${err instanceof Error ? err.message : String(err)})
|
|
8262
|
+
`
|
|
8263
|
+
);
|
|
8264
|
+
}
|
|
8265
|
+
}
|
|
8266
|
+
async function runSessionStartHook(config, options = {}) {
|
|
8267
|
+
try {
|
|
8268
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
8269
|
+
if (!repoRoot) {
|
|
8270
|
+
return;
|
|
8271
|
+
}
|
|
8272
|
+
const project = (0, import_node_path5.basename)(repoRoot);
|
|
8273
|
+
process.stdout.write(`## Threadnote \u2014 latest context for ${project}
|
|
8274
|
+
|
|
8275
|
+
`);
|
|
8276
|
+
await runRecall(config, {
|
|
8277
|
+
dryRun: options.dryRun === true,
|
|
8278
|
+
inferScope: true,
|
|
8279
|
+
nodeLimit: "5",
|
|
8280
|
+
query: `${project} latest handoff durable feature memory`
|
|
8281
|
+
});
|
|
8282
|
+
} catch (err) {
|
|
8283
|
+
process.stderr.write(
|
|
8284
|
+
`threadnote session-start-hook: recall skipped (${err instanceof Error ? err.message : String(err)})
|
|
8285
|
+
`
|
|
8286
|
+
);
|
|
8287
|
+
}
|
|
8288
|
+
}
|
|
8289
|
+
|
|
8086
8290
|
// src/seeding.ts
|
|
8087
|
-
var
|
|
8088
|
-
var
|
|
8291
|
+
var import_promises6 = require("node:fs/promises");
|
|
8292
|
+
var import_node_path6 = require("node:path");
|
|
8089
8293
|
async function runSeed(config, options) {
|
|
8090
8294
|
const manifest = await readSeedManifest(config.manifestPath);
|
|
8091
8295
|
const ignorePatterns = await loadIgnorePatterns();
|
|
@@ -8114,7 +8318,7 @@ async function runSeed(config, options) {
|
|
|
8114
8318
|
}
|
|
8115
8319
|
async function runInitManifest(config, options) {
|
|
8116
8320
|
const manifestPath = expandPath(
|
|
8117
|
-
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0,
|
|
8321
|
+
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path6.join)(config.agentContextHome, USER_MANIFEST_NAME)
|
|
8118
8322
|
);
|
|
8119
8323
|
const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
|
|
8120
8324
|
const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
|
|
@@ -8157,9 +8361,9 @@ async function runInitManifest(config, options) {
|
|
|
8157
8361
|
console.log(output2.trimEnd());
|
|
8158
8362
|
return;
|
|
8159
8363
|
}
|
|
8160
|
-
await ensureDirectory((0,
|
|
8161
|
-
await (0,
|
|
8162
|
-
await (0,
|
|
8364
|
+
await ensureDirectory((0, import_node_path6.dirname)(manifestPath), false);
|
|
8365
|
+
await (0, import_promises6.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
8366
|
+
await (0, import_promises6.chmod)(manifestPath, 384);
|
|
8163
8367
|
console.log(`Wrote manifest: ${manifestPath}`);
|
|
8164
8368
|
console.log("Seed with:");
|
|
8165
8369
|
console.log(" threadnote seed --dry-run");
|
|
@@ -8189,13 +8393,13 @@ async function resolveRepoRoot(repoInput) {
|
|
|
8189
8393
|
async function projectIdentity(path) {
|
|
8190
8394
|
const expanded = expandPath(path);
|
|
8191
8395
|
try {
|
|
8192
|
-
return await (0,
|
|
8396
|
+
return await (0, import_promises6.realpath)(expanded);
|
|
8193
8397
|
} catch (_err) {
|
|
8194
8398
|
return expanded;
|
|
8195
8399
|
}
|
|
8196
8400
|
}
|
|
8197
8401
|
function projectManifestForRepo(repoRoot, existingProjects) {
|
|
8198
|
-
const baseName = uriSegment((0,
|
|
8402
|
+
const baseName = uriSegment((0, import_node_path6.basename)(repoRoot));
|
|
8199
8403
|
const usedNames = new Set(existingProjects.map((project) => project.name));
|
|
8200
8404
|
const usedUris = new Set(existingProjects.map((project) => project.uri));
|
|
8201
8405
|
let name = baseName;
|
|
@@ -8217,7 +8421,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
|
8217
8421
|
for (const pattern of project.seed) {
|
|
8218
8422
|
const files = await resolveProjectPattern(projectRoot, pattern);
|
|
8219
8423
|
for (const filePath of files) {
|
|
8220
|
-
const relativePath = toPosixPath((0,
|
|
8424
|
+
const relativePath = toPosixPath((0, import_node_path6.relative)(projectRoot, filePath));
|
|
8221
8425
|
if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
|
|
8222
8426
|
continue;
|
|
8223
8427
|
}
|
|
@@ -8235,20 +8439,20 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
|
8235
8439
|
async function resolveProjectPattern(projectRoot, pattern) {
|
|
8236
8440
|
const normalizedPattern = toPosixPath(pattern);
|
|
8237
8441
|
if (!hasGlob(normalizedPattern)) {
|
|
8238
|
-
const filePath = (0,
|
|
8442
|
+
const filePath = (0, import_node_path6.join)(projectRoot, normalizedPattern);
|
|
8239
8443
|
return await isFile(filePath) ? [filePath] : [];
|
|
8240
8444
|
}
|
|
8241
8445
|
const globBase = getGlobBase(normalizedPattern);
|
|
8242
|
-
const basePath = (0,
|
|
8446
|
+
const basePath = (0, import_node_path6.join)(projectRoot, globBase);
|
|
8243
8447
|
if (!await exists(basePath)) {
|
|
8244
8448
|
return [];
|
|
8245
8449
|
}
|
|
8246
8450
|
const regex = globToRegExp(normalizedPattern);
|
|
8247
8451
|
const files = await walkFiles(basePath);
|
|
8248
|
-
return files.filter((filePath) => regex.test(toPosixPath((0,
|
|
8452
|
+
return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path6.relative)(projectRoot, filePath))));
|
|
8249
8453
|
}
|
|
8250
8454
|
async function prepareSeedFile(config, candidate, dryRun) {
|
|
8251
|
-
const content = await (0,
|
|
8455
|
+
const content = await (0, import_promises6.readFile)(candidate.filePath, "utf8");
|
|
8252
8456
|
const redactedContent = shouldRedactPath(candidate.relativePath) ? redactContent(candidate.relativePath, content) : content;
|
|
8253
8457
|
const secretMatches = detectSecretMatches(redactedContent);
|
|
8254
8458
|
if (secretMatches.length > 0) {
|
|
@@ -8260,14 +8464,14 @@ async function prepareSeedFile(config, candidate, dryRun) {
|
|
|
8260
8464
|
if (redactedContent === content) {
|
|
8261
8465
|
return candidate.filePath;
|
|
8262
8466
|
}
|
|
8263
|
-
const redactedPath = (0,
|
|
8467
|
+
const redactedPath = (0, import_node_path6.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
|
|
8264
8468
|
if (dryRun) {
|
|
8265
8469
|
console.log(`Would write redacted copy: ${redactedPath}`);
|
|
8266
8470
|
return redactedPath;
|
|
8267
8471
|
}
|
|
8268
|
-
await ensureDirectory((0,
|
|
8269
|
-
await (0,
|
|
8270
|
-
await (0,
|
|
8472
|
+
await ensureDirectory((0, import_node_path6.dirname)(redactedPath), false);
|
|
8473
|
+
await (0, import_promises6.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
|
|
8474
|
+
await (0, import_promises6.chmod)(redactedPath, 384);
|
|
8271
8475
|
return redactedPath;
|
|
8272
8476
|
}
|
|
8273
8477
|
async function collectSkillCandidates(config) {
|
|
@@ -8292,7 +8496,7 @@ async function collectSkillCandidates(config) {
|
|
|
8292
8496
|
for (const source of sources) {
|
|
8293
8497
|
const files = await resolveAbsolutePattern(expandPath(source.pattern));
|
|
8294
8498
|
for (const filePath of files) {
|
|
8295
|
-
const content = await (0,
|
|
8499
|
+
const content = await (0, import_promises6.readFile)(filePath, "utf8");
|
|
8296
8500
|
const matches = detectSecretMatches(content);
|
|
8297
8501
|
if (matches.length > 0) {
|
|
8298
8502
|
console.log(`SKIP skill with possible secret: ${filePath}`);
|
|
@@ -8323,10 +8527,10 @@ async function resolveAbsolutePattern(pattern) {
|
|
|
8323
8527
|
return files.filter((filePath) => regex.test(toPosixPath(filePath)));
|
|
8324
8528
|
}
|
|
8325
8529
|
function skillResourceUri(skill) {
|
|
8326
|
-
return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0,
|
|
8530
|
+
return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path6.basename)((0, import_node_path6.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
|
|
8327
8531
|
}
|
|
8328
8532
|
async function loadIgnorePatterns() {
|
|
8329
|
-
const raw = await (0,
|
|
8533
|
+
const raw = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), ".threadnoteignore"), "utf8");
|
|
8330
8534
|
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
8331
8535
|
}
|
|
8332
8536
|
function matchesIgnore(relativePath, patterns) {
|
|
@@ -8397,9 +8601,9 @@ function detectSecretMatches(content) {
|
|
|
8397
8601
|
}
|
|
8398
8602
|
|
|
8399
8603
|
// src/share.ts
|
|
8400
|
-
var
|
|
8604
|
+
var import_promises7 = require("node:fs/promises");
|
|
8401
8605
|
var import_node_os4 = require("node:os");
|
|
8402
|
-
var
|
|
8606
|
+
var import_node_path7 = require("node:path");
|
|
8403
8607
|
var TEAMS_FILE_VERSION = 1;
|
|
8404
8608
|
var SHARED_SEGMENT = "shared";
|
|
8405
8609
|
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
@@ -8437,8 +8641,8 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
8437
8641
|
if (await exists(gitdir)) {
|
|
8438
8642
|
throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
|
|
8439
8643
|
}
|
|
8440
|
-
await ensureDirectory((0,
|
|
8441
|
-
await ensureDirectory((0,
|
|
8644
|
+
await ensureDirectory((0, import_node_path7.dirname)(worktree), dryRun);
|
|
8645
|
+
await ensureDirectory((0, import_node_path7.dirname)(gitdir), dryRun);
|
|
8442
8646
|
const git = await requiredExecutable("git");
|
|
8443
8647
|
await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
|
|
8444
8648
|
const newConfig = {
|
|
@@ -8469,7 +8673,7 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
8469
8673
|
var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
|
|
8470
8674
|
var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
|
|
8471
8675
|
async function ensureSharedGitignore(worktree, git, push) {
|
|
8472
|
-
const gitignorePath = (0,
|
|
8676
|
+
const gitignorePath = (0, import_node_path7.join)(worktree, ".gitignore");
|
|
8473
8677
|
const existing = await readFileIfExists(gitignorePath) ?? "";
|
|
8474
8678
|
const lines = existing.split("\n").map((line) => line.trim());
|
|
8475
8679
|
const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
@@ -8488,7 +8692,7 @@ async function ensureSharedGitignore(worktree, git, push) {
|
|
|
8488
8692
|
segments.push(SHARED_GITIGNORE_HEADER, "\n");
|
|
8489
8693
|
}
|
|
8490
8694
|
segments.push(missingPatterns.join("\n"), "\n");
|
|
8491
|
-
await (0,
|
|
8695
|
+
await (0, import_promises7.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
|
|
8492
8696
|
console.log(`Added ${missingPatterns.join(", ")} to ${portablePath(gitignorePath)}`);
|
|
8493
8697
|
await maybeRun(false, git, ["-C", worktree, "add", ".gitignore"]);
|
|
8494
8698
|
const commitResult = await runCommand(
|
|
@@ -8553,7 +8757,7 @@ async function runShareSync(config, options) {
|
|
|
8553
8757
|
if (dryRun) {
|
|
8554
8758
|
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
8555
8759
|
} else if (pullResult && pullResult.exitCode !== 0) {
|
|
8556
|
-
if (await exists((0,
|
|
8760
|
+
if (await exists((0, import_node_path7.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path7.join)(team.config.gitdir, "rebase-apply"))) {
|
|
8557
8761
|
throw new Error(
|
|
8558
8762
|
`git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
|
|
8559
8763
|
Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
|
|
@@ -8704,10 +8908,10 @@ function normalizeTeamName(input2) {
|
|
|
8704
8908
|
return candidate;
|
|
8705
8909
|
}
|
|
8706
8910
|
function teamsFilePath(config) {
|
|
8707
|
-
return (0,
|
|
8911
|
+
return (0, import_node_path7.join)(config.agentContextHome, "share", "teams.json");
|
|
8708
8912
|
}
|
|
8709
8913
|
function teamWorktreePath(config, team) {
|
|
8710
|
-
return (0,
|
|
8914
|
+
return (0, import_node_path7.join)(
|
|
8711
8915
|
config.agentContextHome,
|
|
8712
8916
|
"data",
|
|
8713
8917
|
"viking",
|
|
@@ -8720,7 +8924,7 @@ function teamWorktreePath(config, team) {
|
|
|
8720
8924
|
);
|
|
8721
8925
|
}
|
|
8722
8926
|
function teamGitdirPath(config, team) {
|
|
8723
|
-
return (0,
|
|
8927
|
+
return (0, import_node_path7.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
8724
8928
|
}
|
|
8725
8929
|
async function readTeamsFile(config) {
|
|
8726
8930
|
const path = teamsFilePath(config);
|
|
@@ -8763,13 +8967,13 @@ async function readTeamsFile(config) {
|
|
|
8763
8967
|
}
|
|
8764
8968
|
async function writeTeamsFile(config, contents) {
|
|
8765
8969
|
const path = teamsFilePath(config);
|
|
8766
|
-
await (0,
|
|
8970
|
+
await (0, import_promises7.mkdir)((0, import_node_path7.dirname)(path), { recursive: true });
|
|
8767
8971
|
const serializable = {
|
|
8768
8972
|
defaultTeam: contents.defaultTeam,
|
|
8769
8973
|
teams: contents.teams,
|
|
8770
8974
|
version: contents.version
|
|
8771
8975
|
};
|
|
8772
|
-
await (0,
|
|
8976
|
+
await (0, import_promises7.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
|
|
8773
8977
|
`, { encoding: "utf8", mode: 384 });
|
|
8774
8978
|
}
|
|
8775
8979
|
async function resolveTeam(config, requested) {
|
|
@@ -8799,7 +9003,7 @@ async function assertWorktreeUsable(worktree) {
|
|
|
8799
9003
|
if (!await isDirectory(worktree)) {
|
|
8800
9004
|
throw new Error(`Cannot use ${worktree} as a worktree: not a directory.`);
|
|
8801
9005
|
}
|
|
8802
|
-
const entries = await (0,
|
|
9006
|
+
const entries = await (0, import_promises7.readdir)(worktree);
|
|
8803
9007
|
if (entries.length > 0) {
|
|
8804
9008
|
const preview = entries.slice(0, 5).join(", ");
|
|
8805
9009
|
const suffix = entries.length > 5 ? `, +${entries.length - 5} more` : "";
|
|
@@ -8823,7 +9027,7 @@ async function walkMemoryFiles(root) {
|
|
|
8823
9027
|
async function visit(path, depth) {
|
|
8824
9028
|
let entries;
|
|
8825
9029
|
try {
|
|
8826
|
-
entries = await (0,
|
|
9030
|
+
entries = await (0, import_promises7.readdir)(path, { withFileTypes: true });
|
|
8827
9031
|
} catch (err) {
|
|
8828
9032
|
console.warn(`Skipping ${path} during shared-tree walk: ${err instanceof Error ? err.message : String(err)}`);
|
|
8829
9033
|
return;
|
|
@@ -8832,7 +9036,7 @@ async function walkMemoryFiles(root) {
|
|
|
8832
9036
|
if (entry.name === ".git") {
|
|
8833
9037
|
continue;
|
|
8834
9038
|
}
|
|
8835
|
-
const full = (0,
|
|
9039
|
+
const full = (0, import_node_path7.join)(path, entry.name);
|
|
8836
9040
|
if (entry.isDirectory()) {
|
|
8837
9041
|
if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
|
|
8838
9042
|
continue;
|
|
@@ -8856,7 +9060,7 @@ async function walkMemoryFiles(root) {
|
|
|
8856
9060
|
return out;
|
|
8857
9061
|
}
|
|
8858
9062
|
function workfileToVikingUri(config, team, filePath) {
|
|
8859
|
-
const rel = (0,
|
|
9063
|
+
const rel = (0, import_node_path7.relative)(team.worktree, filePath).split(import_node_path7.sep).join("/");
|
|
8860
9064
|
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
8861
9065
|
}
|
|
8862
9066
|
function isInSharedNamespace(config, uri) {
|
|
@@ -8950,13 +9154,13 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun) {
|
|
|
8950
9154
|
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8951
9155
|
return;
|
|
8952
9156
|
}
|
|
8953
|
-
const stagingDir = await (0,
|
|
8954
|
-
const tempPath = (0,
|
|
9157
|
+
const stagingDir = await (0, import_promises7.mkdtemp)((0, import_node_path7.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
|
|
9158
|
+
const tempPath = (0, import_node_path7.join)(stagingDir, "body.txt");
|
|
8955
9159
|
try {
|
|
8956
|
-
await (0,
|
|
9160
|
+
await (0, import_promises7.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
8957
9161
|
await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode);
|
|
8958
9162
|
} finally {
|
|
8959
|
-
await (0,
|
|
9163
|
+
await (0, import_promises7.rm)(stagingDir, { force: true, recursive: true });
|
|
8960
9164
|
}
|
|
8961
9165
|
}
|
|
8962
9166
|
async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode) {
|
|
@@ -9014,7 +9218,7 @@ ${stdout}`.toLowerCase();
|
|
|
9014
9218
|
return output2.includes("resource is busy") || output2.includes("resource is being processed") || output2.includes("network error") || output2.includes("error sending request") || output2.includes("http request failed") || output2.includes("connection refused") || output2.includes("connection reset") || output2.includes("timed out");
|
|
9015
9219
|
}
|
|
9016
9220
|
async function ingestSingleFile(ov, config, uri, filePath, initialMode) {
|
|
9017
|
-
const content = await (0,
|
|
9221
|
+
const content = await (0, import_promises7.readFile)(filePath, "utf8");
|
|
9018
9222
|
await writeMemoryFile(config, ov, uri, content, initialMode, false);
|
|
9019
9223
|
}
|
|
9020
9224
|
async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
|
|
@@ -9056,7 +9260,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
|
9056
9260
|
if (!relative3) {
|
|
9057
9261
|
return;
|
|
9058
9262
|
}
|
|
9059
|
-
await (0,
|
|
9263
|
+
await (0, import_promises7.rm)((0, import_node_path7.join)(worktree, relative3), { force: true });
|
|
9060
9264
|
}
|
|
9061
9265
|
async function removeMemoryUri(config, ov, uri, dryRun) {
|
|
9062
9266
|
const args = withIdentity(config, ["rm", uri]);
|
|
@@ -9113,8 +9317,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
9113
9317
|
const oldRel = entries[index + 1];
|
|
9114
9318
|
const newRel = entries[index + 2];
|
|
9115
9319
|
if (oldRel && newRel) {
|
|
9116
|
-
changes.push({ path: (0,
|
|
9117
|
-
changes.push({ path: (0,
|
|
9320
|
+
changes.push({ path: (0, import_node_path7.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
9321
|
+
changes.push({ path: (0, import_node_path7.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
9118
9322
|
}
|
|
9119
9323
|
index += 3;
|
|
9120
9324
|
continue;
|
|
@@ -9122,7 +9326,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
9122
9326
|
const rel = entries[index + 1];
|
|
9123
9327
|
if (rel) {
|
|
9124
9328
|
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
9125
|
-
changes.push({ path: (0,
|
|
9329
|
+
changes.push({ path: (0, import_node_path7.join)(worktree, rel), relativePath: rel, status });
|
|
9126
9330
|
}
|
|
9127
9331
|
index += 2;
|
|
9128
9332
|
}
|
|
@@ -9160,16 +9364,18 @@ async function applyChangesToOpenViking(config, team, changes) {
|
|
|
9160
9364
|
// src/lifecycle.ts
|
|
9161
9365
|
var import_node_child_process2 = require("node:child_process");
|
|
9162
9366
|
var import_node_fs5 = require("node:fs");
|
|
9163
|
-
var
|
|
9367
|
+
var import_promises10 = require("node:fs/promises");
|
|
9164
9368
|
var import_node_os6 = require("node:os");
|
|
9165
|
-
var
|
|
9369
|
+
var import_node_path9 = require("node:path");
|
|
9370
|
+
var import_node_process2 = require("node:process");
|
|
9371
|
+
var import_promises11 = require("node:readline/promises");
|
|
9166
9372
|
|
|
9167
9373
|
// src/update.ts
|
|
9168
9374
|
var import_node_fs4 = require("node:fs");
|
|
9169
|
-
var
|
|
9375
|
+
var import_promises8 = require("node:fs/promises");
|
|
9170
9376
|
var import_node_os5 = require("node:os");
|
|
9171
|
-
var
|
|
9172
|
-
var
|
|
9377
|
+
var import_node_path8 = require("node:path");
|
|
9378
|
+
var import_promises9 = require("node:readline/promises");
|
|
9173
9379
|
var import_node_process = require("node:process");
|
|
9174
9380
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
9175
9381
|
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
@@ -9317,7 +9523,7 @@ async function getUpdateInfo(config, options) {
|
|
|
9317
9523
|
};
|
|
9318
9524
|
}
|
|
9319
9525
|
async function currentPackageVersion() {
|
|
9320
|
-
const rawPackage = await (0,
|
|
9526
|
+
const rawPackage = await (0, import_promises8.readFile)((0, import_node_path8.join)(toolRoot(), "package.json"), "utf8");
|
|
9321
9527
|
const parsed = JSON.parse(rawPackage);
|
|
9322
9528
|
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
9323
9529
|
throw new Error("Could not read current threadnote package version.");
|
|
@@ -9367,11 +9573,11 @@ async function readFreshCache(config, registry) {
|
|
|
9367
9573
|
}
|
|
9368
9574
|
async function writeUpdateCache(config, cache) {
|
|
9369
9575
|
await ensureDirectory(config.agentContextHome, false);
|
|
9370
|
-
await (0,
|
|
9576
|
+
await (0, import_promises8.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
|
|
9371
9577
|
`, { encoding: "utf8", mode: 384 });
|
|
9372
9578
|
}
|
|
9373
9579
|
function updateCachePath(config) {
|
|
9374
|
-
return (0,
|
|
9580
|
+
return (0, import_node_path8.join)(config.agentContextHome, "update-check.json");
|
|
9375
9581
|
}
|
|
9376
9582
|
async function runApplicablePostUpdateMigrations(config, options) {
|
|
9377
9583
|
const state = await readPostUpdateState(config);
|
|
@@ -9435,7 +9641,7 @@ async function applicablePostUpdateMigrations(config, options) {
|
|
|
9435
9641
|
return applicable;
|
|
9436
9642
|
}
|
|
9437
9643
|
async function readPostUpdateMigrations() {
|
|
9438
|
-
const raw = await readFileIfExists((0,
|
|
9644
|
+
const raw = await readFileIfExists((0, import_node_path8.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
|
|
9439
9645
|
if (!raw) {
|
|
9440
9646
|
return [];
|
|
9441
9647
|
}
|
|
@@ -9474,7 +9680,7 @@ function printPostUpdateMigration(migration) {
|
|
|
9474
9680
|
}
|
|
9475
9681
|
}
|
|
9476
9682
|
async function confirmPostUpdateMigration(prompt) {
|
|
9477
|
-
const readline = (0,
|
|
9683
|
+
const readline = (0, import_promises9.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
9478
9684
|
try {
|
|
9479
9685
|
const answer = (await readline.question(prompt)).trim().toLowerCase();
|
|
9480
9686
|
return answer === "y" || answer === "yes";
|
|
@@ -9506,11 +9712,11 @@ async function readPostUpdateState(config) {
|
|
|
9506
9712
|
}
|
|
9507
9713
|
async function writePostUpdateState(config, state) {
|
|
9508
9714
|
await ensureDirectory(config.agentContextHome, false);
|
|
9509
|
-
await (0,
|
|
9715
|
+
await (0, import_promises8.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
|
|
9510
9716
|
`, { encoding: "utf8", mode: 384 });
|
|
9511
9717
|
}
|
|
9512
9718
|
function postUpdateStatePath(config) {
|
|
9513
|
-
return (0,
|
|
9719
|
+
return (0, import_node_path8.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
|
|
9514
9720
|
}
|
|
9515
9721
|
async function resolveUpdateRuntime(runtime) {
|
|
9516
9722
|
if (runtime !== "auto") {
|
|
@@ -9540,18 +9746,18 @@ async function runtimeThreadnoteBin(runtime) {
|
|
|
9540
9746
|
if (runtime === "npm") {
|
|
9541
9747
|
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
9542
9748
|
const prefix = result.stdout.trim();
|
|
9543
|
-
return prefix ? (0,
|
|
9749
|
+
return prefix ? (0, import_node_path8.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
9544
9750
|
}
|
|
9545
9751
|
if (runtime === "bun") {
|
|
9546
9752
|
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
9547
9753
|
const binDir = result.stdout.trim();
|
|
9548
|
-
return binDir ? (0,
|
|
9754
|
+
return binDir ? (0, import_node_path8.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
9549
9755
|
}
|
|
9550
|
-
return (0,
|
|
9756
|
+
return (0, import_node_path8.join)(process.env.DENO_INSTALL ?? (0, import_node_path8.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
|
|
9551
9757
|
}
|
|
9552
9758
|
async function isExecutable(path) {
|
|
9553
9759
|
try {
|
|
9554
|
-
await (0,
|
|
9760
|
+
await (0, import_promises8.access)(path, import_node_fs4.constants.X_OK);
|
|
9555
9761
|
return true;
|
|
9556
9762
|
} catch (_err) {
|
|
9557
9763
|
return false;
|
|
@@ -9642,9 +9848,9 @@ async function runDoctor(config, options) {
|
|
|
9642
9848
|
checks.push(await commandShimCheck());
|
|
9643
9849
|
checks.push(...await userAgentInstructionsChecks());
|
|
9644
9850
|
checks.push(await manifestCheck(config.manifestPath));
|
|
9645
|
-
checks.push(await fileCheck((0,
|
|
9646
|
-
checks.push(await fileCheck((0,
|
|
9647
|
-
checks.push(await fileCheck((0,
|
|
9851
|
+
checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
|
|
9852
|
+
checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
|
|
9853
|
+
checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
|
|
9648
9854
|
checks.push(await healthCheck(config));
|
|
9649
9855
|
for (const check of checks) {
|
|
9650
9856
|
console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
|
|
@@ -9661,9 +9867,9 @@ async function runInstall(config, options) {
|
|
|
9661
9867
|
const repairInvalidConfigs = options.repairInvalidConfigs === true;
|
|
9662
9868
|
const dryRun = options.dryRun === true;
|
|
9663
9869
|
await ensureDirectory(config.agentContextHome, dryRun);
|
|
9664
|
-
await ensureDirectory((0,
|
|
9665
|
-
await ensureDirectory((0,
|
|
9666
|
-
await ensureDirectory((0,
|
|
9870
|
+
await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "logs"), dryRun);
|
|
9871
|
+
await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "redacted"), dryRun);
|
|
9872
|
+
await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "mcp"), dryRun);
|
|
9667
9873
|
await installCommandShim(dryRun);
|
|
9668
9874
|
await installUserAgentInstructions(dryRun);
|
|
9669
9875
|
const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
@@ -9689,17 +9895,17 @@ async function runInstall(config, options) {
|
|
|
9689
9895
|
}
|
|
9690
9896
|
await writeTemplateIfMissing({
|
|
9691
9897
|
config,
|
|
9692
|
-
destinationPath: (0,
|
|
9898
|
+
destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ov.conf"),
|
|
9693
9899
|
dryRun,
|
|
9694
9900
|
shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
9695
|
-
templatePath: (0,
|
|
9901
|
+
templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json")
|
|
9696
9902
|
});
|
|
9697
9903
|
await writeTemplateIfMissing({
|
|
9698
9904
|
config,
|
|
9699
|
-
destinationPath: (0,
|
|
9905
|
+
destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ovcli.conf"),
|
|
9700
9906
|
dryRun,
|
|
9701
9907
|
shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
9702
|
-
templatePath: (0,
|
|
9908
|
+
templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json")
|
|
9703
9909
|
});
|
|
9704
9910
|
if (options.start !== false) {
|
|
9705
9911
|
const healthy = await repairServerHealth(config, dryRun);
|
|
@@ -9736,6 +9942,10 @@ async function runRepair(config, options) {
|
|
|
9736
9942
|
await runMcpInstall(config, client, { apply: !dryRun, name: OPENVIKING_MCP_NAME });
|
|
9737
9943
|
}
|
|
9738
9944
|
}
|
|
9945
|
+
if (await hasManagedClaudeHooks()) {
|
|
9946
|
+
console.log("\nRepairing claude hooks (re-asserting threadnote-managed entries).");
|
|
9947
|
+
await runHooksInstall(config, "claude", { apply: !dryRun, dryRun });
|
|
9948
|
+
}
|
|
9739
9949
|
console.log("\nPost-repair doctor:");
|
|
9740
9950
|
await runDoctor(config, { dryRun, strict: false });
|
|
9741
9951
|
if (options.postUpdate !== false) {
|
|
@@ -9749,10 +9959,13 @@ async function runUninstall(config, options) {
|
|
|
9749
9959
|
}
|
|
9750
9960
|
console.log("Uninstalling local Threadnote setup.");
|
|
9751
9961
|
await runStop(config, { dryRun });
|
|
9752
|
-
await removePathIfExists((0,
|
|
9962
|
+
await removePathIfExists((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
|
|
9753
9963
|
await removeLaunchAgent(dryRun);
|
|
9754
9964
|
await removeMcpConfigs(options.mcp ?? "available", dryRun);
|
|
9755
9965
|
await removeMcpSnippets(config, dryRun);
|
|
9966
|
+
if (await hasManagedClaudeHooks()) {
|
|
9967
|
+
await runHooksInstall(config, "claude", { apply: !dryRun, dryRun, remove: true });
|
|
9968
|
+
}
|
|
9756
9969
|
await removeCommandShim(dryRun);
|
|
9757
9970
|
await removeUserAgentInstructions(dryRun);
|
|
9758
9971
|
if (options.eraseMemories === true) {
|
|
@@ -9803,16 +10016,16 @@ async function repairManifest(config, dryRun) {
|
|
|
9803
10016
|
console.log(output2.trimEnd());
|
|
9804
10017
|
return;
|
|
9805
10018
|
}
|
|
9806
|
-
await ensureDirectory((0,
|
|
10019
|
+
await ensureDirectory((0, import_node_path9.dirname)(config.manifestPath), false);
|
|
9807
10020
|
const currentContent = await readFileIfExists(config.manifestPath);
|
|
9808
10021
|
if (currentContent !== void 0) {
|
|
9809
10022
|
const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
|
|
9810
|
-
await (0,
|
|
9811
|
-
await (0,
|
|
10023
|
+
await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
10024
|
+
await (0, import_promises10.chmod)(backupPath, 384);
|
|
9812
10025
|
console.log(`Backup: ${backupPath}`);
|
|
9813
10026
|
}
|
|
9814
|
-
await (0,
|
|
9815
|
-
await (0,
|
|
10027
|
+
await (0, import_promises10.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
10028
|
+
await (0, import_promises10.chmod)(config.manifestPath, 384);
|
|
9816
10029
|
console.log(`Wrote replacement manifest: ${config.manifestPath}`);
|
|
9817
10030
|
}
|
|
9818
10031
|
async function repairServerHealth(config, dryRun) {
|
|
@@ -9853,7 +10066,7 @@ async function runStart(config, options) {
|
|
|
9853
10066
|
);
|
|
9854
10067
|
}
|
|
9855
10068
|
const logPath = openVikingLogPath(config);
|
|
9856
|
-
await ensureDirectory((0,
|
|
10069
|
+
await ensureDirectory((0, import_node_path9.dirname)(logPath), false);
|
|
9857
10070
|
if (options.foreground === true) {
|
|
9858
10071
|
const result = await runInteractive(server, args);
|
|
9859
10072
|
process.exitCode = result;
|
|
@@ -9862,7 +10075,7 @@ async function runStart(config, options) {
|
|
|
9862
10075
|
const logFd = (0, import_node_fs5.openSync)(logPath, "a");
|
|
9863
10076
|
const child = spawnDetachedServerWithLog(server, args, logFd);
|
|
9864
10077
|
child.unref();
|
|
9865
|
-
await (0,
|
|
10078
|
+
await (0, import_promises10.writeFile)((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
9866
10079
|
`, "utf8");
|
|
9867
10080
|
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
9868
10081
|
if (health) {
|
|
@@ -9895,7 +10108,7 @@ async function runStop(config, options) {
|
|
|
9895
10108
|
console.log(`No LaunchAgent found: ${launchAgentPath}`);
|
|
9896
10109
|
}
|
|
9897
10110
|
}
|
|
9898
|
-
const pidPath = (0,
|
|
10111
|
+
const pidPath = (0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid");
|
|
9899
10112
|
const pidText = await readFileIfExists(pidPath);
|
|
9900
10113
|
if (!pidText) {
|
|
9901
10114
|
console.log("No pid file found for detached OpenViking server.");
|
|
@@ -9991,7 +10204,7 @@ async function pythonSystemCertificatesCheck() {
|
|
|
9991
10204
|
};
|
|
9992
10205
|
}
|
|
9993
10206
|
async function commandShimCheck() {
|
|
9994
|
-
const shimPath = (0,
|
|
10207
|
+
const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
9995
10208
|
const content = await readFileIfExists(shimPath);
|
|
9996
10209
|
if (content === void 0) {
|
|
9997
10210
|
return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
|
|
@@ -10057,11 +10270,11 @@ async function hasPythonModule(serverPath, moduleName) {
|
|
|
10057
10270
|
async function siblingPythonForExecutable(executablePath) {
|
|
10058
10271
|
let resolvedPath;
|
|
10059
10272
|
try {
|
|
10060
|
-
resolvedPath = await (0,
|
|
10273
|
+
resolvedPath = await (0, import_promises10.realpath)(executablePath);
|
|
10061
10274
|
} catch (_err) {
|
|
10062
10275
|
return void 0;
|
|
10063
10276
|
}
|
|
10064
|
-
const pythonPath = (0,
|
|
10277
|
+
const pythonPath = (0, import_node_path9.join)((0, import_node_path9.dirname)(resolvedPath), "python");
|
|
10065
10278
|
return await exists(pythonPath) ? pythonPath : void 0;
|
|
10066
10279
|
}
|
|
10067
10280
|
async function manifestCheck(path) {
|
|
@@ -10110,11 +10323,83 @@ async function waitForOpenVikingHealth(config, timeoutMs) {
|
|
|
10110
10323
|
return void 0;
|
|
10111
10324
|
}
|
|
10112
10325
|
async function runInstallCommands(config, preferred, force, dryRun) {
|
|
10113
|
-
|
|
10326
|
+
let manager = preferred;
|
|
10327
|
+
if (manager === void 0 && !dryRun) {
|
|
10328
|
+
const detected = await detectPackageManager();
|
|
10329
|
+
if (detected === "pip" && await offerToInstallUv()) {
|
|
10330
|
+
const rediscovered = await detectPackageManager();
|
|
10331
|
+
if (rediscovered === "uv") {
|
|
10332
|
+
manager = "uv";
|
|
10333
|
+
}
|
|
10334
|
+
}
|
|
10335
|
+
}
|
|
10336
|
+
const installCommands = await getInstallCommands(config, manager, force);
|
|
10114
10337
|
for (const installCommand of installCommands) {
|
|
10115
10338
|
await maybeRun(dryRun, installCommand.executable, installCommand.args);
|
|
10116
10339
|
}
|
|
10117
10340
|
}
|
|
10341
|
+
async function offerToInstallUv() {
|
|
10342
|
+
if (import_node_process2.stdin.isTTY !== true || import_node_process2.stdout.isTTY !== true) {
|
|
10343
|
+
console.warn(
|
|
10344
|
+
"Neither uv nor pipx was found on PATH. Falling back to `python3 -m pip install --user`, which fails on PEP 668 (Homebrew/system) Python.\nRe-run with --package-manager uv after installing uv (brew install uv), or pass --package-manager pipx."
|
|
10345
|
+
);
|
|
10346
|
+
return false;
|
|
10347
|
+
}
|
|
10348
|
+
const readline = (0, import_promises11.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
10349
|
+
let answer;
|
|
10350
|
+
try {
|
|
10351
|
+
answer = (await readline.question(
|
|
10352
|
+
"OpenViking installs into Python; neither uv nor pipx is on PATH so threadnote would fall back to `pip install --user`, which fails on PEP 668 setups.\nInstall uv now? [Y/n] "
|
|
10353
|
+
)).trim().toLowerCase();
|
|
10354
|
+
} finally {
|
|
10355
|
+
readline.close();
|
|
10356
|
+
}
|
|
10357
|
+
if (answer === "n" || answer === "no") {
|
|
10358
|
+
console.log("Continuing with `python3 -m pip install --user`. You may hit PEP 668 errors on managed Pythons.");
|
|
10359
|
+
return false;
|
|
10360
|
+
}
|
|
10361
|
+
return await installUv();
|
|
10362
|
+
}
|
|
10363
|
+
async function installUv() {
|
|
10364
|
+
const brew = await findExecutable(["brew"]);
|
|
10365
|
+
if (brew) {
|
|
10366
|
+
console.log("Installing uv via Homebrew...");
|
|
10367
|
+
const result = await runCommand(brew, ["install", "uv"], { allowFailure: true });
|
|
10368
|
+
if (result.exitCode === 0) {
|
|
10369
|
+
if (result.stdout.trim()) {
|
|
10370
|
+
console.log(result.stdout.trim());
|
|
10371
|
+
}
|
|
10372
|
+
if (await findExecutable(["uv"])) {
|
|
10373
|
+
return true;
|
|
10374
|
+
}
|
|
10375
|
+
} else {
|
|
10376
|
+
console.warn(`brew install uv failed: ${(result.stderr || result.stdout).trim()}`);
|
|
10377
|
+
}
|
|
10378
|
+
}
|
|
10379
|
+
if (await findExecutable(["curl"]) && await findExecutable(["sh"])) {
|
|
10380
|
+
console.log("Installing uv via the official install script (curl -LsSf https://astral.sh/uv/install.sh | sh)...");
|
|
10381
|
+
const result = await runCommand("sh", ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"], {
|
|
10382
|
+
allowFailure: true
|
|
10383
|
+
});
|
|
10384
|
+
if (result.exitCode === 0) {
|
|
10385
|
+
if (result.stdout.trim()) {
|
|
10386
|
+
console.log(result.stdout.trim());
|
|
10387
|
+
}
|
|
10388
|
+
if (await findExecutable(["uv"])) {
|
|
10389
|
+
return true;
|
|
10390
|
+
}
|
|
10391
|
+
console.warn(
|
|
10392
|
+
"uv installed, but the new binary is not yet on this shell PATH. Open a new shell (or `source ~/.zshrc` / `source ~/.bashrc`) and re-run `threadnote install`."
|
|
10393
|
+
);
|
|
10394
|
+
return false;
|
|
10395
|
+
}
|
|
10396
|
+
console.warn(`uv install script failed: ${(result.stderr || result.stdout).trim()}`);
|
|
10397
|
+
}
|
|
10398
|
+
console.warn(
|
|
10399
|
+
"Could not install uv automatically. Install it manually (brew install uv) and re-run threadnote install."
|
|
10400
|
+
);
|
|
10401
|
+
return false;
|
|
10402
|
+
}
|
|
10118
10403
|
async function getPythonSystemCertificatesInstallCommand(serverPath) {
|
|
10119
10404
|
const pythonPath = await siblingPythonForExecutable(serverPath);
|
|
10120
10405
|
if (!pythonPath) {
|
|
@@ -10183,38 +10468,38 @@ function printInstallNextSteps(options) {
|
|
|
10183
10468
|
}
|
|
10184
10469
|
async function writeTemplateIfMissing(options) {
|
|
10185
10470
|
if (await exists(options.destinationPath)) {
|
|
10186
|
-
const currentContent = await (0,
|
|
10471
|
+
const currentContent = await (0, import_promises10.readFile)(options.destinationPath, "utf8");
|
|
10187
10472
|
if (options.shouldRepair?.(currentContent) !== true) {
|
|
10188
10473
|
console.log(`Already exists: ${options.destinationPath}`);
|
|
10189
10474
|
return;
|
|
10190
10475
|
}
|
|
10191
|
-
const rendered2 = renderTemplate(await (0,
|
|
10476
|
+
const rendered2 = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
|
|
10192
10477
|
if (options.dryRun) {
|
|
10193
10478
|
console.log(`Would repair generated config: ${options.destinationPath}`);
|
|
10194
10479
|
return;
|
|
10195
10480
|
}
|
|
10196
10481
|
const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
|
|
10197
|
-
await (0,
|
|
10198
|
-
await (0,
|
|
10199
|
-
await (0,
|
|
10200
|
-
await (0,
|
|
10482
|
+
await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
10483
|
+
await (0, import_promises10.chmod)(backupPath, 384);
|
|
10484
|
+
await (0, import_promises10.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
|
|
10485
|
+
await (0, import_promises10.chmod)(options.destinationPath, 384);
|
|
10201
10486
|
console.log(`Repaired generated config: ${options.destinationPath}`);
|
|
10202
10487
|
console.log(`Backup: ${backupPath}`);
|
|
10203
10488
|
return;
|
|
10204
10489
|
}
|
|
10205
|
-
const rendered = renderTemplate(await (0,
|
|
10490
|
+
const rendered = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
|
|
10206
10491
|
if (options.dryRun) {
|
|
10207
10492
|
console.log(`Would write ${options.destinationPath}`);
|
|
10208
10493
|
return;
|
|
10209
10494
|
}
|
|
10210
|
-
await ensureDirectory((0,
|
|
10211
|
-
await (0,
|
|
10212
|
-
await (0,
|
|
10495
|
+
await ensureDirectory((0, import_node_path9.dirname)(options.destinationPath), false);
|
|
10496
|
+
await (0, import_promises10.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
|
|
10497
|
+
await (0, import_promises10.chmod)(options.destinationPath, 384);
|
|
10213
10498
|
console.log(`Wrote ${options.destinationPath}`);
|
|
10214
10499
|
}
|
|
10215
10500
|
async function installCommandShim(dryRun) {
|
|
10216
10501
|
const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
|
|
10217
|
-
const shimPath = (0,
|
|
10502
|
+
const shimPath = (0, import_node_path9.join)(binDir, "threadnote");
|
|
10218
10503
|
const existingContent = await readFileIfExists(shimPath);
|
|
10219
10504
|
if (existingContent && !isManagedCommandShim(existingContent)) {
|
|
10220
10505
|
console.log(`WARN not overwriting existing command shim: ${shimPath}`);
|
|
@@ -10230,12 +10515,12 @@ async function installCommandShim(dryRun) {
|
|
|
10230
10515
|
return;
|
|
10231
10516
|
}
|
|
10232
10517
|
await ensureDirectory(binDir, false);
|
|
10233
|
-
await (0,
|
|
10234
|
-
await (0,
|
|
10518
|
+
await (0, import_promises10.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
|
|
10519
|
+
await (0, import_promises10.chmod)(shimPath, 493);
|
|
10235
10520
|
console.log(`Wrote command shim: ${shimPath}`);
|
|
10236
10521
|
}
|
|
10237
10522
|
async function removeCommandShim(dryRun) {
|
|
10238
|
-
const shimPath = (0,
|
|
10523
|
+
const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
10239
10524
|
const content = await readFileIfExists(shimPath);
|
|
10240
10525
|
if (content === void 0) {
|
|
10241
10526
|
console.log(`Already absent: ${shimPath}`);
|
|
@@ -10269,8 +10554,8 @@ async function installUserAgentInstructions(dryRun) {
|
|
|
10269
10554
|
console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
|
|
10270
10555
|
continue;
|
|
10271
10556
|
}
|
|
10272
|
-
await ensureDirectory((0,
|
|
10273
|
-
await (0,
|
|
10557
|
+
await ensureDirectory((0, import_node_path9.dirname)(targetPath), false);
|
|
10558
|
+
await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
10274
10559
|
console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
|
|
10275
10560
|
}
|
|
10276
10561
|
}
|
|
@@ -10307,7 +10592,7 @@ async function removeUserAgentInstructions(dryRun) {
|
|
|
10307
10592
|
console.log(`Would update ${targetPath}`);
|
|
10308
10593
|
continue;
|
|
10309
10594
|
}
|
|
10310
|
-
await (0,
|
|
10595
|
+
await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
10311
10596
|
console.log(`Updated ${targetPath}`);
|
|
10312
10597
|
}
|
|
10313
10598
|
}
|
|
@@ -10327,7 +10612,7 @@ async function renderUserAgentInstructions(target) {
|
|
|
10327
10612
|
].join("\n");
|
|
10328
10613
|
}
|
|
10329
10614
|
async function renderUserAgentInstructionsBlock() {
|
|
10330
|
-
const instructions = (await (0,
|
|
10615
|
+
const instructions = (await (0, import_promises10.readFile)((0, import_node_path9.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
|
|
10331
10616
|
return `${USER_INSTRUCTIONS_START_MARKER}
|
|
10332
10617
|
${instructions}
|
|
10333
10618
|
${USER_INSTRUCTIONS_END_MARKER}`;
|
|
@@ -10405,9 +10690,9 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
10405
10690
|
if ((0, import_node_os6.platform)() !== "darwin") {
|
|
10406
10691
|
throw new Error("launchd autostart is only supported on macOS.");
|
|
10407
10692
|
}
|
|
10408
|
-
const source = (0,
|
|
10693
|
+
const source = (0, import_node_path9.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
|
|
10409
10694
|
const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
10410
|
-
const rendered = renderTemplate(await (0,
|
|
10695
|
+
const rendered = renderTemplate(await (0, import_promises10.readFile)(source, "utf8"), config);
|
|
10411
10696
|
if (dryRun) {
|
|
10412
10697
|
console.log(`Would write ${destination}`);
|
|
10413
10698
|
console.log(`Would run: launchctl unload ${destination}`);
|
|
@@ -10415,9 +10700,9 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
10415
10700
|
console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
|
|
10416
10701
|
return;
|
|
10417
10702
|
}
|
|
10418
|
-
await ensureDirectory((0,
|
|
10419
|
-
await ensureDirectory((0,
|
|
10420
|
-
await (0,
|
|
10703
|
+
await ensureDirectory((0, import_node_path9.dirname)(destination), false);
|
|
10704
|
+
await ensureDirectory((0, import_node_path9.dirname)(openVikingLogPath(config)), false);
|
|
10705
|
+
await (0, import_promises10.writeFile)(destination, rendered, "utf8");
|
|
10421
10706
|
await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
|
|
10422
10707
|
await maybeRun(false, "launchctl", ["load", destination]);
|
|
10423
10708
|
await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
|
|
@@ -10480,7 +10765,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
|
|
|
10480
10765
|
if (typeof parsed.default_user !== "string") {
|
|
10481
10766
|
return false;
|
|
10482
10767
|
}
|
|
10483
|
-
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0,
|
|
10768
|
+
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path9.join)(config.agentContextHome, "data")) {
|
|
10484
10769
|
return false;
|
|
10485
10770
|
}
|
|
10486
10771
|
return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
|
|
@@ -10508,9 +10793,20 @@ async function main() {
|
|
|
10508
10793
|
await runDoctor(config, options);
|
|
10509
10794
|
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
10510
10795
|
});
|
|
10511
|
-
program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).
|
|
10796
|
+
program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).option(
|
|
10797
|
+
"--with-hooks",
|
|
10798
|
+
"Also install agent-side hooks (Claude PreCompact + SessionStart) for deterministic handoff snapshots and context preload"
|
|
10799
|
+
).action(async (options) => {
|
|
10512
10800
|
const config = getRuntimeConfig(program2);
|
|
10513
10801
|
await runInstall(config, options);
|
|
10802
|
+
if (options.withHooks === true) {
|
|
10803
|
+
const dryRun = options.dryRun === true;
|
|
10804
|
+
for (const agent of ["claude", "codex", "cursor", "copilot"]) {
|
|
10805
|
+
console.log(`
|
|
10806
|
+
--- ${agent} hooks ---`);
|
|
10807
|
+
await runHooksInstall(config, agent, { apply: !dryRun, dryRun });
|
|
10808
|
+
}
|
|
10809
|
+
}
|
|
10514
10810
|
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
10515
10811
|
});
|
|
10516
10812
|
program2.command("update").description("Update the published Threadnote package, then repair local shims and MCP config").option("--check", "Only check whether a newer version is available").option("--dry-run", "Print update and repair commands without running them").option("--force", "Run package-manager update even if this version is already current").option("--registry <url>", "npm registry URL", process.env.THREADNOTE_NPM_REGISTRY).option("--runtime <runtime>", "auto, npm, bun, or deno", parseUpdateRuntime, "auto").option("--no-repair", "Skip threadnote repair after updating the package").option("--no-post-update", "Skip post-update migration prompts").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
|
|
@@ -10555,6 +10851,21 @@ async function main() {
|
|
|
10555
10851
|
program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, cursor, or copilot").option("--apply", "Actually modify the selected agent config").option("--name <name>", "MCP server name", OPENVIKING_MCP_NAME).option("--native-http", "Install OpenViking native HTTP MCP endpoint instead of the local stdio adapter").option("--scope <scope>", "Claude MCP config scope: user, local, or project", parseClaudeMcpScope, "user").option("--url <url>", "OpenViking native HTTP MCP URL").option("--bearer-token-env-var <name>", "Environment variable containing the local API key").action(async (agent, options) => {
|
|
10556
10852
|
await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
|
|
10557
10853
|
});
|
|
10854
|
+
program2.command("install-hooks").description(
|
|
10855
|
+
"Install deterministic agent hooks (Claude PreCompact + SessionStart). Soft instruction files remain the cross-agent guidance surface; hooks add a deterministic safety net where the agent supports it."
|
|
10856
|
+
).argument("<agent>", "codex, claude, cursor, or copilot").option("--apply", "Actually modify the selected agent config").option("--dry-run", "Print the planned change without applying it").option("--remove", "Remove threadnote-managed hook entries instead of adding them").action(async (agent, options) => {
|
|
10857
|
+
await runHooksInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
|
|
10858
|
+
});
|
|
10859
|
+
program2.command("pre-compact-hook", { hidden: true }).description(
|
|
10860
|
+
"Hook entry point: store a handoff snapshot before context compaction. Used by `install-hooks claude`."
|
|
10861
|
+
).option("--dry-run", "Print the handoff payload without writing it").action(async (options) => {
|
|
10862
|
+
await runPreCompactHook(getRuntimeConfig(program2), options);
|
|
10863
|
+
});
|
|
10864
|
+
program2.command("session-start-hook", { hidden: true }).description(
|
|
10865
|
+
"Hook entry point: print the latest threadnote handoff/feature memory for the current repo so Claude can preload it."
|
|
10866
|
+
).option("--dry-run", "Print the planned ov command without running it").action(async (options) => {
|
|
10867
|
+
await runSessionStartHook(getRuntimeConfig(program2), options);
|
|
10868
|
+
});
|
|
10558
10869
|
program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind, "durable").option("--project <name>", "Project/repo/topic namespace for lifecycle-aware storage").option("--replace <uri>", "Supersede an existing viking:// memory after the new memory is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--status <status>", "active, archived, or superseded", parseMemoryStatus, "active").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").option("--topic <name>", "Stable topic name; active memories with the same project/topic update one file").action(async (options) => {
|
|
10559
10870
|
await runRemember(getRuntimeConfig(program2), options);
|
|
10560
10871
|
});
|