threadnote 0.3.6 → 0.3.8
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 +83 -17
- package/config/post-update-migrations.json +23 -0
- package/dist/mcp_server.cjs +254 -53
- package/dist/threadnote.cjs +1198 -436
- package/docs/agent-instructions.md +47 -4
- package/docs/migration.md +26 -10
- package/docs/rollout.md +1 -1
- package/docs/security.md +5 -4
- package/docs/troubleshooting.md +11 -1
- package/package.json +1 -1
package/dist/threadnote.cjs
CHANGED
|
@@ -552,13 +552,13 @@ var require_help = __commonJS({
|
|
|
552
552
|
function callFormatItem(term, description) {
|
|
553
553
|
return helper.formatItem(term, termWidth, description, helper);
|
|
554
554
|
}
|
|
555
|
-
let
|
|
555
|
+
let output2 = [
|
|
556
556
|
`${helper.styleTitle("Usage:")} ${helper.styleUsage(helper.commandUsage(cmd))}`,
|
|
557
557
|
""
|
|
558
558
|
];
|
|
559
559
|
const commandDescription = helper.commandDescription(cmd);
|
|
560
560
|
if (commandDescription.length > 0) {
|
|
561
|
-
|
|
561
|
+
output2 = output2.concat([
|
|
562
562
|
helper.boxWrap(
|
|
563
563
|
helper.styleCommandDescription(commandDescription),
|
|
564
564
|
helpWidth
|
|
@@ -572,7 +572,7 @@ var require_help = __commonJS({
|
|
|
572
572
|
helper.styleArgumentDescription(helper.argumentDescription(argument))
|
|
573
573
|
);
|
|
574
574
|
});
|
|
575
|
-
|
|
575
|
+
output2 = output2.concat(
|
|
576
576
|
this.formatItemList("Arguments:", argumentList, helper)
|
|
577
577
|
);
|
|
578
578
|
const optionGroups = this.groupItems(
|
|
@@ -587,7 +587,7 @@ var require_help = __commonJS({
|
|
|
587
587
|
helper.styleOptionDescription(helper.optionDescription(option))
|
|
588
588
|
);
|
|
589
589
|
});
|
|
590
|
-
|
|
590
|
+
output2 = output2.concat(this.formatItemList(group, optionList, helper));
|
|
591
591
|
});
|
|
592
592
|
if (helper.showGlobalOptions) {
|
|
593
593
|
const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => {
|
|
@@ -596,7 +596,7 @@ var require_help = __commonJS({
|
|
|
596
596
|
helper.styleOptionDescription(helper.optionDescription(option))
|
|
597
597
|
);
|
|
598
598
|
});
|
|
599
|
-
|
|
599
|
+
output2 = output2.concat(
|
|
600
600
|
this.formatItemList("Global Options:", globalOptionList, helper)
|
|
601
601
|
);
|
|
602
602
|
}
|
|
@@ -612,9 +612,9 @@ var require_help = __commonJS({
|
|
|
612
612
|
helper.styleSubcommandDescription(helper.subcommandDescription(sub))
|
|
613
613
|
);
|
|
614
614
|
});
|
|
615
|
-
|
|
615
|
+
output2 = output2.concat(this.formatItemList(group, commandList, helper));
|
|
616
616
|
});
|
|
617
|
-
return
|
|
617
|
+
return output2.join("\n");
|
|
618
618
|
}
|
|
619
619
|
/**
|
|
620
620
|
* Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations.
|
|
@@ -3489,9 +3489,14 @@ var USER_INSTRUCTIONS_START_MARKER = "<!-- BEGIN THREADNOTE USER INSTRUCTIONS --
|
|
|
3489
3489
|
var USER_INSTRUCTIONS_END_MARKER = "<!-- END THREADNOTE USER INSTRUCTIONS -->";
|
|
3490
3490
|
var USER_MANIFEST_NAME = "seed-manifest.yaml";
|
|
3491
3491
|
var USER_AGENT_INSTRUCTION_TARGETS = [
|
|
3492
|
-
{ label: "codex user instructions", path: "~/.codex/AGENTS.md" },
|
|
3493
|
-
{ label: "claude user instructions", path: "~/.claude/CLAUDE.md" },
|
|
3494
|
-
{ label: "cursor user rule", path: "~/.cursor/rules/threadnote.md" }
|
|
3492
|
+
{ kind: "block", label: "codex user instructions", path: "~/.codex/AGENTS.md" },
|
|
3493
|
+
{ kind: "block", label: "claude user instructions", path: "~/.claude/CLAUDE.md" },
|
|
3494
|
+
{ kind: "block", label: "cursor user rule", path: "~/.cursor/rules/threadnote.md" },
|
|
3495
|
+
{
|
|
3496
|
+
kind: "file",
|
|
3497
|
+
label: "copilot user instructions",
|
|
3498
|
+
path: "~/.copilot/instructions/threadnote.instructions.md"
|
|
3499
|
+
}
|
|
3495
3500
|
];
|
|
3496
3501
|
var DEFAULT_SEED_PATTERNS = [
|
|
3497
3502
|
"AGENTS.md",
|
|
@@ -3564,7 +3569,7 @@ async function walkFiles(root) {
|
|
|
3564
3569
|
return files;
|
|
3565
3570
|
}
|
|
3566
3571
|
function globToRegExp(glob) {
|
|
3567
|
-
let
|
|
3572
|
+
let output2 = "^";
|
|
3568
3573
|
for (let index = 0; index < glob.length; index += 1) {
|
|
3569
3574
|
const char = glob[index];
|
|
3570
3575
|
if (char === "*") {
|
|
@@ -3572,23 +3577,23 @@ function globToRegExp(glob) {
|
|
|
3572
3577
|
if (next === "*") {
|
|
3573
3578
|
const afterNext = glob[index + 2];
|
|
3574
3579
|
if (afterNext === "/") {
|
|
3575
|
-
|
|
3580
|
+
output2 += "(?:.*/)?";
|
|
3576
3581
|
index += 2;
|
|
3577
3582
|
} else {
|
|
3578
|
-
|
|
3583
|
+
output2 += ".*";
|
|
3579
3584
|
index += 1;
|
|
3580
3585
|
}
|
|
3581
3586
|
} else {
|
|
3582
|
-
|
|
3587
|
+
output2 += "[^/]*";
|
|
3583
3588
|
}
|
|
3584
3589
|
} else if (char === "?") {
|
|
3585
|
-
|
|
3590
|
+
output2 += "[^/]";
|
|
3586
3591
|
} else {
|
|
3587
|
-
|
|
3592
|
+
output2 += escapeRegExp(char);
|
|
3588
3593
|
}
|
|
3589
3594
|
}
|
|
3590
|
-
|
|
3591
|
-
return new RegExp(
|
|
3595
|
+
output2 += "$";
|
|
3596
|
+
return new RegExp(output2);
|
|
3592
3597
|
}
|
|
3593
3598
|
function getGlobBase(pattern) {
|
|
3594
3599
|
const parts = pattern.split("/");
|
|
@@ -3933,8 +3938,8 @@ function exactRecallTerms(query) {
|
|
|
3933
3938
|
}
|
|
3934
3939
|
return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
|
|
3935
3940
|
}
|
|
3936
|
-
function grepOutputHasMatches(
|
|
3937
|
-
return !
|
|
3941
|
+
function grepOutputHasMatches(output2) {
|
|
3942
|
+
return !output2.includes("matches []") && !output2.includes('"matches":[]') && !output2.includes("match_count 0");
|
|
3938
3943
|
}
|
|
3939
3944
|
function exactRecallTermScore(term) {
|
|
3940
3945
|
let score = term.length;
|
|
@@ -3981,6 +3986,7 @@ function errorMessage(err) {
|
|
|
3981
3986
|
}
|
|
3982
3987
|
|
|
3983
3988
|
// src/mcp.ts
|
|
3989
|
+
var import_node_fs2 = require("node:fs");
|
|
3984
3990
|
var import_promises2 = require("node:fs/promises");
|
|
3985
3991
|
var import_node_path2 = require("node:path");
|
|
3986
3992
|
var import_node_os2 = require("node:os");
|
|
@@ -4010,6 +4016,15 @@ async function runMcpInstall(config, agent, options) {
|
|
|
4010
4016
|
});
|
|
4011
4017
|
return;
|
|
4012
4018
|
}
|
|
4019
|
+
if (agent === "copilot") {
|
|
4020
|
+
await runCopilotMcpInstall(config, name, {
|
|
4021
|
+
apply,
|
|
4022
|
+
bearerTokenEnvVar: options.bearerTokenEnvVar,
|
|
4023
|
+
nativeHttp,
|
|
4024
|
+
url
|
|
4025
|
+
});
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4013
4028
|
const command = buildMcpInstallCommand(config, agent, name, {
|
|
4014
4029
|
bearerTokenEnvVar: options.bearerTokenEnvVar,
|
|
4015
4030
|
nativeHttp,
|
|
@@ -4059,6 +4074,34 @@ async function runCursorMcpInstall(config, name, options) {
|
|
|
4059
4074
|
await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
|
|
4060
4075
|
console.log(currentContent === void 0 ? `Wrote Cursor MCP config: ${path}` : `Updated Cursor MCP config: ${path}`);
|
|
4061
4076
|
}
|
|
4077
|
+
async function runCopilotMcpInstall(config, name, options) {
|
|
4078
|
+
const path = copilotMcpConfigPath();
|
|
4079
|
+
const serverConfig = buildCopilotMcpServerConfig(config, {
|
|
4080
|
+
bearerTokenEnvVar: options.bearerTokenEnvVar,
|
|
4081
|
+
nativeHttp: options.nativeHttp,
|
|
4082
|
+
url: options.url
|
|
4083
|
+
});
|
|
4084
|
+
const currentContent = await readFileIfExists(path);
|
|
4085
|
+
const nextContent = renderCopilotMcpConfig(currentContent, name, serverConfig);
|
|
4086
|
+
if (!options.apply) {
|
|
4087
|
+
console.log("Dry run. Re-run with --apply to modify GitHub Copilot MCP config.");
|
|
4088
|
+
printCopilotMcpSnippet(config, name, {
|
|
4089
|
+
bearerTokenEnvVar: options.bearerTokenEnvVar,
|
|
4090
|
+
nativeHttp: options.nativeHttp,
|
|
4091
|
+
url: options.url
|
|
4092
|
+
});
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
if (currentContent === nextContent) {
|
|
4096
|
+
console.log(`Already configured: ${path}`);
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
await ensureDirectory((0, import_node_path2.dirname)(path), false);
|
|
4100
|
+
await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
|
|
4101
|
+
console.log(
|
|
4102
|
+
currentContent === void 0 ? `Wrote GitHub Copilot MCP config: ${path}` : `Updated GitHub Copilot MCP config: ${path}`
|
|
4103
|
+
);
|
|
4104
|
+
}
|
|
4062
4105
|
async function removeMcpConfigs(value, dryRun) {
|
|
4063
4106
|
const clients = await resolveMcpClients(value, "remove");
|
|
4064
4107
|
if (clients.length === 0) {
|
|
@@ -4070,6 +4113,10 @@ async function removeMcpConfigs(value, dryRun) {
|
|
|
4070
4113
|
await removeCursorMcpConfig(OPENVIKING_MCP_NAME, dryRun);
|
|
4071
4114
|
continue;
|
|
4072
4115
|
}
|
|
4116
|
+
if (client === "copilot") {
|
|
4117
|
+
await removeCopilotMcpConfig(OPENVIKING_MCP_NAME, dryRun);
|
|
4118
|
+
continue;
|
|
4119
|
+
}
|
|
4073
4120
|
const command = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
|
|
4074
4121
|
await maybeRun(dryRun, command.executable, command.args, { allowFailure: true, cwd: command.cwd });
|
|
4075
4122
|
}
|
|
@@ -4090,11 +4137,19 @@ async function removeMcpSnippets(config, dryRun) {
|
|
|
4090
4137
|
"MCP snippet",
|
|
4091
4138
|
dryRun
|
|
4092
4139
|
);
|
|
4140
|
+
await removePathIfExists(
|
|
4141
|
+
(0, import_node_path2.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.copilot.json`),
|
|
4142
|
+
"MCP snippet",
|
|
4143
|
+
dryRun
|
|
4144
|
+
);
|
|
4093
4145
|
}
|
|
4094
4146
|
function buildMcpInstallCommand(config, agent, name, options) {
|
|
4095
4147
|
if (agent === "cursor") {
|
|
4096
4148
|
throw new Error("Cursor MCP config is written directly to ~/.cursor/mcp.json.");
|
|
4097
4149
|
}
|
|
4150
|
+
if (agent === "copilot") {
|
|
4151
|
+
throw new Error("GitHub Copilot MCP config is written directly to the VS Code user mcp.json file.");
|
|
4152
|
+
}
|
|
4098
4153
|
const claudeCwd = getInvocationCwd();
|
|
4099
4154
|
const claudeScope = options.scope ?? "user";
|
|
4100
4155
|
if (!options.nativeHttp) {
|
|
@@ -4139,6 +4194,9 @@ function buildMcpRemoveCommand(agent, name) {
|
|
|
4139
4194
|
if (agent === "cursor") {
|
|
4140
4195
|
throw new Error("Cursor MCP config is removed directly from ~/.cursor/mcp.json.");
|
|
4141
4196
|
}
|
|
4197
|
+
if (agent === "copilot") {
|
|
4198
|
+
throw new Error("GitHub Copilot MCP config is removed directly from the VS Code user mcp.json file.");
|
|
4199
|
+
}
|
|
4142
4200
|
return agent === "codex" ? { executable: "codex", args: ["mcp", "remove", name] } : { executable: "claude", args: ["mcp", "remove", name], cwd: getInvocationCwd() };
|
|
4143
4201
|
}
|
|
4144
4202
|
function mcpEnvironment(config) {
|
|
@@ -4171,6 +4229,21 @@ function buildCursorMcpServerConfig(config, options) {
|
|
|
4171
4229
|
env: mcpEnvironmentObject(config)
|
|
4172
4230
|
};
|
|
4173
4231
|
}
|
|
4232
|
+
function buildCopilotMcpServerConfig(config, options) {
|
|
4233
|
+
if (options.nativeHttp) {
|
|
4234
|
+
const server = { type: "http", url: options.url };
|
|
4235
|
+
if (options.bearerTokenEnvVar) {
|
|
4236
|
+
server.headers = { Authorization: `Bearer \${env:${options.bearerTokenEnvVar}}` };
|
|
4237
|
+
}
|
|
4238
|
+
return server;
|
|
4239
|
+
}
|
|
4240
|
+
return {
|
|
4241
|
+
args: [mcpAdapterCommand()[0]],
|
|
4242
|
+
command: "/usr/bin/env",
|
|
4243
|
+
env: mcpEnvironmentObject(config),
|
|
4244
|
+
type: "stdio"
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4174
4247
|
function renderCursorMcpConfig(currentContent, name, serverConfig) {
|
|
4175
4248
|
const parsed = currentContent === void 0 ? {} : parseJsonConfigObject(currentContent);
|
|
4176
4249
|
if (parsed === void 0) {
|
|
@@ -4186,6 +4259,21 @@ function renderCursorMcpConfig(currentContent, name, serverConfig) {
|
|
|
4186
4259
|
return `${JSON.stringify(nextConfig, null, 2)}
|
|
4187
4260
|
`;
|
|
4188
4261
|
}
|
|
4262
|
+
function renderCopilotMcpConfig(currentContent, name, serverConfig) {
|
|
4263
|
+
const parsed = currentContent === void 0 ? {} : parseJsonConfigObject(currentContent);
|
|
4264
|
+
if (parsed === void 0) {
|
|
4265
|
+
throw new Error(`${copilotMcpConfigPath()} exists but is not a JSON object; not modifying it.`);
|
|
4266
|
+
}
|
|
4267
|
+
if (parsed.servers !== void 0 && !isJsonObject(parsed.servers)) {
|
|
4268
|
+
throw new Error(`${copilotMcpConfigPath()} has a non-object servers field; not modifying it.`);
|
|
4269
|
+
}
|
|
4270
|
+
const nextConfig = { ...parsed };
|
|
4271
|
+
const servers = isJsonObject(parsed.servers) ? { ...parsed.servers } : {};
|
|
4272
|
+
servers[name] = serverConfig;
|
|
4273
|
+
nextConfig.servers = servers;
|
|
4274
|
+
return `${JSON.stringify(nextConfig, null, 2)}
|
|
4275
|
+
`;
|
|
4276
|
+
}
|
|
4189
4277
|
async function removeCursorMcpConfig(name, dryRun) {
|
|
4190
4278
|
const path = cursorMcpConfigPath();
|
|
4191
4279
|
const currentContent = await readFileIfExists(path);
|
|
@@ -4215,11 +4303,44 @@ async function removeCursorMcpConfig(name, dryRun) {
|
|
|
4215
4303
|
await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
|
|
4216
4304
|
console.log(`Updated Cursor MCP config: ${path}`);
|
|
4217
4305
|
}
|
|
4306
|
+
async function removeCopilotMcpConfig(name, dryRun) {
|
|
4307
|
+
const path = copilotMcpConfigPath();
|
|
4308
|
+
const currentContent = await readFileIfExists(path);
|
|
4309
|
+
if (currentContent === void 0) {
|
|
4310
|
+
console.log(`Already absent: ${path}`);
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
const parsed = parseJsonConfigObject(currentContent);
|
|
4314
|
+
if (parsed === void 0) {
|
|
4315
|
+
console.log(`WARN ${path} exists but is not a JSON object; not modifying it.`);
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
if (!isJsonObject(parsed.servers) || parsed.servers[name] === void 0) {
|
|
4319
|
+
console.log(`No GitHub Copilot MCP config found: ${path}`);
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
const nextConfig = { ...parsed };
|
|
4323
|
+
const servers = { ...parsed.servers };
|
|
4324
|
+
delete servers[name];
|
|
4325
|
+
nextConfig.servers = servers;
|
|
4326
|
+
const nextContent = `${JSON.stringify(nextConfig, null, 2)}
|
|
4327
|
+
`;
|
|
4328
|
+
if (dryRun) {
|
|
4329
|
+
console.log(`Would update GitHub Copilot MCP config: ${path}`);
|
|
4330
|
+
return;
|
|
4331
|
+
}
|
|
4332
|
+
await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
|
|
4333
|
+
console.log(`Updated GitHub Copilot MCP config: ${path}`);
|
|
4334
|
+
}
|
|
4218
4335
|
function printMcpSnippet(config, agent, name, options) {
|
|
4219
4336
|
if (agent === "cursor") {
|
|
4220
4337
|
printCursorMcpSnippet(config, name, { nativeHttp: options.nativeHttp, url: options.url });
|
|
4221
4338
|
return;
|
|
4222
4339
|
}
|
|
4340
|
+
if (agent === "copilot") {
|
|
4341
|
+
printCopilotMcpSnippet(config, name, { nativeHttp: options.nativeHttp, url: options.url });
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4223
4344
|
const snippetPath = (0, import_node_path2.join)(config.agentContextHome, "mcp", `${name}.${agent}.${agent === "codex" ? "toml" : "txt"}`);
|
|
4224
4345
|
const command = buildMcpInstallCommand(config, agent, name, {
|
|
4225
4346
|
nativeHttp: options.nativeHttp,
|
|
@@ -4239,14 +4360,37 @@ function printCursorMcpSnippet(config, name, options) {
|
|
|
4239
4360
|
Snippet (${snippetPath}; merge into ${cursorMcpConfigPath()}):
|
|
4240
4361
|
${snippet2}`);
|
|
4241
4362
|
}
|
|
4363
|
+
function printCopilotMcpSnippet(config, name, options) {
|
|
4364
|
+
const snippetPath = (0, import_node_path2.join)(config.agentContextHome, "mcp", `${name}.copilot.json`);
|
|
4365
|
+
const snippet2 = JSON.stringify({ servers: { [name]: buildCopilotMcpServerConfig(config, options) } }, null, 2);
|
|
4366
|
+
console.log(`
|
|
4367
|
+
Snippet (${snippetPath}; merge into ${copilotMcpConfigPath()}):
|
|
4368
|
+
${snippet2}`);
|
|
4369
|
+
}
|
|
4242
4370
|
function cursorMcpConfigPath() {
|
|
4243
4371
|
return expandPath("~/.cursor/mcp.json");
|
|
4244
4372
|
}
|
|
4373
|
+
function copilotMcpConfigPath() {
|
|
4374
|
+
if (process.env.THREADNOTE_COPILOT_MCP_CONFIG) {
|
|
4375
|
+
return expandPath(process.env.THREADNOTE_COPILOT_MCP_CONFIG);
|
|
4376
|
+
}
|
|
4377
|
+
if ((0, import_node_os2.platform)() === "darwin") {
|
|
4378
|
+
const stablePath = expandPath("~/Library/Application Support/Code/User/mcp.json");
|
|
4379
|
+
const insidersPath = expandPath("~/Library/Application Support/Code - Insiders/User/mcp.json");
|
|
4380
|
+
return existsSyncDirectory((0, import_node_path2.dirname)(stablePath)) || !existsSyncDirectory((0, import_node_path2.dirname)(insidersPath)) ? stablePath : insidersPath;
|
|
4381
|
+
}
|
|
4382
|
+
if ((0, import_node_os2.platform)() === "win32") {
|
|
4383
|
+
const appData = process.env.APPDATA;
|
|
4384
|
+
return appData ? (0, import_node_path2.join)(appData, "Code", "User", "mcp.json") : expandPath("~/AppData/Roaming/Code/User/mcp.json");
|
|
4385
|
+
}
|
|
4386
|
+
const configHome = process.env.XDG_CONFIG_HOME ? expandPath(process.env.XDG_CONFIG_HOME) : expandPath("~/.config");
|
|
4387
|
+
return (0, import_node_path2.join)(configHome, "Code", "User", "mcp.json");
|
|
4388
|
+
}
|
|
4245
4389
|
function parseAgentClient(value) {
|
|
4246
|
-
if (value === "codex" || value === "claude" || value === "cursor") {
|
|
4390
|
+
if (value === "codex" || value === "claude" || value === "copilot" || value === "cursor") {
|
|
4247
4391
|
return value;
|
|
4248
4392
|
}
|
|
4249
|
-
throw new Error(`Unsupported agent: ${value}. Expected codex, claude, or cursor.`);
|
|
4393
|
+
throw new Error(`Unsupported agent: ${value}. Expected codex, claude, copilot, or cursor.`);
|
|
4250
4394
|
}
|
|
4251
4395
|
function parseClaudeMcpScope(value) {
|
|
4252
4396
|
if (value === "local" || value === "project" || value === "user") {
|
|
@@ -4261,7 +4405,7 @@ async function resolveMcpClients(value, action) {
|
|
|
4261
4405
|
}
|
|
4262
4406
|
let requested;
|
|
4263
4407
|
if (normalized === "available" || normalized === "all") {
|
|
4264
|
-
requested = ["codex", "claude", "cursor"];
|
|
4408
|
+
requested = ["codex", "claude", "cursor", "copilot"];
|
|
4265
4409
|
} else {
|
|
4266
4410
|
requested = normalized.split(",").map((part) => part.trim()).filter(Boolean).map(parseAgentClient);
|
|
4267
4411
|
}
|
|
@@ -4277,6 +4421,16 @@ async function resolveMcpClients(value, action) {
|
|
|
4277
4421
|
}
|
|
4278
4422
|
continue;
|
|
4279
4423
|
}
|
|
4424
|
+
if (client === "copilot") {
|
|
4425
|
+
if (!await isCopilotAvailable()) {
|
|
4426
|
+
console.log(`WARN VS Code/Copilot config not found; cannot ${action} copilot MCP config.`);
|
|
4427
|
+
continue;
|
|
4428
|
+
}
|
|
4429
|
+
if (!clients.includes(client)) {
|
|
4430
|
+
clients.push(client);
|
|
4431
|
+
}
|
|
4432
|
+
continue;
|
|
4433
|
+
}
|
|
4280
4434
|
if (!await findExecutable([client])) {
|
|
4281
4435
|
console.log(`WARN ${client} command not found; cannot ${action} ${client} MCP config.`);
|
|
4282
4436
|
continue;
|
|
@@ -4296,9 +4450,28 @@ async function isCursorAvailable() {
|
|
|
4296
4450
|
}
|
|
4297
4451
|
return (0, import_node_os2.platform)() === "darwin" && await exists("/Applications/Cursor.app");
|
|
4298
4452
|
}
|
|
4453
|
+
async function isCopilotAvailable() {
|
|
4454
|
+
if (process.env.THREADNOTE_COPILOT_MCP_CONFIG) {
|
|
4455
|
+
return true;
|
|
4456
|
+
}
|
|
4457
|
+
if (await exists((0, import_node_path2.dirname)(copilotMcpConfigPath()))) {
|
|
4458
|
+
return true;
|
|
4459
|
+
}
|
|
4460
|
+
if (await findExecutable(["code", "code-insiders"])) {
|
|
4461
|
+
return true;
|
|
4462
|
+
}
|
|
4463
|
+
return (0, import_node_os2.platform)() === "darwin" && await exists("/Applications/Visual Studio Code.app");
|
|
4464
|
+
}
|
|
4465
|
+
function existsSyncDirectory(path) {
|
|
4466
|
+
try {
|
|
4467
|
+
return (0, import_node_fs2.statSync)(path).isDirectory();
|
|
4468
|
+
} catch (_err) {
|
|
4469
|
+
return false;
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4299
4472
|
|
|
4300
4473
|
// src/runtime.ts
|
|
4301
|
-
var
|
|
4474
|
+
var import_node_fs3 = require("node:fs");
|
|
4302
4475
|
var import_node_os3 = require("node:os");
|
|
4303
4476
|
var import_node_path3 = require("node:path");
|
|
4304
4477
|
function getRuntimeConfig(program2, manifestOverride) {
|
|
@@ -4320,7 +4493,7 @@ function getRuntimeConfig(program2, manifestOverride) {
|
|
|
4320
4493
|
}
|
|
4321
4494
|
function defaultManifestPath(agentContextHome) {
|
|
4322
4495
|
const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
|
|
4323
|
-
return (0,
|
|
4496
|
+
return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
|
|
4324
4497
|
}
|
|
4325
4498
|
function builtInExampleManifestPath() {
|
|
4326
4499
|
return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
|
|
@@ -4991,14 +5164,14 @@ function resolveYamlBinary(data) {
|
|
|
4991
5164
|
return bitlen % 8 === 0;
|
|
4992
5165
|
}
|
|
4993
5166
|
function constructYamlBinary(data) {
|
|
4994
|
-
var idx, tailbits,
|
|
5167
|
+
var idx, tailbits, input2 = data.replace(/[\r\n=]/g, ""), max = input2.length, map2 = BASE64_MAP, bits = 0, result = [];
|
|
4995
5168
|
for (idx = 0; idx < max; idx++) {
|
|
4996
5169
|
if (idx % 4 === 0 && idx) {
|
|
4997
5170
|
result.push(bits >> 16 & 255);
|
|
4998
5171
|
result.push(bits >> 8 & 255);
|
|
4999
5172
|
result.push(bits & 255);
|
|
5000
5173
|
}
|
|
5001
|
-
bits = bits << 6 | map2.indexOf(
|
|
5174
|
+
bits = bits << 6 | map2.indexOf(input2.charAt(idx));
|
|
5002
5175
|
}
|
|
5003
5176
|
tailbits = max % 4 * 6;
|
|
5004
5177
|
if (tailbits === 0) {
|
|
@@ -5231,8 +5404,8 @@ for (i = 0; i < 256; i++) {
|
|
|
5231
5404
|
simpleEscapeMap[i] = simpleEscapeSequence(i);
|
|
5232
5405
|
}
|
|
5233
5406
|
var i;
|
|
5234
|
-
function State$1(
|
|
5235
|
-
this.input =
|
|
5407
|
+
function State$1(input2, options) {
|
|
5408
|
+
this.input = input2;
|
|
5236
5409
|
this.filename = options["filename"] || null;
|
|
5237
5410
|
this.schema = options["schema"] || _default;
|
|
5238
5411
|
this.onWarning = options["onWarning"] || null;
|
|
@@ -5241,7 +5414,7 @@ function State$1(input, options) {
|
|
|
5241
5414
|
this.listener = options["listener"] || null;
|
|
5242
5415
|
this.implicitTypes = this.schema.compiledImplicit;
|
|
5243
5416
|
this.typeMap = this.schema.compiledTypeMap;
|
|
5244
|
-
this.length =
|
|
5417
|
+
this.length = input2.length;
|
|
5245
5418
|
this.position = 0;
|
|
5246
5419
|
this.line = 0;
|
|
5247
5420
|
this.lineStart = 0;
|
|
@@ -6245,19 +6418,19 @@ function readDocument(state) {
|
|
|
6245
6418
|
return;
|
|
6246
6419
|
}
|
|
6247
6420
|
}
|
|
6248
|
-
function loadDocuments(
|
|
6249
|
-
|
|
6421
|
+
function loadDocuments(input2, options) {
|
|
6422
|
+
input2 = String(input2);
|
|
6250
6423
|
options = options || {};
|
|
6251
|
-
if (
|
|
6252
|
-
if (
|
|
6253
|
-
|
|
6424
|
+
if (input2.length !== 0) {
|
|
6425
|
+
if (input2.charCodeAt(input2.length - 1) !== 10 && input2.charCodeAt(input2.length - 1) !== 13) {
|
|
6426
|
+
input2 += "\n";
|
|
6254
6427
|
}
|
|
6255
|
-
if (
|
|
6256
|
-
|
|
6428
|
+
if (input2.charCodeAt(0) === 65279) {
|
|
6429
|
+
input2 = input2.slice(1);
|
|
6257
6430
|
}
|
|
6258
6431
|
}
|
|
6259
|
-
var state = new State$1(
|
|
6260
|
-
var nullpos =
|
|
6432
|
+
var state = new State$1(input2, options);
|
|
6433
|
+
var nullpos = input2.indexOf("\0");
|
|
6261
6434
|
if (nullpos !== -1) {
|
|
6262
6435
|
state.position = nullpos;
|
|
6263
6436
|
throwError(state, "null byte is not allowed in input");
|
|
@@ -6272,12 +6445,12 @@ function loadDocuments(input, options) {
|
|
|
6272
6445
|
}
|
|
6273
6446
|
return state.documents;
|
|
6274
6447
|
}
|
|
6275
|
-
function loadAll$1(
|
|
6448
|
+
function loadAll$1(input2, iterator, options) {
|
|
6276
6449
|
if (iterator !== null && typeof iterator === "object" && typeof options === "undefined") {
|
|
6277
6450
|
options = iterator;
|
|
6278
6451
|
iterator = null;
|
|
6279
6452
|
}
|
|
6280
|
-
var documents = loadDocuments(
|
|
6453
|
+
var documents = loadDocuments(input2, options);
|
|
6281
6454
|
if (typeof iterator !== "function") {
|
|
6282
6455
|
return documents;
|
|
6283
6456
|
}
|
|
@@ -6285,8 +6458,8 @@ function loadAll$1(input, iterator, options) {
|
|
|
6285
6458
|
iterator(documents[index]);
|
|
6286
6459
|
}
|
|
6287
6460
|
}
|
|
6288
|
-
function load$1(
|
|
6289
|
-
var documents = loadDocuments(
|
|
6461
|
+
function load$1(input2, options) {
|
|
6462
|
+
var documents = loadDocuments(input2, options);
|
|
6290
6463
|
if (documents.length === 0) {
|
|
6291
6464
|
return void 0;
|
|
6292
6465
|
} else if (documents.length === 1) {
|
|
@@ -6907,11 +7080,11 @@ function inspectNode(object, objects, duplicatesIndexes) {
|
|
|
6907
7080
|
}
|
|
6908
7081
|
}
|
|
6909
7082
|
}
|
|
6910
|
-
function dump$1(
|
|
7083
|
+
function dump$1(input2, options) {
|
|
6911
7084
|
options = options || {};
|
|
6912
7085
|
var state = new State(options);
|
|
6913
|
-
if (!state.noRefs) getDuplicateReferences(
|
|
6914
|
-
var value =
|
|
7086
|
+
if (!state.noRefs) getDuplicateReferences(input2, state);
|
|
7087
|
+
var value = input2;
|
|
6915
7088
|
if (state.replacer) {
|
|
6916
7089
|
value = state.replacer.call({ "": value }, "", value);
|
|
6917
7090
|
}
|
|
@@ -7033,21 +7206,34 @@ function readStringArray(object, key) {
|
|
|
7033
7206
|
}
|
|
7034
7207
|
|
|
7035
7208
|
// src/memory.ts
|
|
7209
|
+
function parseMemoryKind(value) {
|
|
7210
|
+
if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
|
|
7211
|
+
return value;
|
|
7212
|
+
}
|
|
7213
|
+
throw new Error(`Unsupported memory kind "${value}". Expected durable, handoff, incident, preference, or smoke.`);
|
|
7214
|
+
}
|
|
7215
|
+
function parseMemoryStatus(value) {
|
|
7216
|
+
if (["active", "archived", "superseded"].includes(value)) {
|
|
7217
|
+
return value;
|
|
7218
|
+
}
|
|
7219
|
+
throw new Error(`Unsupported memory status "${value}". Expected active, archived, or superseded.`);
|
|
7220
|
+
}
|
|
7036
7221
|
async function runRemember(config, options) {
|
|
7037
7222
|
const text = await getInputText(options.text, options.stdin === true);
|
|
7038
7223
|
if (!text.trim()) {
|
|
7039
7224
|
throw new Error("Provide memory text with --text or --stdin.");
|
|
7040
7225
|
}
|
|
7041
|
-
const
|
|
7042
|
-
"
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7226
|
+
const metadata = {
|
|
7227
|
+
kind: options.kind ?? "durable",
|
|
7228
|
+
project: normalizeOptionalMetadata(options.project),
|
|
7229
|
+
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
7230
|
+
status: options.status ?? "active",
|
|
7231
|
+
supersedes: options.replace,
|
|
7232
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7233
|
+
topic: normalizeOptionalMetadata(options.topic)
|
|
7234
|
+
};
|
|
7235
|
+
const memory = formatMemoryDocument("MEMORY", metadata, text.trim());
|
|
7236
|
+
await storeMemory(config, memory, { dryRun: options.dryRun === true, metadata, replaceUri: options.replace });
|
|
7051
7237
|
}
|
|
7052
7238
|
async function runMigrateMemories(config, options) {
|
|
7053
7239
|
const dryRun = options.dryRun === true;
|
|
@@ -7098,7 +7284,7 @@ async function runMigrateMemories(config, options) {
|
|
|
7098
7284
|
if (!dryRun) {
|
|
7099
7285
|
await (0, import_promises4.writeFile)(migrationPath, candidate.text, { encoding: "utf8", mode: 384 });
|
|
7100
7286
|
await (0, import_promises4.chmod)(migrationPath, 384);
|
|
7101
|
-
await writeDurableMemoryFile(ov, config, memoryUri, migrationPath);
|
|
7287
|
+
await writeDurableMemoryFile(ov, config, memoryUri, migrationPath, "create");
|
|
7102
7288
|
existingHashes.add(candidate.hash);
|
|
7103
7289
|
}
|
|
7104
7290
|
migratedCount += 1;
|
|
@@ -7118,6 +7304,62 @@ async function runMigrateMemories(config, options) {
|
|
|
7118
7304
|
].join("; ")
|
|
7119
7305
|
);
|
|
7120
7306
|
}
|
|
7307
|
+
async function runMigrateLifecycle(config, options) {
|
|
7308
|
+
const dryRun = options.dryRun === true || options.apply !== true;
|
|
7309
|
+
const limit = options.limit ? parsePositiveInteger(options.limit, "lifecycle migration limit") : void 0;
|
|
7310
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
7311
|
+
const candidates = await legacyLifecycleHandoffCandidates(config);
|
|
7312
|
+
const migrationPath = (0, import_node_path4.join)(config.agentContextHome, "lifecycle-memory-migration.txt");
|
|
7313
|
+
let existingCount = 0;
|
|
7314
|
+
let migratedCount = 0;
|
|
7315
|
+
let skippedCount = 0;
|
|
7316
|
+
try {
|
|
7317
|
+
for (const candidate of candidates) {
|
|
7318
|
+
if (limit !== void 0 && migratedCount >= limit) {
|
|
7319
|
+
break;
|
|
7320
|
+
}
|
|
7321
|
+
const destinationUri = lifecycleMigrationUri(config, candidate.metadata, sha256(candidate.original.trim()));
|
|
7322
|
+
const migratedMemory = formatMemoryDocument(
|
|
7323
|
+
"HANDOFF",
|
|
7324
|
+
candidate.metadata,
|
|
7325
|
+
["Migrated legacy handoff from the historical events trail.", "", candidate.original.trim()].join("\n")
|
|
7326
|
+
);
|
|
7327
|
+
console.log(`${dryRun ? "Would migrate" : "Migrating"} ${candidate.sourceUri} -> ${destinationUri}`);
|
|
7328
|
+
if (!dryRun) {
|
|
7329
|
+
if (await vikingResourceExists(ov, config, destinationUri)) {
|
|
7330
|
+
existingCount += 1;
|
|
7331
|
+
console.log(`Archived copy already exists; cleaning up legacy source: ${candidate.sourceUri}`);
|
|
7332
|
+
} else {
|
|
7333
|
+
await (0, import_promises4.writeFile)(migrationPath, migratedMemory, { encoding: "utf8", mode: 384 });
|
|
7334
|
+
await (0, import_promises4.chmod)(migrationPath, 384);
|
|
7335
|
+
await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, candidate.metadata));
|
|
7336
|
+
await writeDurableMemoryFile(ov, config, destinationUri, migrationPath, "create");
|
|
7337
|
+
}
|
|
7338
|
+
const removedOriginal = await removeVikingResourceWithRetry(ov, config, candidate.sourceUri);
|
|
7339
|
+
if (!removedOriginal) {
|
|
7340
|
+
console.error(
|
|
7341
|
+
`Migrated copy stored, but original is still processing. Retry later: threadnote forget ${candidate.sourceUri}`
|
|
7342
|
+
);
|
|
7343
|
+
skippedCount += 1;
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
migratedCount += 1;
|
|
7347
|
+
}
|
|
7348
|
+
} finally {
|
|
7349
|
+
if (!dryRun) {
|
|
7350
|
+
await (0, import_promises4.rm)(migrationPath, { force: true });
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
console.log(
|
|
7354
|
+
[
|
|
7355
|
+
`Lifecycle migration summary: ${migratedCount} clear legacy handoff(s) ${dryRun ? "would be migrated" : "migrated"}`,
|
|
7356
|
+
`${existingCount} existing archived copy/copies reused`,
|
|
7357
|
+
`${skippedCount} legacy source(s) still processing`,
|
|
7358
|
+
`${candidates.length} clear legacy handoff candidate(s) found`,
|
|
7359
|
+
dryRun ? "Run with --apply to perform this migration." : void 0
|
|
7360
|
+
].filter((part) => part !== void 0).join("; ")
|
|
7361
|
+
);
|
|
7362
|
+
}
|
|
7121
7363
|
async function runRecall(config, options) {
|
|
7122
7364
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
7123
7365
|
const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, options.query));
|
|
@@ -7130,7 +7372,10 @@ async function runRecall(config, options) {
|
|
|
7130
7372
|
args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
|
|
7131
7373
|
}
|
|
7132
7374
|
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
7133
|
-
await printExactMemoryMatches(config, ov, options.query,
|
|
7375
|
+
await printExactMemoryMatches(config, ov, options.query, {
|
|
7376
|
+
dryRun: options.dryRun === true,
|
|
7377
|
+
includeArchived: options.includeArchived === true
|
|
7378
|
+
});
|
|
7134
7379
|
}
|
|
7135
7380
|
async function runRead(config, uri, options) {
|
|
7136
7381
|
assertVikingUri(uri);
|
|
@@ -7161,13 +7406,70 @@ async function runList(config, uri, options) {
|
|
|
7161
7406
|
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
7162
7407
|
}
|
|
7163
7408
|
async function runHandoff(config, options) {
|
|
7164
|
-
const handoff = await buildHandoff(options);
|
|
7165
|
-
await storeMemory(config, handoff, { dryRun: options.dryRun === true, replaceUri: options.replace });
|
|
7409
|
+
const { handoff, metadata } = await buildHandoff(options);
|
|
7410
|
+
await storeMemory(config, handoff, { dryRun: options.dryRun === true, metadata, replaceUri: options.replace });
|
|
7411
|
+
}
|
|
7412
|
+
async function runArchive(config, uri, options) {
|
|
7413
|
+
assertVikingUri(uri);
|
|
7414
|
+
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
7415
|
+
const readResult = await maybeRun(options.dryRun === true, ov, withIdentity(config, ["read", uri]));
|
|
7416
|
+
const original = readResult?.stdout.trim();
|
|
7417
|
+
if (options.dryRun === true) {
|
|
7418
|
+
const fallbackMetadata = {
|
|
7419
|
+
archivedFrom: uri,
|
|
7420
|
+
kind: options.kind ?? "handoff",
|
|
7421
|
+
project: normalizeOptionalMetadata(options.project),
|
|
7422
|
+
sourceAgentClient: "threadnote",
|
|
7423
|
+
status: "archived",
|
|
7424
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7425
|
+
topic: normalizeOptionalMetadata(options.topic)
|
|
7426
|
+
};
|
|
7427
|
+
const archiveMemory2 = formatMemoryDocument(
|
|
7428
|
+
"MEMORY",
|
|
7429
|
+
fallbackMetadata,
|
|
7430
|
+
["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n")
|
|
7431
|
+
);
|
|
7432
|
+
await storeMemory(config, archiveMemory2, { dryRun: true, metadata: fallbackMetadata });
|
|
7433
|
+
console.log(formatShellCommand(ov, withIdentity(config, ["rm", uri])));
|
|
7434
|
+
return;
|
|
7435
|
+
}
|
|
7436
|
+
if (!original) {
|
|
7437
|
+
throw new Error(`Could not read ${uri} before archiving.`);
|
|
7438
|
+
}
|
|
7439
|
+
const inferredMetadata = inferMemoryMetadata(original);
|
|
7440
|
+
const metadata = {
|
|
7441
|
+
archivedFrom: uri,
|
|
7442
|
+
kind: options.kind ?? inferredMetadata.kind ?? "handoff",
|
|
7443
|
+
project: normalizeOptionalMetadata(options.project) ?? inferredMetadata.project,
|
|
7444
|
+
sourceAgentClient: "threadnote",
|
|
7445
|
+
status: "archived",
|
|
7446
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7447
|
+
topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
|
|
7448
|
+
};
|
|
7449
|
+
const archiveMemory = formatMemoryDocument(
|
|
7450
|
+
"MEMORY",
|
|
7451
|
+
metadata,
|
|
7452
|
+
["Archived original Threadnote memory.", "", original].join("\n")
|
|
7453
|
+
);
|
|
7454
|
+
await storeMemory(config, archiveMemory, { dryRun: false, metadata });
|
|
7455
|
+
const removedOriginal = await removeVikingResourceWithRetry(ov, config, uri);
|
|
7456
|
+
if (removedOriginal) {
|
|
7457
|
+
console.log(`Archived original memory: ${uri}`);
|
|
7458
|
+
} else {
|
|
7459
|
+
console.error(`Archive stored, but original memory is still processing. Retry later: threadnote forget ${uri}`);
|
|
7460
|
+
}
|
|
7166
7461
|
}
|
|
7167
7462
|
async function runForget(config, uri, options) {
|
|
7168
7463
|
assertVikingUri(uri);
|
|
7169
7464
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
7170
|
-
|
|
7465
|
+
if (options.dryRun === true) {
|
|
7466
|
+
await maybeRun(true, ov, withIdentity(config, ["rm", uri]));
|
|
7467
|
+
return;
|
|
7468
|
+
}
|
|
7469
|
+
const removed = await removeVikingResourceWithRetry(ov, config, uri);
|
|
7470
|
+
if (!removed) {
|
|
7471
|
+
throw new Error(`Resource is still being processed; retry later: threadnote forget ${uri}`);
|
|
7472
|
+
}
|
|
7171
7473
|
}
|
|
7172
7474
|
async function runExportPack(config, options) {
|
|
7173
7475
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
@@ -7203,27 +7505,24 @@ async function inferProjectFromQuery(config, normalizedQuery) {
|
|
|
7203
7505
|
return void 0;
|
|
7204
7506
|
}
|
|
7205
7507
|
}
|
|
7206
|
-
async function printExactMemoryMatches(config, ov, query,
|
|
7508
|
+
async function printExactMemoryMatches(config, ov, query, options) {
|
|
7207
7509
|
const terms = exactRecallTerms(query);
|
|
7208
7510
|
if (terms.length === 0) {
|
|
7209
7511
|
return;
|
|
7210
7512
|
}
|
|
7211
|
-
const scopes =
|
|
7212
|
-
`viking://user/${uriSegment(config.user)}/memories`,
|
|
7213
|
-
`viking://agent/${uriSegment(config.agentId)}/memories`
|
|
7214
|
-
];
|
|
7513
|
+
const scopes = exactMemoryScopes(config, options.includeArchived);
|
|
7215
7514
|
const outputs = [];
|
|
7216
7515
|
for (const term of terms) {
|
|
7217
7516
|
for (const scope of scopes) {
|
|
7218
7517
|
const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
|
|
7219
|
-
if (dryRun) {
|
|
7518
|
+
if (options.dryRun) {
|
|
7220
7519
|
outputs.push(formatShellCommand(ov, args));
|
|
7221
7520
|
continue;
|
|
7222
7521
|
}
|
|
7223
7522
|
const result = await runCommand(ov, args, { allowFailure: true });
|
|
7224
|
-
const
|
|
7225
|
-
if (result.exitCode === 0 && grepOutputHasMatches(
|
|
7226
|
-
outputs.push(
|
|
7523
|
+
const output2 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
7524
|
+
if (result.exitCode === 0 && grepOutputHasMatches(output2)) {
|
|
7525
|
+
outputs.push(output2);
|
|
7227
7526
|
}
|
|
7228
7527
|
}
|
|
7229
7528
|
}
|
|
@@ -7239,7 +7538,8 @@ async function storeMemory(config, memory, options) {
|
|
|
7239
7538
|
}
|
|
7240
7539
|
const ov = await openVikingCliForMode(options.dryRun);
|
|
7241
7540
|
const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
|
|
7242
|
-
const memoryUri =
|
|
7541
|
+
const memoryUri = memoryUriFor(config, memory, options.metadata);
|
|
7542
|
+
const writeMode = await memoryWriteMode(ov, config, memoryUri, options.metadata);
|
|
7243
7543
|
if (options.dryRun) {
|
|
7244
7544
|
console.log(memory);
|
|
7245
7545
|
console.log("\nWould run:");
|
|
@@ -7252,36 +7552,44 @@ async function storeMemory(config, memory, options) {
|
|
|
7252
7552
|
"--from-file",
|
|
7253
7553
|
memoryPath,
|
|
7254
7554
|
"--mode",
|
|
7255
|
-
|
|
7555
|
+
writeMode,
|
|
7256
7556
|
"--wait",
|
|
7257
7557
|
"--timeout",
|
|
7258
7558
|
"120"
|
|
7259
7559
|
])
|
|
7260
7560
|
)
|
|
7261
7561
|
);
|
|
7262
|
-
if (options.replaceUri) {
|
|
7562
|
+
if (options.replaceUri && options.replaceUri !== memoryUri) {
|
|
7263
7563
|
console.log(formatShellCommand(ov, withIdentity(config, ["rm", options.replaceUri])));
|
|
7264
7564
|
}
|
|
7265
7565
|
return;
|
|
7266
7566
|
}
|
|
7267
7567
|
await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
|
|
7268
7568
|
await (0, import_promises4.chmod)(memoryPath, 384);
|
|
7269
|
-
await
|
|
7270
|
-
await writeDurableMemoryFile(ov, config, memoryUri, memoryPath);
|
|
7271
|
-
console.log(`Stored
|
|
7272
|
-
if (options.replaceUri) {
|
|
7273
|
-
await
|
|
7274
|
-
|
|
7569
|
+
await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, options.metadata));
|
|
7570
|
+
await writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode);
|
|
7571
|
+
console.log(`Stored memory: ${memoryUri}`);
|
|
7572
|
+
if (options.replaceUri && options.replaceUri !== memoryUri) {
|
|
7573
|
+
const removedReplacedMemory = await removeVikingResourceWithRetry(ov, config, options.replaceUri);
|
|
7574
|
+
if (removedReplacedMemory) {
|
|
7575
|
+
console.log(`Forgot replaced memory: ${options.replaceUri}`);
|
|
7576
|
+
} else {
|
|
7577
|
+
console.error(
|
|
7578
|
+
`Replacement stored, but the superseded memory is still processing. Retry later: threadnote forget ${options.replaceUri}`
|
|
7579
|
+
);
|
|
7580
|
+
}
|
|
7581
|
+
} else if (options.replaceUri === memoryUri) {
|
|
7582
|
+
console.log(`Updated existing memory in place: ${memoryUri}`);
|
|
7275
7583
|
}
|
|
7276
7584
|
}
|
|
7277
|
-
async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath) {
|
|
7585
|
+
async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
|
|
7278
7586
|
const args = withIdentity(config, [
|
|
7279
7587
|
"write",
|
|
7280
7588
|
memoryUri,
|
|
7281
7589
|
"--from-file",
|
|
7282
7590
|
memoryPath,
|
|
7283
7591
|
"--mode",
|
|
7284
|
-
|
|
7592
|
+
writeMode,
|
|
7285
7593
|
"--wait",
|
|
7286
7594
|
"--timeout",
|
|
7287
7595
|
"120"
|
|
@@ -7318,29 +7626,49 @@ async function waitForOpenVikingQueue(ov, config) {
|
|
|
7318
7626
|
console.error(result.stderr.trim());
|
|
7319
7627
|
}
|
|
7320
7628
|
}
|
|
7629
|
+
async function removeVikingResourceWithRetry(ov, config, uri) {
|
|
7630
|
+
const args = withIdentity(config, ["rm", uri]);
|
|
7631
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
7632
|
+
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
7633
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
7634
|
+
if (result.exitCode === 0) {
|
|
7635
|
+
if (result.stdout.trim()) {
|
|
7636
|
+
console.log(result.stdout.trim());
|
|
7637
|
+
}
|
|
7638
|
+
if (result.stderr.trim()) {
|
|
7639
|
+
console.error(result.stderr.trim());
|
|
7640
|
+
}
|
|
7641
|
+
return true;
|
|
7642
|
+
}
|
|
7643
|
+
if (isResourceBusy(result.stderr, result.stdout) && attempt === 3) {
|
|
7644
|
+
return false;
|
|
7645
|
+
}
|
|
7646
|
+
if (!isResourceBusy(result.stderr, result.stdout)) {
|
|
7647
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
7648
|
+
}
|
|
7649
|
+
await sleep(1e3 * (attempt + 1));
|
|
7650
|
+
}
|
|
7651
|
+
return false;
|
|
7652
|
+
}
|
|
7321
7653
|
async function vikingResourceExists(ov, config, uri) {
|
|
7322
7654
|
const stat3 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
7323
7655
|
return stat3.exitCode === 0;
|
|
7324
7656
|
}
|
|
7325
7657
|
async function ensureDurableMemoryDirectory(ov, config) {
|
|
7326
|
-
|
|
7327
|
-
const stat3 = await runCommand(ov, withIdentity(config, ["stat", directoryUri]), { allowFailure: true });
|
|
7328
|
-
if (stat3.exitCode === 0) {
|
|
7329
|
-
return;
|
|
7330
|
-
}
|
|
7331
|
-
await maybeRun(
|
|
7332
|
-
false,
|
|
7333
|
-
ov,
|
|
7334
|
-
withIdentity(config, [
|
|
7335
|
-
"mkdir",
|
|
7336
|
-
directoryUri,
|
|
7337
|
-
"--description",
|
|
7338
|
-
"Threadnote durable handoffs, memories, and cross-agent notes."
|
|
7339
|
-
])
|
|
7340
|
-
);
|
|
7658
|
+
await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
|
|
7341
7659
|
}
|
|
7342
|
-
function
|
|
7343
|
-
|
|
7660
|
+
async function ensureMemoryDirectory(ov, config, directoryUri) {
|
|
7661
|
+
for (const uri of vikingDirectoryChain(directoryUri)) {
|
|
7662
|
+
const statResult = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
7663
|
+
if (statResult.exitCode === 0) {
|
|
7664
|
+
continue;
|
|
7665
|
+
}
|
|
7666
|
+
await maybeRun(
|
|
7667
|
+
false,
|
|
7668
|
+
ov,
|
|
7669
|
+
withIdentity(config, ["mkdir", uri, "--description", "Threadnote lifecycle-aware local memories."])
|
|
7670
|
+
);
|
|
7671
|
+
}
|
|
7344
7672
|
}
|
|
7345
7673
|
function durableMemoryDirectoryUri(config) {
|
|
7346
7674
|
return `viking://user/${uriSegment(config.user)}/memories/events`;
|
|
@@ -7348,6 +7676,185 @@ function durableMemoryDirectoryUri(config) {
|
|
|
7348
7676
|
function migratedDurableMemoryUri(config, hash) {
|
|
7349
7677
|
return `${durableMemoryDirectoryUri(config)}/threadnote-migrated-${hash.slice(0, 16)}.md`;
|
|
7350
7678
|
}
|
|
7679
|
+
async function hasLegacyLifecycleHandoffCandidates(config) {
|
|
7680
|
+
return (await legacyLifecycleHandoffCandidates(config, 1)).length > 0;
|
|
7681
|
+
}
|
|
7682
|
+
async function legacyLifecycleHandoffCandidates(config, limit) {
|
|
7683
|
+
const eventsRoot = (0, import_node_path4.join)(localUserMemoriesRoot(config), "events");
|
|
7684
|
+
let entries;
|
|
7685
|
+
try {
|
|
7686
|
+
entries = await (0, import_promises4.readdir)(eventsRoot, { withFileTypes: true });
|
|
7687
|
+
} catch (_err) {
|
|
7688
|
+
return [];
|
|
7689
|
+
}
|
|
7690
|
+
const candidates = [];
|
|
7691
|
+
for (const entry of entries) {
|
|
7692
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
|
|
7693
|
+
continue;
|
|
7694
|
+
}
|
|
7695
|
+
const sourcePath = (0, import_node_path4.join)(eventsRoot, entry.name);
|
|
7696
|
+
const original = await readTextIfExists(sourcePath);
|
|
7697
|
+
if (!original || !isClearLegacyHandoffMemory(original) || sensitiveMemoryReason(original)) {
|
|
7698
|
+
continue;
|
|
7699
|
+
}
|
|
7700
|
+
const sourceUri = `${durableMemoryDirectoryUri(config)}/${entry.name}`;
|
|
7701
|
+
candidates.push({
|
|
7702
|
+
metadata: {
|
|
7703
|
+
archivedFrom: sourceUri,
|
|
7704
|
+
kind: "handoff",
|
|
7705
|
+
project: inferLegacyProject(original),
|
|
7706
|
+
sourceAgentClient: "threadnote",
|
|
7707
|
+
status: "archived",
|
|
7708
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7709
|
+
},
|
|
7710
|
+
original,
|
|
7711
|
+
sourceUri
|
|
7712
|
+
});
|
|
7713
|
+
if (limit !== void 0 && candidates.length >= limit) {
|
|
7714
|
+
break;
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7717
|
+
return candidates;
|
|
7718
|
+
}
|
|
7719
|
+
function lifecycleMigrationUri(config, metadata, hash) {
|
|
7720
|
+
return `${memoryDirectoryUri(config, metadata)}/legacy-${hash.slice(0, 16)}.md`;
|
|
7721
|
+
}
|
|
7722
|
+
function exactMemoryScopes(config, includeArchived) {
|
|
7723
|
+
const userBase = `viking://user/${uriSegment(config.user)}/memories`;
|
|
7724
|
+
const scopes = [
|
|
7725
|
+
`${userBase}/preferences`,
|
|
7726
|
+
`${userBase}/durable/projects`,
|
|
7727
|
+
`${userBase}/handoffs/active`,
|
|
7728
|
+
`${userBase}/incidents/active`,
|
|
7729
|
+
`${userBase}/events`,
|
|
7730
|
+
`viking://agent/${uriSegment(config.agentId)}/memories`
|
|
7731
|
+
];
|
|
7732
|
+
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
7733
|
+
}
|
|
7734
|
+
function memoryUriFor(config, memory, metadata) {
|
|
7735
|
+
const filename = shouldUseStableMemoryUri(metadata) ? `${uriSegment(metadata.topic ?? "current")}.md` : `threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
|
|
7736
|
+
return `${memoryDirectoryUri(config, metadata)}/${filename}`;
|
|
7737
|
+
}
|
|
7738
|
+
function memoryDirectoryUri(config, metadata) {
|
|
7739
|
+
const baseUri = `viking://user/${uriSegment(config.user)}/memories`;
|
|
7740
|
+
const projectSegment = uriSegment(metadata.project ?? "general");
|
|
7741
|
+
switch (metadata.kind) {
|
|
7742
|
+
case "preference":
|
|
7743
|
+
return metadata.status === "active" ? `${baseUri}/preferences` : `${baseUri}/preferences/${uriSegment(metadata.status)}`;
|
|
7744
|
+
case "handoff":
|
|
7745
|
+
return `${baseUri}/handoffs/${uriSegment(metadata.status)}/${projectSegment}`;
|
|
7746
|
+
case "incident":
|
|
7747
|
+
return `${baseUri}/incidents/${uriSegment(metadata.status)}/${projectSegment}`;
|
|
7748
|
+
case "smoke":
|
|
7749
|
+
return `${baseUri}/smoke/${uriSegment(metadata.status)}`;
|
|
7750
|
+
case "durable":
|
|
7751
|
+
return metadata.status === "active" ? `${baseUri}/durable/projects/${projectSegment}` : `${baseUri}/durable/${uriSegment(metadata.status)}/${projectSegment}`;
|
|
7752
|
+
}
|
|
7753
|
+
}
|
|
7754
|
+
function shouldUseStableMemoryUri(metadata) {
|
|
7755
|
+
return metadata.status === "active" && metadata.topic !== void 0 && metadata.kind !== "smoke";
|
|
7756
|
+
}
|
|
7757
|
+
async function memoryWriteMode(ov, config, memoryUri, metadata) {
|
|
7758
|
+
if (!shouldUseStableMemoryUri(metadata)) {
|
|
7759
|
+
return "create";
|
|
7760
|
+
}
|
|
7761
|
+
return await vikingResourceExists(ov, config, memoryUri) ? "replace" : "create";
|
|
7762
|
+
}
|
|
7763
|
+
function vikingDirectoryChain(directoryUri) {
|
|
7764
|
+
const prefix = "viking://";
|
|
7765
|
+
if (!directoryUri.startsWith(prefix)) {
|
|
7766
|
+
return [directoryUri];
|
|
7767
|
+
}
|
|
7768
|
+
const parts = directoryUri.slice(prefix.length).split("/").filter(Boolean);
|
|
7769
|
+
const startIndex = parts[0] === "user" && parts.length > 2 ? 3 : 1;
|
|
7770
|
+
const chain = [];
|
|
7771
|
+
for (let index = startIndex; index <= parts.length; index += 1) {
|
|
7772
|
+
chain.push(`${prefix}${parts.slice(0, index).join("/")}`);
|
|
7773
|
+
}
|
|
7774
|
+
return chain;
|
|
7775
|
+
}
|
|
7776
|
+
function formatMemoryDocument(title, metadata, body) {
|
|
7777
|
+
const header = [
|
|
7778
|
+
title,
|
|
7779
|
+
`kind: ${metadata.kind}`,
|
|
7780
|
+
`status: ${metadata.status}`,
|
|
7781
|
+
metadata.project ? `project: ${metadata.project}` : void 0,
|
|
7782
|
+
metadata.topic ? `topic: ${metadata.topic}` : void 0,
|
|
7783
|
+
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
7784
|
+
`timestamp: ${metadata.timestamp}`,
|
|
7785
|
+
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
7786
|
+
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
7787
|
+
].filter((line) => line !== void 0);
|
|
7788
|
+
return [...header, "", body.trim()].join("\n");
|
|
7789
|
+
}
|
|
7790
|
+
function inferMemoryMetadata(memory) {
|
|
7791
|
+
const header = memory.slice(0, Math.max(0, memory.indexOf("\n\n")) || memory.length);
|
|
7792
|
+
const firstLine2 = header.split("\n")[0]?.trim();
|
|
7793
|
+
const kind = parseOptionalMemoryKind(parseHeaderValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
|
|
7794
|
+
const status = parseOptionalMemoryStatus(parseHeaderValue(header, "status"));
|
|
7795
|
+
const project = normalizeOptionalMetadata(parseHeaderValue(header, "project")) ?? normalizeOptionalMetadata(parseHeaderValue(header, "repo"));
|
|
7796
|
+
const topic = normalizeOptionalMetadata(parseHeaderValue(header, "topic")) ?? normalizeOptionalMetadata(parseHeaderValue(header, "task"));
|
|
7797
|
+
return {
|
|
7798
|
+
kind,
|
|
7799
|
+
project,
|
|
7800
|
+
sourceAgentClient: parseHeaderValue(header, "source_agent_client") ?? void 0,
|
|
7801
|
+
status,
|
|
7802
|
+
topic
|
|
7803
|
+
};
|
|
7804
|
+
}
|
|
7805
|
+
function parseHeaderValue(header, key) {
|
|
7806
|
+
const prefix = `${key}:`;
|
|
7807
|
+
return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
|
|
7808
|
+
}
|
|
7809
|
+
function isClearLegacyHandoffMemory(memory) {
|
|
7810
|
+
if (/^kind:\s*/m.test(memory) || /^status:\s*/m.test(memory)) {
|
|
7811
|
+
return false;
|
|
7812
|
+
}
|
|
7813
|
+
const trimmed = memory.trim();
|
|
7814
|
+
if (trimmed.startsWith("HANDOFF\n")) {
|
|
7815
|
+
return true;
|
|
7816
|
+
}
|
|
7817
|
+
if (!trimmed.startsWith("MEMORY\n")) {
|
|
7818
|
+
return false;
|
|
7819
|
+
}
|
|
7820
|
+
return /^(?:#+\s*)?(?:final\s+)?handoff(?:\s+update)?\b/i.test(memoryBody(trimmed));
|
|
7821
|
+
}
|
|
7822
|
+
function memoryBody(memory) {
|
|
7823
|
+
const separatorIndex = memory.indexOf("\n\n");
|
|
7824
|
+
return separatorIndex === -1 ? "" : memory.slice(separatorIndex + 2).trim();
|
|
7825
|
+
}
|
|
7826
|
+
function inferLegacyProject(memory) {
|
|
7827
|
+
const explicit = parseHeaderValue(memory, "project") ?? parseHeaderValue(memory, "repo") ?? parseHeaderValue(memory, "repo_path") ?? /\brepo(?:_path)?\s+([~/A-Za-z0-9_.:/-]+)/.exec(memory)?.[1];
|
|
7828
|
+
if (!explicit) {
|
|
7829
|
+
return "general";
|
|
7830
|
+
}
|
|
7831
|
+
const trimmed = explicit.trim().replace(/[`.,;]+$/g, "");
|
|
7832
|
+
return trimmed.includes("/") ? (0, import_node_path4.basename)(trimmed) : trimmed;
|
|
7833
|
+
}
|
|
7834
|
+
function parseOptionalMemoryKind(value) {
|
|
7835
|
+
if (!value) {
|
|
7836
|
+
return void 0;
|
|
7837
|
+
}
|
|
7838
|
+
try {
|
|
7839
|
+
return parseMemoryKind(value);
|
|
7840
|
+
} catch (_err) {
|
|
7841
|
+
return void 0;
|
|
7842
|
+
}
|
|
7843
|
+
}
|
|
7844
|
+
function parseOptionalMemoryStatus(value) {
|
|
7845
|
+
if (!value) {
|
|
7846
|
+
return void 0;
|
|
7847
|
+
}
|
|
7848
|
+
try {
|
|
7849
|
+
return parseMemoryStatus(value);
|
|
7850
|
+
} catch (_err) {
|
|
7851
|
+
return void 0;
|
|
7852
|
+
}
|
|
7853
|
+
}
|
|
7854
|
+
function normalizeOptionalMetadata(value) {
|
|
7855
|
+
const trimmed = value?.trim();
|
|
7856
|
+
return trimmed ? trimmed : void 0;
|
|
7857
|
+
}
|
|
7351
7858
|
async function legacySourceAccounts(config, options) {
|
|
7352
7859
|
const explicitAccounts = options.sourceAccount?.filter((account) => account.trim().length > 0) ?? [];
|
|
7353
7860
|
if (explicitAccounts.length > 0) {
|
|
@@ -7500,12 +8007,16 @@ function legacySourceLabel(candidate) {
|
|
|
7500
8007
|
function localVikingDataRoot(config) {
|
|
7501
8008
|
return (0, import_node_path4.join)(config.agentContextHome, "data", "viking");
|
|
7502
8009
|
}
|
|
8010
|
+
function localUserMemoriesRoot(config) {
|
|
8011
|
+
return (0, import_node_path4.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
|
|
8012
|
+
}
|
|
7503
8013
|
function uniqueStrings(values) {
|
|
7504
8014
|
return [...new Set(values)].sort();
|
|
7505
8015
|
}
|
|
7506
8016
|
function isResourceBusy(stderr, stdout) {
|
|
7507
|
-
|
|
7508
|
-
${stdout}`.
|
|
8017
|
+
const output2 = `${stderr}
|
|
8018
|
+
${stdout}`.toLowerCase();
|
|
8019
|
+
return output2.includes("resource is busy") || output2.includes("resource is being processed");
|
|
7509
8020
|
}
|
|
7510
8021
|
async function buildHandoff(options) {
|
|
7511
8022
|
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]) ?? getInvocationCwd();
|
|
@@ -7513,19 +8024,20 @@ async function buildHandoff(options) {
|
|
|
7513
8024
|
const status = await gitValue(["status", "--short"], repoRoot) ?? "";
|
|
7514
8025
|
const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
|
|
7515
8026
|
const touchedFiles = await gitTouchedFiles(repoRoot);
|
|
7516
|
-
const
|
|
7517
|
-
|
|
7518
|
-
|
|
8027
|
+
const repoName = (0, import_node_path4.basename)(repoRoot);
|
|
8028
|
+
const metadata = {
|
|
8029
|
+
kind: "handoff",
|
|
8030
|
+
project: normalizeOptionalMetadata(options.project) ?? repoName,
|
|
8031
|
+
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
8032
|
+
status: "active",
|
|
8033
|
+
supersedes: options.replace,
|
|
8034
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8035
|
+
topic: normalizeOptionalMetadata(options.topic)
|
|
8036
|
+
};
|
|
8037
|
+
const body = [
|
|
8038
|
+
`repo: ${repoName}`,
|
|
7519
8039
|
`repo_path: ${repoRoot}`,
|
|
7520
8040
|
`branch: ${branch || "unknown"}`,
|
|
7521
|
-
`source_agent_client: ${options.sourceAgentClient ?? "codex"}`,
|
|
7522
|
-
`timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
7523
|
-
];
|
|
7524
|
-
if (options.replace) {
|
|
7525
|
-
header.push(`supersedes: ${options.replace}`);
|
|
7526
|
-
}
|
|
7527
|
-
return [
|
|
7528
|
-
...header,
|
|
7529
8041
|
`task: ${options.task ?? "unspecified"}`,
|
|
7530
8042
|
"",
|
|
7531
8043
|
"files_touched:",
|
|
@@ -7546,6 +8058,7 @@ async function buildHandoff(options) {
|
|
|
7546
8058
|
"next_step:",
|
|
7547
8059
|
options.nextStep ?? "- inspect the current repo state and continue from this handoff"
|
|
7548
8060
|
].join("\n");
|
|
8061
|
+
return { handoff: formatMemoryDocument("HANDOFF", metadata, body), metadata };
|
|
7549
8062
|
}
|
|
7550
8063
|
async function gitTouchedFiles(cwd) {
|
|
7551
8064
|
const changedFiles = await gitValue(["diff", "--name-only", "HEAD"], cwd);
|
|
@@ -7637,14 +8150,14 @@ async function runInitManifest(config, options) {
|
|
|
7637
8150
|
uri: existingManifest.futureMonorepo.uri
|
|
7638
8151
|
};
|
|
7639
8152
|
}
|
|
7640
|
-
const
|
|
8153
|
+
const output2 = jsYaml.dump(outputManifest, { lineWidth: 120, noRefs: true });
|
|
7641
8154
|
if (options.dryRun === true) {
|
|
7642
8155
|
console.log(`# Would write ${manifestPath}`);
|
|
7643
|
-
console.log(
|
|
8156
|
+
console.log(output2.trimEnd());
|
|
7644
8157
|
return;
|
|
7645
8158
|
}
|
|
7646
8159
|
await ensureDirectory((0, import_node_path5.dirname)(manifestPath), false);
|
|
7647
|
-
await (0, import_promises5.writeFile)(manifestPath,
|
|
8160
|
+
await (0, import_promises5.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
7648
8161
|
await (0, import_promises5.chmod)(manifestPath, 384);
|
|
7649
8162
|
console.log(`Wrote manifest: ${manifestPath}`);
|
|
7650
8163
|
console.log("Seed with:");
|
|
@@ -7855,11 +8368,11 @@ function redactJsonValue(value) {
|
|
|
7855
8368
|
if (!isJsonObject(value)) {
|
|
7856
8369
|
return value;
|
|
7857
8370
|
}
|
|
7858
|
-
const
|
|
8371
|
+
const output2 = {};
|
|
7859
8372
|
for (const [key, nestedValue] of Object.entries(value)) {
|
|
7860
|
-
|
|
8373
|
+
output2[key] = isSensitiveKey(key) ? "[REDACTED]" : redactJsonValue(nestedValue);
|
|
7861
8374
|
}
|
|
7862
|
-
return
|
|
8375
|
+
return output2;
|
|
7863
8376
|
}
|
|
7864
8377
|
function isSensitiveKey(key) {
|
|
7865
8378
|
return /token|secret|password|credential|authorization|api[_-]?key|client[_-]?secret|bearer/i.test(key);
|
|
@@ -7884,14 +8397,477 @@ function detectSecretMatches(content) {
|
|
|
7884
8397
|
|
|
7885
8398
|
// src/lifecycle.ts
|
|
7886
8399
|
var import_node_child_process2 = require("node:child_process");
|
|
7887
|
-
var
|
|
8400
|
+
var import_node_fs5 = require("node:fs");
|
|
8401
|
+
var import_promises8 = require("node:fs/promises");
|
|
8402
|
+
var import_node_os5 = require("node:os");
|
|
8403
|
+
var import_node_path7 = require("node:path");
|
|
8404
|
+
|
|
8405
|
+
// src/update.ts
|
|
8406
|
+
var import_node_fs4 = require("node:fs");
|
|
7888
8407
|
var import_promises6 = require("node:fs/promises");
|
|
7889
8408
|
var import_node_os4 = require("node:os");
|
|
7890
8409
|
var import_node_path6 = require("node:path");
|
|
8410
|
+
var import_promises7 = require("node:readline/promises");
|
|
8411
|
+
var import_node_process = require("node:process");
|
|
8412
|
+
var NPM_PACKAGE_NAME = "threadnote";
|
|
8413
|
+
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
8414
|
+
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
8415
|
+
var POST_UPDATE_MIGRATIONS_FILE = "post-update-migrations.json";
|
|
8416
|
+
var POST_UPDATE_STATE_FILE = "post-update-state.json";
|
|
8417
|
+
function parseUpdateRuntime(value) {
|
|
8418
|
+
if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
|
|
8419
|
+
return value;
|
|
8420
|
+
}
|
|
8421
|
+
throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
|
|
8422
|
+
}
|
|
8423
|
+
async function maybeNotifyUpdate(config, options = {}) {
|
|
8424
|
+
if (isUpdateNotificationDisabled()) {
|
|
8425
|
+
return;
|
|
8426
|
+
}
|
|
8427
|
+
try {
|
|
8428
|
+
const info = await getUpdateInfo(config, {
|
|
8429
|
+
allowCacheWrite: options.dryRun !== true,
|
|
8430
|
+
preferFresh: false,
|
|
8431
|
+
registry: updateRegistry()
|
|
8432
|
+
});
|
|
8433
|
+
if (!info.isUpdateAvailable) {
|
|
8434
|
+
return;
|
|
8435
|
+
}
|
|
8436
|
+
console.log("");
|
|
8437
|
+
console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
|
|
8438
|
+
console.log("Run: threadnote update");
|
|
8439
|
+
} catch (_err) {
|
|
8440
|
+
return;
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
async function runUpdate(config, options) {
|
|
8444
|
+
const registry = normalizeRegistry(options.registry ?? updateRegistry());
|
|
8445
|
+
const info = await getUpdateInfo(config, {
|
|
8446
|
+
allowCacheWrite: options.dryRun !== true,
|
|
8447
|
+
preferFresh: true,
|
|
8448
|
+
registry
|
|
8449
|
+
});
|
|
8450
|
+
console.log(`Current version: ${info.currentVersion}`);
|
|
8451
|
+
console.log(`Latest version: ${info.latestVersion}`);
|
|
8452
|
+
console.log(`Registry: ${info.registry}`);
|
|
8453
|
+
if (options.check === true) {
|
|
8454
|
+
if (info.isUpdateAvailable) {
|
|
8455
|
+
console.log(`Update available. Run: threadnote update`);
|
|
8456
|
+
} else {
|
|
8457
|
+
console.log(
|
|
8458
|
+
compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
|
|
8459
|
+
);
|
|
8460
|
+
}
|
|
8461
|
+
return;
|
|
8462
|
+
}
|
|
8463
|
+
if (!info.isUpdateAvailable && options.force !== true) {
|
|
8464
|
+
console.log("Threadnote is up to date.");
|
|
8465
|
+
return;
|
|
8466
|
+
}
|
|
8467
|
+
const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
|
|
8468
|
+
const updateCommand = updatePackageCommand(runtime, registry);
|
|
8469
|
+
await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
|
|
8470
|
+
if (options.repair === false) {
|
|
8471
|
+
console.log("Skipping repair because --no-repair was provided.");
|
|
8472
|
+
return;
|
|
8473
|
+
}
|
|
8474
|
+
const threadnoteCommand = await installedThreadnoteCommand(runtime);
|
|
8475
|
+
await maybeRun(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
|
|
8476
|
+
if (options.postUpdate !== false) {
|
|
8477
|
+
const postUpdateArgs = [
|
|
8478
|
+
"post-update",
|
|
8479
|
+
"--from-version",
|
|
8480
|
+
info.currentVersion,
|
|
8481
|
+
"--to-version",
|
|
8482
|
+
info.latestVersion,
|
|
8483
|
+
...options.yes === true ? ["--yes"] : []
|
|
8484
|
+
];
|
|
8485
|
+
if (options.dryRun === true) {
|
|
8486
|
+
await maybeRun(true, threadnoteCommand, postUpdateArgs);
|
|
8487
|
+
} else {
|
|
8488
|
+
console.log(`Running: ${formatShellCommand(threadnoteCommand, postUpdateArgs)}`);
|
|
8489
|
+
const postUpdateExitCode = await runInteractive(threadnoteCommand, postUpdateArgs);
|
|
8490
|
+
if (postUpdateExitCode !== 0) {
|
|
8491
|
+
throw new Error(`${formatShellCommand(threadnoteCommand, postUpdateArgs)} exited with ${postUpdateExitCode}.`);
|
|
8492
|
+
}
|
|
8493
|
+
}
|
|
8494
|
+
} else {
|
|
8495
|
+
console.log("Skipping post-update migration prompts because --no-post-update was provided.");
|
|
8496
|
+
}
|
|
8497
|
+
console.log(
|
|
8498
|
+
"Update complete. Restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload."
|
|
8499
|
+
);
|
|
8500
|
+
}
|
|
8501
|
+
async function runPostUpdate(config, options) {
|
|
8502
|
+
if (!options.fromVersion || !options.toVersion) {
|
|
8503
|
+
throw new Error("Provide --from-version and --to-version for post-update.");
|
|
8504
|
+
}
|
|
8505
|
+
await runApplicablePostUpdateMigrations(config, {
|
|
8506
|
+
dryRun: options.dryRun === true,
|
|
8507
|
+
fromVersion: options.fromVersion,
|
|
8508
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
8509
|
+
markHandled: true,
|
|
8510
|
+
toVersion: options.toVersion,
|
|
8511
|
+
yes: options.yes === true
|
|
8512
|
+
});
|
|
8513
|
+
}
|
|
8514
|
+
async function maybeRunPostUpdateAfterRepair(config, options) {
|
|
8515
|
+
const toVersion = await currentPackageVersion();
|
|
8516
|
+
const state = await readPostUpdateState(config);
|
|
8517
|
+
const migrations = await applicablePostUpdateMigrations(config, {
|
|
8518
|
+
fromVersion: "0.0.0",
|
|
8519
|
+
handledMigrationIds: state.handledMigrationIds,
|
|
8520
|
+
toVersion
|
|
8521
|
+
});
|
|
8522
|
+
if (migrations.length === 0) {
|
|
8523
|
+
return;
|
|
8524
|
+
}
|
|
8525
|
+
console.log("");
|
|
8526
|
+
console.log("Repair found package post-update migrations.");
|
|
8527
|
+
console.log("This also covers updates launched by older Threadnote versions that only knew how to run repair.");
|
|
8528
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
8529
|
+
console.log(
|
|
8530
|
+
"This process is non-interactive, so Threadnote will print the manual migration command instead of prompting."
|
|
8531
|
+
);
|
|
8532
|
+
console.log(`Run the prompt manually with: threadnote post-update --from-version 0.0.0 --to-version ${toVersion}`);
|
|
8533
|
+
}
|
|
8534
|
+
await runApplicablePostUpdateMigrations(config, {
|
|
8535
|
+
dryRun: options.dryRun,
|
|
8536
|
+
fromVersion: "0.0.0",
|
|
8537
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
8538
|
+
markHandled: true,
|
|
8539
|
+
toVersion,
|
|
8540
|
+
yes: false
|
|
8541
|
+
});
|
|
8542
|
+
}
|
|
8543
|
+
async function getUpdateInfo(config, options) {
|
|
8544
|
+
const currentVersion = await currentPackageVersion();
|
|
8545
|
+
const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
|
|
8546
|
+
const latestVersion = cached?.latestVersion ?? await fetchLatestVersion(options.registry);
|
|
8547
|
+
if (!cached && options.allowCacheWrite) {
|
|
8548
|
+
await writeUpdateCache(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
|
|
8549
|
+
}
|
|
8550
|
+
return {
|
|
8551
|
+
currentVersion,
|
|
8552
|
+
isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
|
|
8553
|
+
latestVersion,
|
|
8554
|
+
registry: options.registry
|
|
8555
|
+
};
|
|
8556
|
+
}
|
|
8557
|
+
async function currentPackageVersion() {
|
|
8558
|
+
const rawPackage = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), "package.json"), "utf8");
|
|
8559
|
+
const parsed = JSON.parse(rawPackage);
|
|
8560
|
+
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8561
|
+
throw new Error("Could not read current threadnote package version.");
|
|
8562
|
+
}
|
|
8563
|
+
return parsed.version;
|
|
8564
|
+
}
|
|
8565
|
+
async function fetchLatestVersion(registry) {
|
|
8566
|
+
const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
|
|
8567
|
+
const controller = new AbortController();
|
|
8568
|
+
const timeout = setTimeout(() => {
|
|
8569
|
+
controller.abort();
|
|
8570
|
+
}, 2500);
|
|
8571
|
+
try {
|
|
8572
|
+
const response = await fetch(url, { headers: { accept: "application/json" }, signal: controller.signal });
|
|
8573
|
+
if (!response.ok) {
|
|
8574
|
+
throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
8575
|
+
}
|
|
8576
|
+
const parsed = await response.json();
|
|
8577
|
+
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8578
|
+
throw new Error("npm registry response did not include a version.");
|
|
8579
|
+
}
|
|
8580
|
+
return parsed.version;
|
|
8581
|
+
} catch (err) {
|
|
8582
|
+
throw new Error(`Could not check npm for updates: ${errorMessage(err)}`, { cause: err });
|
|
8583
|
+
} finally {
|
|
8584
|
+
clearTimeout(timeout);
|
|
8585
|
+
}
|
|
8586
|
+
}
|
|
8587
|
+
async function readFreshCache(config, registry) {
|
|
8588
|
+
const rawCache = await readFileIfExists(updateCachePath(config));
|
|
8589
|
+
if (!rawCache) {
|
|
8590
|
+
return void 0;
|
|
8591
|
+
}
|
|
8592
|
+
try {
|
|
8593
|
+
const parsed = JSON.parse(rawCache);
|
|
8594
|
+
if (!isJsonObject(parsed) || typeof parsed.checkedAt !== "string" || typeof parsed.latestVersion !== "string" || parsed.registry !== registry) {
|
|
8595
|
+
return void 0;
|
|
8596
|
+
}
|
|
8597
|
+
const checkedAt = Date.parse(parsed.checkedAt);
|
|
8598
|
+
if (!Number.isFinite(checkedAt) || Date.now() - checkedAt > UPDATE_CHECK_TTL_MS) {
|
|
8599
|
+
return void 0;
|
|
8600
|
+
}
|
|
8601
|
+
return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, registry };
|
|
8602
|
+
} catch (_err) {
|
|
8603
|
+
return void 0;
|
|
8604
|
+
}
|
|
8605
|
+
}
|
|
8606
|
+
async function writeUpdateCache(config, cache) {
|
|
8607
|
+
await ensureDirectory(config.agentContextHome, false);
|
|
8608
|
+
await (0, import_promises6.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
|
|
8609
|
+
`, { encoding: "utf8", mode: 384 });
|
|
8610
|
+
}
|
|
8611
|
+
function updateCachePath(config) {
|
|
8612
|
+
return (0, import_node_path6.join)(config.agentContextHome, "update-check.json");
|
|
8613
|
+
}
|
|
8614
|
+
async function runApplicablePostUpdateMigrations(config, options) {
|
|
8615
|
+
const state = await readPostUpdateState(config);
|
|
8616
|
+
const migrations = await applicablePostUpdateMigrations(config, {
|
|
8617
|
+
fromVersion: options.fromVersion,
|
|
8618
|
+
handledMigrationIds: state.handledMigrationIds,
|
|
8619
|
+
toVersion: options.toVersion
|
|
8620
|
+
});
|
|
8621
|
+
if (migrations.length === 0) {
|
|
8622
|
+
console.log("No post-update memory migrations apply.");
|
|
8623
|
+
return;
|
|
8624
|
+
}
|
|
8625
|
+
console.log("");
|
|
8626
|
+
console.log("Post-update memory migrations are available.");
|
|
8627
|
+
const threadnoteCommand = currentThreadnoteCommand() ?? await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
|
|
8628
|
+
const handledMigrationIds = new Set(state.handledMigrationIds);
|
|
8629
|
+
for (const migration of migrations) {
|
|
8630
|
+
printPostUpdateMigration(migration);
|
|
8631
|
+
const accepted = options.dryRun || options.yes || options.interactive && await confirmPostUpdateMigration("Apply this migration now? [y/N] ");
|
|
8632
|
+
if (!accepted) {
|
|
8633
|
+
console.log("Skipped. Run manually later:");
|
|
8634
|
+
console.log(` ${formatMigrationCommand(threadnoteCommand, migration.commandArgs)}`);
|
|
8635
|
+
continue;
|
|
8636
|
+
}
|
|
8637
|
+
await maybeRun(options.dryRun, threadnoteCommand, migration.commandArgs);
|
|
8638
|
+
if (!options.dryRun) {
|
|
8639
|
+
handledMigrationIds.add(migration.id);
|
|
8640
|
+
for (const instruction of migration.instructions) {
|
|
8641
|
+
console.log(instruction);
|
|
8642
|
+
}
|
|
8643
|
+
} else {
|
|
8644
|
+
console.log("After this migration succeeds, Threadnote will print:");
|
|
8645
|
+
for (const instruction of migration.instructions) {
|
|
8646
|
+
console.log(` ${instruction}`);
|
|
8647
|
+
}
|
|
8648
|
+
}
|
|
8649
|
+
}
|
|
8650
|
+
if (!options.dryRun && options.markHandled) {
|
|
8651
|
+
await writePostUpdateState(config, { handledMigrationIds: [...handledMigrationIds].sort() });
|
|
8652
|
+
}
|
|
8653
|
+
}
|
|
8654
|
+
async function applicablePostUpdateMigrations(config, options) {
|
|
8655
|
+
const migrations = await readPostUpdateMigrations();
|
|
8656
|
+
const handled = new Set(options.handledMigrationIds);
|
|
8657
|
+
const applicable = [];
|
|
8658
|
+
for (const migration of migrations) {
|
|
8659
|
+
if (handled.has(migration.id)) {
|
|
8660
|
+
continue;
|
|
8661
|
+
}
|
|
8662
|
+
if (compareVersions(options.fromVersion, migration.introducedIn) >= 0) {
|
|
8663
|
+
continue;
|
|
8664
|
+
}
|
|
8665
|
+
if (compareVersions(migration.introducedIn, options.toVersion) > 0) {
|
|
8666
|
+
continue;
|
|
8667
|
+
}
|
|
8668
|
+
if (migration.requiresLegacyHandoffs === true && !await hasLegacyLifecycleHandoffCandidates(config)) {
|
|
8669
|
+
continue;
|
|
8670
|
+
}
|
|
8671
|
+
applicable.push(migration);
|
|
8672
|
+
}
|
|
8673
|
+
return applicable;
|
|
8674
|
+
}
|
|
8675
|
+
async function readPostUpdateMigrations() {
|
|
8676
|
+
const raw = await readFileIfExists((0, import_node_path6.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
|
|
8677
|
+
if (!raw) {
|
|
8678
|
+
return [];
|
|
8679
|
+
}
|
|
8680
|
+
const parsed = JSON.parse(raw);
|
|
8681
|
+
if (!isJsonObject(parsed) || !Array.isArray(parsed.migrations)) {
|
|
8682
|
+
throw new Error(`${POST_UPDATE_MIGRATIONS_FILE} must contain a migrations array.`);
|
|
8683
|
+
}
|
|
8684
|
+
return parsed.migrations.map(parsePostUpdateMigration);
|
|
8685
|
+
}
|
|
8686
|
+
function parsePostUpdateMigration(value) {
|
|
8687
|
+
if (!isJsonObject(value) || typeof value.id !== "string" || typeof value.introducedIn !== "string" || typeof value.title !== "string" || !Array.isArray(value.description) || !Array.isArray(value.commandArgs) || !Array.isArray(value.instructions)) {
|
|
8688
|
+
throw new Error(`Invalid entry in ${POST_UPDATE_MIGRATIONS_FILE}.`);
|
|
8689
|
+
}
|
|
8690
|
+
return {
|
|
8691
|
+
commandArgs: stringArray(value, "commandArgs"),
|
|
8692
|
+
description: stringArray(value, "description"),
|
|
8693
|
+
id: value.id,
|
|
8694
|
+
instructions: stringArray(value, "instructions"),
|
|
8695
|
+
introducedIn: value.introducedIn,
|
|
8696
|
+
requiresLegacyHandoffs: value.requiresLegacyHandoffs === true,
|
|
8697
|
+
title: value.title
|
|
8698
|
+
};
|
|
8699
|
+
}
|
|
8700
|
+
function stringArray(value, key) {
|
|
8701
|
+
const raw = value[key];
|
|
8702
|
+
if (!Array.isArray(raw) || !raw.every((item) => typeof item === "string")) {
|
|
8703
|
+
throw new Error(`Invalid ${key} in ${POST_UPDATE_MIGRATIONS_FILE}.`);
|
|
8704
|
+
}
|
|
8705
|
+
return raw;
|
|
8706
|
+
}
|
|
8707
|
+
function printPostUpdateMigration(migration) {
|
|
8708
|
+
console.log("");
|
|
8709
|
+
console.log(`${migration.title} (${migration.introducedIn})`);
|
|
8710
|
+
for (const line of migration.description) {
|
|
8711
|
+
console.log(`- ${line}`);
|
|
8712
|
+
}
|
|
8713
|
+
}
|
|
8714
|
+
async function confirmPostUpdateMigration(prompt) {
|
|
8715
|
+
const readline = (0, import_promises7.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
8716
|
+
try {
|
|
8717
|
+
const answer = (await readline.question(prompt)).trim().toLowerCase();
|
|
8718
|
+
return answer === "y" || answer === "yes";
|
|
8719
|
+
} finally {
|
|
8720
|
+
readline.close();
|
|
8721
|
+
}
|
|
8722
|
+
}
|
|
8723
|
+
function formatMigrationCommand(executable, args) {
|
|
8724
|
+
return [executable, ...args].map((part) => /\s/.test(part) ? JSON.stringify(part) : part).join(" ");
|
|
8725
|
+
}
|
|
8726
|
+
function currentThreadnoteCommand() {
|
|
8727
|
+
const entrypoint = process.argv[1]?.trim();
|
|
8728
|
+
return entrypoint ? entrypoint : void 0;
|
|
8729
|
+
}
|
|
8730
|
+
async function readPostUpdateState(config) {
|
|
8731
|
+
const raw = await readFileIfExists(postUpdateStatePath(config));
|
|
8732
|
+
if (!raw) {
|
|
8733
|
+
return { handledMigrationIds: [] };
|
|
8734
|
+
}
|
|
8735
|
+
try {
|
|
8736
|
+
const parsed = JSON.parse(raw);
|
|
8737
|
+
if (!isJsonObject(parsed) || !Array.isArray(parsed.handledMigrationIds)) {
|
|
8738
|
+
return { handledMigrationIds: [] };
|
|
8739
|
+
}
|
|
8740
|
+
return { handledMigrationIds: parsed.handledMigrationIds.filter((id) => typeof id === "string") };
|
|
8741
|
+
} catch (_err) {
|
|
8742
|
+
return { handledMigrationIds: [] };
|
|
8743
|
+
}
|
|
8744
|
+
}
|
|
8745
|
+
async function writePostUpdateState(config, state) {
|
|
8746
|
+
await ensureDirectory(config.agentContextHome, false);
|
|
8747
|
+
await (0, import_promises6.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
|
|
8748
|
+
`, { encoding: "utf8", mode: 384 });
|
|
8749
|
+
}
|
|
8750
|
+
function postUpdateStatePath(config) {
|
|
8751
|
+
return (0, import_node_path6.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
|
|
8752
|
+
}
|
|
8753
|
+
async function resolveUpdateRuntime(runtime) {
|
|
8754
|
+
if (runtime !== "auto") {
|
|
8755
|
+
await requireRuntime(runtime);
|
|
8756
|
+
return runtime;
|
|
8757
|
+
}
|
|
8758
|
+
for (const candidate of ["npm", "bun", "deno"]) {
|
|
8759
|
+
if (await findExecutable([candidate])) {
|
|
8760
|
+
return candidate;
|
|
8761
|
+
}
|
|
8762
|
+
}
|
|
8763
|
+
throw new Error("Install Node/npm, Bun, or Deno to update threadnote.");
|
|
8764
|
+
}
|
|
8765
|
+
async function requireRuntime(runtime) {
|
|
8766
|
+
if (!await findExecutable([runtime])) {
|
|
8767
|
+
throw new Error(`${runtime} was requested but was not found on PATH.`);
|
|
8768
|
+
}
|
|
8769
|
+
}
|
|
8770
|
+
async function installedThreadnoteCommand(runtime) {
|
|
8771
|
+
const runtimeBin = await runtimeThreadnoteBin(runtime);
|
|
8772
|
+
if (runtimeBin && await isExecutable(runtimeBin)) {
|
|
8773
|
+
return runtimeBin;
|
|
8774
|
+
}
|
|
8775
|
+
return await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
|
|
8776
|
+
}
|
|
8777
|
+
async function runtimeThreadnoteBin(runtime) {
|
|
8778
|
+
if (runtime === "npm") {
|
|
8779
|
+
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
8780
|
+
const prefix = result.stdout.trim();
|
|
8781
|
+
return prefix ? (0, import_node_path6.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
8782
|
+
}
|
|
8783
|
+
if (runtime === "bun") {
|
|
8784
|
+
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
8785
|
+
const binDir = result.stdout.trim();
|
|
8786
|
+
return binDir ? (0, import_node_path6.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
8787
|
+
}
|
|
8788
|
+
return (0, import_node_path6.join)(process.env.DENO_INSTALL ?? (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
|
|
8789
|
+
}
|
|
8790
|
+
async function isExecutable(path) {
|
|
8791
|
+
try {
|
|
8792
|
+
await (0, import_promises6.access)(path, import_node_fs4.constants.X_OK);
|
|
8793
|
+
return true;
|
|
8794
|
+
} catch (_err) {
|
|
8795
|
+
return false;
|
|
8796
|
+
}
|
|
8797
|
+
}
|
|
8798
|
+
function updatePackageCommand(runtime, registry) {
|
|
8799
|
+
if (runtime === "npm") {
|
|
8800
|
+
return { executable: "npm", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
|
|
8801
|
+
}
|
|
8802
|
+
if (runtime === "bun") {
|
|
8803
|
+
return { executable: "bun", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
|
|
8804
|
+
}
|
|
8805
|
+
return {
|
|
8806
|
+
executable: "env",
|
|
8807
|
+
args: [
|
|
8808
|
+
`NPM_CONFIG_REGISTRY=${registry}`,
|
|
8809
|
+
"deno",
|
|
8810
|
+
"install",
|
|
8811
|
+
"--global",
|
|
8812
|
+
"--force",
|
|
8813
|
+
"--name",
|
|
8814
|
+
NPM_PACKAGE_NAME,
|
|
8815
|
+
"--allow-read",
|
|
8816
|
+
"--allow-write",
|
|
8817
|
+
"--allow-run",
|
|
8818
|
+
"--allow-env",
|
|
8819
|
+
"--allow-net",
|
|
8820
|
+
`npm:${NPM_PACKAGE_NAME}@latest`
|
|
8821
|
+
]
|
|
8822
|
+
};
|
|
8823
|
+
}
|
|
8824
|
+
function normalizeRegistry(registry) {
|
|
8825
|
+
return registry.endsWith("/") ? registry : `${registry}/`;
|
|
8826
|
+
}
|
|
8827
|
+
function updateRegistry() {
|
|
8828
|
+
return normalizeRegistry(process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
|
|
8829
|
+
}
|
|
8830
|
+
function isUpdateNotificationDisabled() {
|
|
8831
|
+
return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
|
|
8832
|
+
}
|
|
8833
|
+
function compareVersions(currentVersion, latestVersion) {
|
|
8834
|
+
const current = parseVersion(currentVersion);
|
|
8835
|
+
const latest = parseVersion(latestVersion);
|
|
8836
|
+
for (let index = 0; index < 3; index += 1) {
|
|
8837
|
+
const difference = current.numbers[index] - latest.numbers[index];
|
|
8838
|
+
if (difference !== 0) {
|
|
8839
|
+
return difference;
|
|
8840
|
+
}
|
|
8841
|
+
}
|
|
8842
|
+
if (current.prerelease === latest.prerelease) {
|
|
8843
|
+
return 0;
|
|
8844
|
+
}
|
|
8845
|
+
if (current.prerelease === void 0) {
|
|
8846
|
+
return 1;
|
|
8847
|
+
}
|
|
8848
|
+
if (latest.prerelease === void 0) {
|
|
8849
|
+
return -1;
|
|
8850
|
+
}
|
|
8851
|
+
return current.prerelease.localeCompare(latest.prerelease);
|
|
8852
|
+
}
|
|
8853
|
+
function parseVersion(version) {
|
|
8854
|
+
const normalized = version.trim().replace(/^v/, "");
|
|
8855
|
+
const [core2, prerelease] = normalized.split("-", 2);
|
|
8856
|
+
const parts = core2.split(".").map((part) => Number(part));
|
|
8857
|
+
return {
|
|
8858
|
+
numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
|
|
8859
|
+
prerelease
|
|
8860
|
+
};
|
|
8861
|
+
}
|
|
8862
|
+
function safeVersionNumber(value) {
|
|
8863
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
8864
|
+
}
|
|
8865
|
+
|
|
8866
|
+
// src/lifecycle.ts
|
|
7891
8867
|
async function runDoctor(config, options) {
|
|
7892
8868
|
const checks = [];
|
|
7893
8869
|
checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
|
|
7894
|
-
checks.push({ name: "platform", status: (0,
|
|
8870
|
+
checks.push({ name: "platform", status: (0, import_node_os5.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os5.platform)() });
|
|
7895
8871
|
checks.push(await commandCheck("node", ["--version"]));
|
|
7896
8872
|
checks.push(await commandCheck("python3", ["--version"]));
|
|
7897
8873
|
checks.push(await commandCheck("openviking-server", ["--help"]));
|
|
@@ -7904,9 +8880,9 @@ async function runDoctor(config, options) {
|
|
|
7904
8880
|
checks.push(await commandShimCheck());
|
|
7905
8881
|
checks.push(...await userAgentInstructionsChecks());
|
|
7906
8882
|
checks.push(await manifestCheck(config.manifestPath));
|
|
7907
|
-
checks.push(await fileCheck((0,
|
|
7908
|
-
checks.push(await fileCheck((0,
|
|
7909
|
-
checks.push(await fileCheck((0,
|
|
8883
|
+
checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
|
|
8884
|
+
checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
|
|
8885
|
+
checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
|
|
7910
8886
|
checks.push(await healthCheck(config));
|
|
7911
8887
|
for (const check of checks) {
|
|
7912
8888
|
console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
|
|
@@ -7923,9 +8899,9 @@ async function runInstall(config, options) {
|
|
|
7923
8899
|
const repairInvalidConfigs = options.repairInvalidConfigs === true;
|
|
7924
8900
|
const dryRun = options.dryRun === true;
|
|
7925
8901
|
await ensureDirectory(config.agentContextHome, dryRun);
|
|
7926
|
-
await ensureDirectory((0,
|
|
7927
|
-
await ensureDirectory((0,
|
|
7928
|
-
await ensureDirectory((0,
|
|
8902
|
+
await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "logs"), dryRun);
|
|
8903
|
+
await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "redacted"), dryRun);
|
|
8904
|
+
await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "mcp"), dryRun);
|
|
7929
8905
|
await installCommandShim(dryRun);
|
|
7930
8906
|
await installUserAgentInstructions(dryRun);
|
|
7931
8907
|
const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
@@ -7951,17 +8927,17 @@ async function runInstall(config, options) {
|
|
|
7951
8927
|
}
|
|
7952
8928
|
await writeTemplateIfMissing({
|
|
7953
8929
|
config,
|
|
7954
|
-
destinationPath: (0,
|
|
8930
|
+
destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ov.conf"),
|
|
7955
8931
|
dryRun,
|
|
7956
8932
|
shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
7957
|
-
templatePath: (0,
|
|
8933
|
+
templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ov.conf.template.json")
|
|
7958
8934
|
});
|
|
7959
8935
|
await writeTemplateIfMissing({
|
|
7960
8936
|
config,
|
|
7961
|
-
destinationPath: (0,
|
|
8937
|
+
destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ovcli.conf"),
|
|
7962
8938
|
dryRun,
|
|
7963
8939
|
shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
7964
|
-
templatePath: (0,
|
|
8940
|
+
templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ovcli.conf.template.json")
|
|
7965
8941
|
});
|
|
7966
8942
|
if (options.start !== false) {
|
|
7967
8943
|
const healthy = await repairServerHealth(config, dryRun);
|
|
@@ -8000,6 +8976,9 @@ async function runRepair(config, options) {
|
|
|
8000
8976
|
}
|
|
8001
8977
|
console.log("\nPost-repair doctor:");
|
|
8002
8978
|
await runDoctor(config, { dryRun, strict: false });
|
|
8979
|
+
if (options.postUpdate !== false) {
|
|
8980
|
+
await maybeRunPostUpdateAfterRepair(config, { dryRun });
|
|
8981
|
+
}
|
|
8003
8982
|
}
|
|
8004
8983
|
async function runUninstall(config, options) {
|
|
8005
8984
|
const dryRun = options.dryRun === true;
|
|
@@ -8008,7 +8987,7 @@ async function runUninstall(config, options) {
|
|
|
8008
8987
|
}
|
|
8009
8988
|
console.log("Uninstalling local Threadnote setup.");
|
|
8010
8989
|
await runStop(config, { dryRun });
|
|
8011
|
-
await removePathIfExists((0,
|
|
8990
|
+
await removePathIfExists((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
|
|
8012
8991
|
await removeLaunchAgent(dryRun);
|
|
8013
8992
|
await removeMcpConfigs(options.mcp ?? "available", dryRun);
|
|
8014
8993
|
await removeMcpSnippets(config, dryRun);
|
|
@@ -8043,7 +9022,7 @@ async function repairManifest(config, dryRun) {
|
|
|
8043
9022
|
return;
|
|
8044
9023
|
}
|
|
8045
9024
|
const project = projectManifestForRepo(repoRoot, []);
|
|
8046
|
-
const
|
|
9025
|
+
const output2 = jsYaml.dump(
|
|
8047
9026
|
{
|
|
8048
9027
|
version: 1,
|
|
8049
9028
|
projects: [
|
|
@@ -8059,19 +9038,19 @@ async function repairManifest(config, dryRun) {
|
|
|
8059
9038
|
);
|
|
8060
9039
|
if (dryRun) {
|
|
8061
9040
|
console.log(`# Would write replacement manifest: ${config.manifestPath}`);
|
|
8062
|
-
console.log(
|
|
9041
|
+
console.log(output2.trimEnd());
|
|
8063
9042
|
return;
|
|
8064
9043
|
}
|
|
8065
|
-
await ensureDirectory((0,
|
|
9044
|
+
await ensureDirectory((0, import_node_path7.dirname)(config.manifestPath), false);
|
|
8066
9045
|
const currentContent = await readFileIfExists(config.manifestPath);
|
|
8067
9046
|
if (currentContent !== void 0) {
|
|
8068
9047
|
const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
|
|
8069
|
-
await (0,
|
|
8070
|
-
await (0,
|
|
9048
|
+
await (0, import_promises8.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
9049
|
+
await (0, import_promises8.chmod)(backupPath, 384);
|
|
8071
9050
|
console.log(`Backup: ${backupPath}`);
|
|
8072
9051
|
}
|
|
8073
|
-
await (0,
|
|
8074
|
-
await (0,
|
|
9052
|
+
await (0, import_promises8.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
9053
|
+
await (0, import_promises8.chmod)(config.manifestPath, 384);
|
|
8075
9054
|
console.log(`Wrote replacement manifest: ${config.manifestPath}`);
|
|
8076
9055
|
}
|
|
8077
9056
|
async function repairServerHealth(config, dryRun) {
|
|
@@ -8112,16 +9091,16 @@ async function runStart(config, options) {
|
|
|
8112
9091
|
);
|
|
8113
9092
|
}
|
|
8114
9093
|
const logPath = openVikingLogPath(config);
|
|
8115
|
-
await ensureDirectory((0,
|
|
9094
|
+
await ensureDirectory((0, import_node_path7.dirname)(logPath), false);
|
|
8116
9095
|
if (options.foreground === true) {
|
|
8117
9096
|
const result = await runInteractive(server, args);
|
|
8118
9097
|
process.exitCode = result;
|
|
8119
9098
|
return;
|
|
8120
9099
|
}
|
|
8121
|
-
const logFd = (0,
|
|
9100
|
+
const logFd = (0, import_node_fs5.openSync)(logPath, "a");
|
|
8122
9101
|
const child = spawnDetachedServerWithLog(server, args, logFd);
|
|
8123
9102
|
child.unref();
|
|
8124
|
-
await (0,
|
|
9103
|
+
await (0, import_promises8.writeFile)((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
8125
9104
|
`, "utf8");
|
|
8126
9105
|
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
8127
9106
|
if (health) {
|
|
@@ -8136,7 +9115,7 @@ function spawnDetachedServerWithLog(server, args, logFd) {
|
|
|
8136
9115
|
try {
|
|
8137
9116
|
return spawnDetachedServer(server, args, logFd);
|
|
8138
9117
|
} finally {
|
|
8139
|
-
(0,
|
|
9118
|
+
(0, import_node_fs5.closeSync)(logFd);
|
|
8140
9119
|
}
|
|
8141
9120
|
}
|
|
8142
9121
|
function spawnDetachedServer(server, args, logFd) {
|
|
@@ -8147,14 +9126,14 @@ function spawnDetachedServer(server, args, logFd) {
|
|
|
8147
9126
|
}
|
|
8148
9127
|
async function runStop(config, options) {
|
|
8149
9128
|
const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
8150
|
-
if ((0,
|
|
9129
|
+
if ((0, import_node_os5.platform)() === "darwin") {
|
|
8151
9130
|
if (options.dryRun === true || await exists(launchAgentPath)) {
|
|
8152
9131
|
await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
|
|
8153
9132
|
} else {
|
|
8154
9133
|
console.log(`No LaunchAgent found: ${launchAgentPath}`);
|
|
8155
9134
|
}
|
|
8156
9135
|
}
|
|
8157
|
-
const pidPath = (0,
|
|
9136
|
+
const pidPath = (0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid");
|
|
8158
9137
|
const pidText = await readFileIfExists(pidPath);
|
|
8159
9138
|
if (!pidText) {
|
|
8160
9139
|
console.log("No pid file found for detached OpenViking server.");
|
|
@@ -8250,7 +9229,7 @@ async function pythonSystemCertificatesCheck() {
|
|
|
8250
9229
|
};
|
|
8251
9230
|
}
|
|
8252
9231
|
async function commandShimCheck() {
|
|
8253
|
-
const shimPath = (0,
|
|
9232
|
+
const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
8254
9233
|
const content = await readFileIfExists(shimPath);
|
|
8255
9234
|
if (content === void 0) {
|
|
8256
9235
|
return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
|
|
@@ -8272,9 +9251,9 @@ async function commandShimCheck() {
|
|
|
8272
9251
|
return { name: "threadnote shim", status: "ok", detail: shimPath };
|
|
8273
9252
|
}
|
|
8274
9253
|
async function userAgentInstructionsChecks() {
|
|
8275
|
-
const expectedBlock = await renderUserAgentInstructionsBlock();
|
|
8276
9254
|
return Promise.all(
|
|
8277
9255
|
USER_AGENT_INSTRUCTION_TARGETS.map(async (target) => {
|
|
9256
|
+
const expectedInstructions = await renderUserAgentInstructions(target);
|
|
8278
9257
|
const targetPath = expandPath(target.path);
|
|
8279
9258
|
const content = await readFileIfExists(targetPath);
|
|
8280
9259
|
if (content === void 0) {
|
|
@@ -8288,7 +9267,7 @@ async function userAgentInstructionsChecks() {
|
|
|
8288
9267
|
detail: `${targetPath} missing threadnote block; install will add it`
|
|
8289
9268
|
};
|
|
8290
9269
|
}
|
|
8291
|
-
if (existingBlock !==
|
|
9270
|
+
if (target.kind === "file" && content !== expectedInstructions || target.kind === "block" && existingBlock !== expectedInstructions) {
|
|
8292
9271
|
return {
|
|
8293
9272
|
name: target.label,
|
|
8294
9273
|
status: "warn",
|
|
@@ -8316,11 +9295,11 @@ async function hasPythonModule(serverPath, moduleName) {
|
|
|
8316
9295
|
async function siblingPythonForExecutable(executablePath) {
|
|
8317
9296
|
let resolvedPath;
|
|
8318
9297
|
try {
|
|
8319
|
-
resolvedPath = await (0,
|
|
9298
|
+
resolvedPath = await (0, import_promises8.realpath)(executablePath);
|
|
8320
9299
|
} catch (_err) {
|
|
8321
9300
|
return void 0;
|
|
8322
9301
|
}
|
|
8323
|
-
const pythonPath = (0,
|
|
9302
|
+
const pythonPath = (0, import_node_path7.join)((0, import_node_path7.dirname)(resolvedPath), "python");
|
|
8324
9303
|
return await exists(pythonPath) ? pythonPath : void 0;
|
|
8325
9304
|
}
|
|
8326
9305
|
async function manifestCheck(path) {
|
|
@@ -8442,38 +9421,38 @@ function printInstallNextSteps(options) {
|
|
|
8442
9421
|
}
|
|
8443
9422
|
async function writeTemplateIfMissing(options) {
|
|
8444
9423
|
if (await exists(options.destinationPath)) {
|
|
8445
|
-
const currentContent = await (0,
|
|
9424
|
+
const currentContent = await (0, import_promises8.readFile)(options.destinationPath, "utf8");
|
|
8446
9425
|
if (options.shouldRepair?.(currentContent) !== true) {
|
|
8447
9426
|
console.log(`Already exists: ${options.destinationPath}`);
|
|
8448
9427
|
return;
|
|
8449
9428
|
}
|
|
8450
|
-
const rendered2 = renderTemplate(await (0,
|
|
9429
|
+
const rendered2 = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
|
|
8451
9430
|
if (options.dryRun) {
|
|
8452
9431
|
console.log(`Would repair generated config: ${options.destinationPath}`);
|
|
8453
9432
|
return;
|
|
8454
9433
|
}
|
|
8455
9434
|
const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
|
|
8456
|
-
await (0,
|
|
8457
|
-
await (0,
|
|
8458
|
-
await (0,
|
|
8459
|
-
await (0,
|
|
9435
|
+
await (0, import_promises8.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
9436
|
+
await (0, import_promises8.chmod)(backupPath, 384);
|
|
9437
|
+
await (0, import_promises8.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
|
|
9438
|
+
await (0, import_promises8.chmod)(options.destinationPath, 384);
|
|
8460
9439
|
console.log(`Repaired generated config: ${options.destinationPath}`);
|
|
8461
9440
|
console.log(`Backup: ${backupPath}`);
|
|
8462
9441
|
return;
|
|
8463
9442
|
}
|
|
8464
|
-
const rendered = renderTemplate(await (0,
|
|
9443
|
+
const rendered = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
|
|
8465
9444
|
if (options.dryRun) {
|
|
8466
9445
|
console.log(`Would write ${options.destinationPath}`);
|
|
8467
9446
|
return;
|
|
8468
9447
|
}
|
|
8469
|
-
await ensureDirectory((0,
|
|
8470
|
-
await (0,
|
|
8471
|
-
await (0,
|
|
9448
|
+
await ensureDirectory((0, import_node_path7.dirname)(options.destinationPath), false);
|
|
9449
|
+
await (0, import_promises8.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
|
|
9450
|
+
await (0, import_promises8.chmod)(options.destinationPath, 384);
|
|
8472
9451
|
console.log(`Wrote ${options.destinationPath}`);
|
|
8473
9452
|
}
|
|
8474
9453
|
async function installCommandShim(dryRun) {
|
|
8475
9454
|
const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
|
|
8476
|
-
const shimPath = (0,
|
|
9455
|
+
const shimPath = (0, import_node_path7.join)(binDir, "threadnote");
|
|
8477
9456
|
const existingContent = await readFileIfExists(shimPath);
|
|
8478
9457
|
if (existingContent && !isManagedCommandShim(existingContent)) {
|
|
8479
9458
|
console.log(`WARN not overwriting existing command shim: ${shimPath}`);
|
|
@@ -8489,12 +9468,12 @@ async function installCommandShim(dryRun) {
|
|
|
8489
9468
|
return;
|
|
8490
9469
|
}
|
|
8491
9470
|
await ensureDirectory(binDir, false);
|
|
8492
|
-
await (0,
|
|
8493
|
-
await (0,
|
|
9471
|
+
await (0, import_promises8.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
|
|
9472
|
+
await (0, import_promises8.chmod)(shimPath, 493);
|
|
8494
9473
|
console.log(`Wrote command shim: ${shimPath}`);
|
|
8495
9474
|
}
|
|
8496
9475
|
async function removeCommandShim(dryRun) {
|
|
8497
|
-
const shimPath = (0,
|
|
9476
|
+
const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
8498
9477
|
const content = await readFileIfExists(shimPath);
|
|
8499
9478
|
if (content === void 0) {
|
|
8500
9479
|
console.log(`Already absent: ${shimPath}`);
|
|
@@ -8507,11 +9486,15 @@ async function removeCommandShim(dryRun) {
|
|
|
8507
9486
|
await removePath(shimPath, "command shim", dryRun);
|
|
8508
9487
|
}
|
|
8509
9488
|
async function installUserAgentInstructions(dryRun) {
|
|
8510
|
-
const block = await renderUserAgentInstructionsBlock();
|
|
8511
9489
|
for (const target of USER_AGENT_INSTRUCTION_TARGETS) {
|
|
9490
|
+
const instructions = await renderUserAgentInstructions(target);
|
|
8512
9491
|
const targetPath = expandPath(target.path);
|
|
8513
9492
|
const currentContent = await readFileIfExists(targetPath);
|
|
8514
|
-
|
|
9493
|
+
if (target.kind === "file" && currentContent !== void 0 && extractManagedBlock(currentContent) === void 0) {
|
|
9494
|
+
console.log(`WARN ${targetPath} is not managed by threadnote; not modifying it`);
|
|
9495
|
+
continue;
|
|
9496
|
+
}
|
|
9497
|
+
const nextContent = target.kind === "file" ? instructions : upsertManagedBlock(currentContent ?? "", instructions);
|
|
8515
9498
|
if (nextContent === void 0) {
|
|
8516
9499
|
console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
|
|
8517
9500
|
continue;
|
|
@@ -8524,8 +9507,8 @@ async function installUserAgentInstructions(dryRun) {
|
|
|
8524
9507
|
console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
|
|
8525
9508
|
continue;
|
|
8526
9509
|
}
|
|
8527
|
-
await ensureDirectory((0,
|
|
8528
|
-
await (0,
|
|
9510
|
+
await ensureDirectory((0, import_node_path7.dirname)(targetPath), false);
|
|
9511
|
+
await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
8529
9512
|
console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
|
|
8530
9513
|
}
|
|
8531
9514
|
}
|
|
@@ -8537,6 +9520,14 @@ async function removeUserAgentInstructions(dryRun) {
|
|
|
8537
9520
|
console.log(`Already absent: ${targetPath}`);
|
|
8538
9521
|
continue;
|
|
8539
9522
|
}
|
|
9523
|
+
if (target.kind === "file") {
|
|
9524
|
+
if (extractManagedBlock(currentContent) === void 0) {
|
|
9525
|
+
console.log(`WARN ${targetPath} is not managed by threadnote; not removing it`);
|
|
9526
|
+
continue;
|
|
9527
|
+
}
|
|
9528
|
+
await removePath(targetPath, target.label, dryRun);
|
|
9529
|
+
continue;
|
|
9530
|
+
}
|
|
8540
9531
|
const nextContent = removeManagedBlock(currentContent);
|
|
8541
9532
|
if (nextContent === void 0) {
|
|
8542
9533
|
console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
|
|
@@ -8554,12 +9545,27 @@ async function removeUserAgentInstructions(dryRun) {
|
|
|
8554
9545
|
console.log(`Would update ${targetPath}`);
|
|
8555
9546
|
continue;
|
|
8556
9547
|
}
|
|
8557
|
-
await (0,
|
|
9548
|
+
await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
8558
9549
|
console.log(`Updated ${targetPath}`);
|
|
8559
9550
|
}
|
|
8560
9551
|
}
|
|
9552
|
+
async function renderUserAgentInstructions(target) {
|
|
9553
|
+
const block = await renderUserAgentInstructionsBlock();
|
|
9554
|
+
if (target.kind === "block") {
|
|
9555
|
+
return block;
|
|
9556
|
+
}
|
|
9557
|
+
return [
|
|
9558
|
+
"---",
|
|
9559
|
+
"name: Threadnote",
|
|
9560
|
+
"description: Shared local context and handoffs through Threadnote",
|
|
9561
|
+
'applyTo: "**"',
|
|
9562
|
+
"---",
|
|
9563
|
+
"",
|
|
9564
|
+
block
|
|
9565
|
+
].join("\n");
|
|
9566
|
+
}
|
|
8561
9567
|
async function renderUserAgentInstructionsBlock() {
|
|
8562
|
-
const instructions = (await (0,
|
|
9568
|
+
const instructions = (await (0, import_promises8.readFile)((0, import_node_path7.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
|
|
8563
9569
|
return `${USER_INSTRUCTIONS_START_MARKER}
|
|
8564
9570
|
${instructions}
|
|
8565
9571
|
${USER_INSTRUCTIONS_END_MARKER}`;
|
|
@@ -8634,12 +9640,12 @@ function isManagedCommandShim(content) {
|
|
|
8634
9640
|
return content.includes(SHIM_MARKER);
|
|
8635
9641
|
}
|
|
8636
9642
|
async function installLaunchAgent(config, dryRun) {
|
|
8637
|
-
if ((0,
|
|
9643
|
+
if ((0, import_node_os5.platform)() !== "darwin") {
|
|
8638
9644
|
throw new Error("launchd autostart is only supported on macOS.");
|
|
8639
9645
|
}
|
|
8640
|
-
const source = (0,
|
|
9646
|
+
const source = (0, import_node_path7.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
|
|
8641
9647
|
const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
8642
|
-
const rendered = renderTemplate(await (0,
|
|
9648
|
+
const rendered = renderTemplate(await (0, import_promises8.readFile)(source, "utf8"), config);
|
|
8643
9649
|
if (dryRun) {
|
|
8644
9650
|
console.log(`Would write ${destination}`);
|
|
8645
9651
|
console.log(`Would run: launchctl unload ${destination}`);
|
|
@@ -8647,9 +9653,9 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
8647
9653
|
console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
|
|
8648
9654
|
return;
|
|
8649
9655
|
}
|
|
8650
|
-
await ensureDirectory((0,
|
|
8651
|
-
await ensureDirectory((0,
|
|
8652
|
-
await (0,
|
|
9656
|
+
await ensureDirectory((0, import_node_path7.dirname)(destination), false);
|
|
9657
|
+
await ensureDirectory((0, import_node_path7.dirname)(openVikingLogPath(config)), false);
|
|
9658
|
+
await (0, import_promises8.writeFile)(destination, rendered, "utf8");
|
|
8653
9659
|
await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
|
|
8654
9660
|
await maybeRun(false, "launchctl", ["load", destination]);
|
|
8655
9661
|
await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
|
|
@@ -8712,7 +9718,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
|
|
|
8712
9718
|
if (typeof parsed.default_user !== "string") {
|
|
8713
9719
|
return false;
|
|
8714
9720
|
}
|
|
8715
|
-
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0,
|
|
9721
|
+
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path7.join)(config.agentContextHome, "data")) {
|
|
8716
9722
|
return false;
|
|
8717
9723
|
}
|
|
8718
9724
|
return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
|
|
@@ -8731,259 +9737,6 @@ function parsePackageManager(value) {
|
|
|
8731
9737
|
throw new Error(`Invalid package manager: ${value}`);
|
|
8732
9738
|
}
|
|
8733
9739
|
|
|
8734
|
-
// src/update.ts
|
|
8735
|
-
var import_node_fs4 = require("node:fs");
|
|
8736
|
-
var import_promises7 = require("node:fs/promises");
|
|
8737
|
-
var import_node_os5 = require("node:os");
|
|
8738
|
-
var import_node_path7 = require("node:path");
|
|
8739
|
-
var NPM_PACKAGE_NAME = "threadnote";
|
|
8740
|
-
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
8741
|
-
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
8742
|
-
function parseUpdateRuntime(value) {
|
|
8743
|
-
if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
|
|
8744
|
-
return value;
|
|
8745
|
-
}
|
|
8746
|
-
throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
|
|
8747
|
-
}
|
|
8748
|
-
async function maybeNotifyUpdate(config, options = {}) {
|
|
8749
|
-
if (isUpdateNotificationDisabled()) {
|
|
8750
|
-
return;
|
|
8751
|
-
}
|
|
8752
|
-
try {
|
|
8753
|
-
const info = await getUpdateInfo(config, {
|
|
8754
|
-
allowCacheWrite: options.dryRun !== true,
|
|
8755
|
-
preferFresh: false,
|
|
8756
|
-
registry: updateRegistry()
|
|
8757
|
-
});
|
|
8758
|
-
if (!info.isUpdateAvailable) {
|
|
8759
|
-
return;
|
|
8760
|
-
}
|
|
8761
|
-
console.log("");
|
|
8762
|
-
console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
|
|
8763
|
-
console.log("Run: threadnote update");
|
|
8764
|
-
} catch (_err) {
|
|
8765
|
-
return;
|
|
8766
|
-
}
|
|
8767
|
-
}
|
|
8768
|
-
async function runUpdate(config, options) {
|
|
8769
|
-
const registry = normalizeRegistry(options.registry ?? updateRegistry());
|
|
8770
|
-
const info = await getUpdateInfo(config, {
|
|
8771
|
-
allowCacheWrite: options.dryRun !== true,
|
|
8772
|
-
preferFresh: true,
|
|
8773
|
-
registry
|
|
8774
|
-
});
|
|
8775
|
-
console.log(`Current version: ${info.currentVersion}`);
|
|
8776
|
-
console.log(`Latest version: ${info.latestVersion}`);
|
|
8777
|
-
console.log(`Registry: ${info.registry}`);
|
|
8778
|
-
if (options.check === true) {
|
|
8779
|
-
if (info.isUpdateAvailable) {
|
|
8780
|
-
console.log(`Update available. Run: threadnote update`);
|
|
8781
|
-
} else {
|
|
8782
|
-
console.log(
|
|
8783
|
-
compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
|
|
8784
|
-
);
|
|
8785
|
-
}
|
|
8786
|
-
return;
|
|
8787
|
-
}
|
|
8788
|
-
if (!info.isUpdateAvailable && options.force !== true) {
|
|
8789
|
-
console.log("Threadnote is up to date.");
|
|
8790
|
-
return;
|
|
8791
|
-
}
|
|
8792
|
-
const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
|
|
8793
|
-
const updateCommand = updatePackageCommand(runtime, registry);
|
|
8794
|
-
await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
|
|
8795
|
-
if (options.repair === false) {
|
|
8796
|
-
console.log("Skipping repair because --no-repair was provided.");
|
|
8797
|
-
return;
|
|
8798
|
-
}
|
|
8799
|
-
const threadnoteCommand = await installedThreadnoteCommand(runtime);
|
|
8800
|
-
await maybeRun(options.dryRun === true, threadnoteCommand, ["repair"]);
|
|
8801
|
-
console.log("Update complete. Restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.");
|
|
8802
|
-
}
|
|
8803
|
-
async function getUpdateInfo(config, options) {
|
|
8804
|
-
const currentVersion = await currentPackageVersion();
|
|
8805
|
-
const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
|
|
8806
|
-
const latestVersion = cached?.latestVersion ?? await fetchLatestVersion(options.registry);
|
|
8807
|
-
if (!cached && options.allowCacheWrite) {
|
|
8808
|
-
await writeUpdateCache(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
|
|
8809
|
-
}
|
|
8810
|
-
return {
|
|
8811
|
-
currentVersion,
|
|
8812
|
-
isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
|
|
8813
|
-
latestVersion,
|
|
8814
|
-
registry: options.registry
|
|
8815
|
-
};
|
|
8816
|
-
}
|
|
8817
|
-
async function currentPackageVersion() {
|
|
8818
|
-
const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
|
|
8819
|
-
const parsed = JSON.parse(rawPackage);
|
|
8820
|
-
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8821
|
-
throw new Error("Could not read current threadnote package version.");
|
|
8822
|
-
}
|
|
8823
|
-
return parsed.version;
|
|
8824
|
-
}
|
|
8825
|
-
async function fetchLatestVersion(registry) {
|
|
8826
|
-
const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
|
|
8827
|
-
const controller = new AbortController();
|
|
8828
|
-
const timeout = setTimeout(() => {
|
|
8829
|
-
controller.abort();
|
|
8830
|
-
}, 2500);
|
|
8831
|
-
try {
|
|
8832
|
-
const response = await fetch(url, { headers: { accept: "application/json" }, signal: controller.signal });
|
|
8833
|
-
if (!response.ok) {
|
|
8834
|
-
throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
8835
|
-
}
|
|
8836
|
-
const parsed = await response.json();
|
|
8837
|
-
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8838
|
-
throw new Error("npm registry response did not include a version.");
|
|
8839
|
-
}
|
|
8840
|
-
return parsed.version;
|
|
8841
|
-
} catch (err) {
|
|
8842
|
-
throw new Error(`Could not check npm for updates: ${errorMessage(err)}`, { cause: err });
|
|
8843
|
-
} finally {
|
|
8844
|
-
clearTimeout(timeout);
|
|
8845
|
-
}
|
|
8846
|
-
}
|
|
8847
|
-
async function readFreshCache(config, registry) {
|
|
8848
|
-
const rawCache = await readFileIfExists(updateCachePath(config));
|
|
8849
|
-
if (!rawCache) {
|
|
8850
|
-
return void 0;
|
|
8851
|
-
}
|
|
8852
|
-
try {
|
|
8853
|
-
const parsed = JSON.parse(rawCache);
|
|
8854
|
-
if (!isJsonObject(parsed) || typeof parsed.checkedAt !== "string" || typeof parsed.latestVersion !== "string" || parsed.registry !== registry) {
|
|
8855
|
-
return void 0;
|
|
8856
|
-
}
|
|
8857
|
-
const checkedAt = Date.parse(parsed.checkedAt);
|
|
8858
|
-
if (!Number.isFinite(checkedAt) || Date.now() - checkedAt > UPDATE_CHECK_TTL_MS) {
|
|
8859
|
-
return void 0;
|
|
8860
|
-
}
|
|
8861
|
-
return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, registry };
|
|
8862
|
-
} catch (_err) {
|
|
8863
|
-
return void 0;
|
|
8864
|
-
}
|
|
8865
|
-
}
|
|
8866
|
-
async function writeUpdateCache(config, cache) {
|
|
8867
|
-
await ensureDirectory(config.agentContextHome, false);
|
|
8868
|
-
await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
|
|
8869
|
-
`, { encoding: "utf8", mode: 384 });
|
|
8870
|
-
}
|
|
8871
|
-
function updateCachePath(config) {
|
|
8872
|
-
return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
|
|
8873
|
-
}
|
|
8874
|
-
async function resolveUpdateRuntime(runtime) {
|
|
8875
|
-
if (runtime !== "auto") {
|
|
8876
|
-
await requireRuntime(runtime);
|
|
8877
|
-
return runtime;
|
|
8878
|
-
}
|
|
8879
|
-
for (const candidate of ["npm", "bun", "deno"]) {
|
|
8880
|
-
if (await findExecutable([candidate])) {
|
|
8881
|
-
return candidate;
|
|
8882
|
-
}
|
|
8883
|
-
}
|
|
8884
|
-
throw new Error("Install Node/npm, Bun, or Deno to update threadnote.");
|
|
8885
|
-
}
|
|
8886
|
-
async function requireRuntime(runtime) {
|
|
8887
|
-
if (!await findExecutable([runtime])) {
|
|
8888
|
-
throw new Error(`${runtime} was requested but was not found on PATH.`);
|
|
8889
|
-
}
|
|
8890
|
-
}
|
|
8891
|
-
async function installedThreadnoteCommand(runtime) {
|
|
8892
|
-
const runtimeBin = await runtimeThreadnoteBin(runtime);
|
|
8893
|
-
if (runtimeBin && await isExecutable(runtimeBin)) {
|
|
8894
|
-
return runtimeBin;
|
|
8895
|
-
}
|
|
8896
|
-
return await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
|
|
8897
|
-
}
|
|
8898
|
-
async function runtimeThreadnoteBin(runtime) {
|
|
8899
|
-
if (runtime === "npm") {
|
|
8900
|
-
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
8901
|
-
const prefix = result.stdout.trim();
|
|
8902
|
-
return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
8903
|
-
}
|
|
8904
|
-
if (runtime === "bun") {
|
|
8905
|
-
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
8906
|
-
const binDir = result.stdout.trim();
|
|
8907
|
-
return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
8908
|
-
}
|
|
8909
|
-
return (0, import_node_path7.join)(process.env.DENO_INSTALL ?? (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
|
|
8910
|
-
}
|
|
8911
|
-
async function isExecutable(path) {
|
|
8912
|
-
try {
|
|
8913
|
-
await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
|
|
8914
|
-
return true;
|
|
8915
|
-
} catch (_err) {
|
|
8916
|
-
return false;
|
|
8917
|
-
}
|
|
8918
|
-
}
|
|
8919
|
-
function updatePackageCommand(runtime, registry) {
|
|
8920
|
-
if (runtime === "npm") {
|
|
8921
|
-
return { executable: "npm", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
|
|
8922
|
-
}
|
|
8923
|
-
if (runtime === "bun") {
|
|
8924
|
-
return { executable: "bun", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
|
|
8925
|
-
}
|
|
8926
|
-
return {
|
|
8927
|
-
executable: "env",
|
|
8928
|
-
args: [
|
|
8929
|
-
`NPM_CONFIG_REGISTRY=${registry}`,
|
|
8930
|
-
"deno",
|
|
8931
|
-
"install",
|
|
8932
|
-
"--global",
|
|
8933
|
-
"--force",
|
|
8934
|
-
"--name",
|
|
8935
|
-
NPM_PACKAGE_NAME,
|
|
8936
|
-
"--allow-read",
|
|
8937
|
-
"--allow-write",
|
|
8938
|
-
"--allow-run",
|
|
8939
|
-
"--allow-env",
|
|
8940
|
-
"--allow-net",
|
|
8941
|
-
`npm:${NPM_PACKAGE_NAME}@latest`
|
|
8942
|
-
]
|
|
8943
|
-
};
|
|
8944
|
-
}
|
|
8945
|
-
function normalizeRegistry(registry) {
|
|
8946
|
-
return registry.endsWith("/") ? registry : `${registry}/`;
|
|
8947
|
-
}
|
|
8948
|
-
function updateRegistry() {
|
|
8949
|
-
return normalizeRegistry(process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
|
|
8950
|
-
}
|
|
8951
|
-
function isUpdateNotificationDisabled() {
|
|
8952
|
-
return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
|
|
8953
|
-
}
|
|
8954
|
-
function compareVersions(currentVersion, latestVersion) {
|
|
8955
|
-
const current = parseVersion(currentVersion);
|
|
8956
|
-
const latest = parseVersion(latestVersion);
|
|
8957
|
-
for (let index = 0; index < 3; index += 1) {
|
|
8958
|
-
const difference = current.numbers[index] - latest.numbers[index];
|
|
8959
|
-
if (difference !== 0) {
|
|
8960
|
-
return difference;
|
|
8961
|
-
}
|
|
8962
|
-
}
|
|
8963
|
-
if (current.prerelease === latest.prerelease) {
|
|
8964
|
-
return 0;
|
|
8965
|
-
}
|
|
8966
|
-
if (current.prerelease === void 0) {
|
|
8967
|
-
return 1;
|
|
8968
|
-
}
|
|
8969
|
-
if (latest.prerelease === void 0) {
|
|
8970
|
-
return -1;
|
|
8971
|
-
}
|
|
8972
|
-
return current.prerelease.localeCompare(latest.prerelease);
|
|
8973
|
-
}
|
|
8974
|
-
function parseVersion(version) {
|
|
8975
|
-
const normalized = version.trim().replace(/^v/, "");
|
|
8976
|
-
const [core2, prerelease] = normalized.split("-", 2);
|
|
8977
|
-
const parts = core2.split(".").map((part) => Number(part));
|
|
8978
|
-
return {
|
|
8979
|
-
numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
|
|
8980
|
-
prerelease
|
|
8981
|
-
};
|
|
8982
|
-
}
|
|
8983
|
-
function safeVersionNumber(value) {
|
|
8984
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
8985
|
-
}
|
|
8986
|
-
|
|
8987
9740
|
// src/threadnote.ts
|
|
8988
9741
|
async function main() {
|
|
8989
9742
|
const program2 = new Command();
|
|
@@ -8998,14 +9751,17 @@ async function main() {
|
|
|
8998
9751
|
await runInstall(config, options);
|
|
8999
9752
|
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
9000
9753
|
});
|
|
9001
|
-
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").action(async (options) => {
|
|
9754
|
+
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) => {
|
|
9002
9755
|
await runUpdate(getRuntimeConfig(program2), options);
|
|
9003
9756
|
});
|
|
9757
|
+
program2.command("post-update", { hidden: true }).description("Run packaged post-update migration prompts").requiredOption("--from-version <version>", "Version before update").requiredOption("--to-version <version>", "Version after update").option("--dry-run", "Print post-update actions without running them").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
|
|
9758
|
+
await runPostUpdate(getRuntimeConfig(program2), options);
|
|
9759
|
+
});
|
|
9004
9760
|
program2.command("repair").description("Repair local OpenViking install, config files, server health, shim, manifest, and MCP config").option("--dry-run", "Print the repair actions without making changes").option(
|
|
9005
9761
|
"--mcp <clients>",
|
|
9006
|
-
"MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
|
|
9762
|
+
"MCP clients to repair: available, all, none, codex, claude, cursor, copilot, or comma-separated list",
|
|
9007
9763
|
"available"
|
|
9008
|
-
).option("--no-start", "Do not start OpenViking if health is failing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).action(async (options) => {
|
|
9764
|
+
).option("--no-start", "Do not start OpenViking if health is failing").option("--no-post-update", "Skip post-update migration prompts after repair").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).action(async (options) => {
|
|
9009
9765
|
const config = getRuntimeConfig(program2);
|
|
9010
9766
|
await runRepair(config, options);
|
|
9011
9767
|
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
@@ -9020,7 +9776,7 @@ async function main() {
|
|
|
9020
9776
|
});
|
|
9021
9777
|
program2.command("uninstall").description("Remove Threadnote setup and optionally erase local memories").option("--dry-run", "Print uninstall actions without making changes").option(
|
|
9022
9778
|
"--mcp <clients>",
|
|
9023
|
-
"MCP clients to remove: available, all, none, codex, claude, cursor, or comma-separated list",
|
|
9779
|
+
"MCP clients to remove: available, all, none, codex, claude, cursor, copilot, or comma-separated list",
|
|
9024
9780
|
"available"
|
|
9025
9781
|
).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
|
|
9026
9782
|
await runUninstall(getRuntimeConfig(program2), options);
|
|
@@ -9034,10 +9790,10 @@ async function main() {
|
|
|
9034
9790
|
program2.command("seed-skills").description("Seed Codex, Claude, and repo-local SKILL.md files as a searchable catalog").option("--dry-run", "Print skill files and ov commands without importing").option("--manifest <path>", "Manifest path for repo-local skill discovery").option("--native", "Use native OpenViking skill ingestion; requires a working VLM config").action(async (options) => {
|
|
9035
9791
|
await runSeedSkills(getRuntimeConfig(program2, options.manifest), options);
|
|
9036
9792
|
});
|
|
9037
|
-
program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, or
|
|
9793
|
+
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) => {
|
|
9038
9794
|
await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
|
|
9039
9795
|
});
|
|
9040
|
-
program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--replace <uri>", "Supersede an existing viking:// memory after the new memory is stored").option("--source-agent-client <name>", "codex, claude, cursor,
|
|
9796
|
+
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) => {
|
|
9041
9797
|
await runRemember(getRuntimeConfig(program2), options);
|
|
9042
9798
|
});
|
|
9043
9799
|
program2.command("migrate-memories").description("Migrate legacy session-only Threadnote memories into durable memory files").option("--all-accounts", "Scan all local OpenViking accounts under THREADNOTE_HOME").option("--dry-run", "Print migration actions without writing memories").option("--limit <count>", "Maximum number of memories to migrate").option(
|
|
@@ -9048,7 +9804,10 @@ async function main() {
|
|
|
9048
9804
|
).action(async (options) => {
|
|
9049
9805
|
await runMigrateMemories(getRuntimeConfig(program2), options);
|
|
9050
9806
|
});
|
|
9051
|
-
program2.command("
|
|
9807
|
+
program2.command("migrate-lifecycle").description("Move clear legacy handoff memories into lifecycle-aware archive paths").option("--apply", "Perform the migration; without this, prints a dry run").option("--dry-run", "Print migration actions without writing or removing memories").option("--limit <count>", "Maximum number of legacy handoffs to migrate").action(async (options) => {
|
|
9808
|
+
await runMigrateLifecycle(getRuntimeConfig(program2), options);
|
|
9809
|
+
});
|
|
9810
|
+
program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in exact durable-memory matches").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
|
|
9052
9811
|
await runRecall(getRuntimeConfig(program2), options);
|
|
9053
9812
|
});
|
|
9054
9813
|
program2.command("read").description("Read a viking:// URI returned by recall or list").argument("<uri>", "viking:// URI to read").option("--dry-run", "Print ov command without reading").action(async (uri, options) => {
|
|
@@ -9057,9 +9816,12 @@ async function main() {
|
|
|
9057
9816
|
program2.command("list").alias("ls").description("List a viking:// directory").argument("[uri]", "viking:// URI to list", "viking://").option("-a, --all", "Show hidden files such as .abstract.md and .overview.md").option("--dry-run", "Print ov command without listing").option("-n, --node-limit <count>", "Maximum number of nodes to list").option("-r, --recursive", "List subdirectories recursively").option("-s, --simple", "Print only paths").action(async (uri, options) => {
|
|
9058
9817
|
await runList(getRuntimeConfig(program2), uri, options);
|
|
9059
9818
|
});
|
|
9060
|
-
program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor,
|
|
9819
|
+
program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
|
|
9061
9820
|
await runHandoff(getRuntimeConfig(program2), options);
|
|
9062
9821
|
});
|
|
9822
|
+
program2.command("archive").description("Move a memory into the archived lifecycle tree, then remove the original after the archive is stored").argument("<uri>", "viking:// memory URI to archive").option("--dry-run", "Print archive content and ov commands without changing anything").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind).option("--project <name>", "Override inferred project/repo namespace").option("--topic <name>", "Override inferred topic").action(async (uri, options) => {
|
|
9823
|
+
await runArchive(getRuntimeConfig(program2), uri, options);
|
|
9824
|
+
});
|
|
9063
9825
|
program2.command("forget").description("Remove a viking:// URI from local OpenViking context").argument("<uri>", "viking:// URI to remove").option("--dry-run", "Print ov command without deleting").action(async (uri, options) => {
|
|
9064
9826
|
await runForget(getRuntimeConfig(program2), uri, options);
|
|
9065
9827
|
});
|