threadnote 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -15
- package/dist/mcp_server.cjs +4086 -1001
- package/dist/threadnote.cjs +1079 -90
- package/docs/agent-instructions.md +59 -5
- package/docs/migration.md +26 -10
- package/docs/rollout.md +1 -1
- package/docs/security.md +5 -4
- package/docs/share.md +151 -0
- package/docs/troubleshooting.md +11 -1
- package/package.json +1 -1
package/dist/threadnote.cjs
CHANGED
|
@@ -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",
|
|
@@ -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");
|
|
@@ -7209,9 +7382,9 @@ async function runRead(config, uri, options) {
|
|
|
7209
7382
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
7210
7383
|
const result = await maybeRun(options.dryRun === true, ov, withIdentity(config, ["read", uri]));
|
|
7211
7384
|
if (result && result.stdout.includes("[Directory overview is not ready]") && (uri.endsWith("/.overview.md") || uri.endsWith("/.abstract.md"))) {
|
|
7212
|
-
const
|
|
7385
|
+
const parentUri2 = parentVikingUri(uri);
|
|
7213
7386
|
console.log("\nThis is a generated summary placeholder. To read the underlying content, inspect leaf nodes:");
|
|
7214
|
-
console.log(` threadnote list ${
|
|
7387
|
+
console.log(` threadnote list ${parentUri2} --all --recursive`);
|
|
7215
7388
|
}
|
|
7216
7389
|
}
|
|
7217
7390
|
async function runList(config, uri, options) {
|
|
@@ -7554,6 +7727,7 @@ function exactMemoryScopes(config, includeArchived) {
|
|
|
7554
7727
|
`${userBase}/handoffs/active`,
|
|
7555
7728
|
`${userBase}/incidents/active`,
|
|
7556
7729
|
`${userBase}/events`,
|
|
7730
|
+
`${userBase}/shared`,
|
|
7557
7731
|
`viking://agent/${uriSegment(config.agentId)}/memories`
|
|
7558
7732
|
];
|
|
7559
7733
|
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
@@ -8222,19 +8396,780 @@ function detectSecretMatches(content) {
|
|
|
8222
8396
|
return matches;
|
|
8223
8397
|
}
|
|
8224
8398
|
|
|
8399
|
+
// src/share.ts
|
|
8400
|
+
var import_promises6 = require("node:fs/promises");
|
|
8401
|
+
var import_node_os4 = require("node:os");
|
|
8402
|
+
var import_node_path6 = require("node:path");
|
|
8403
|
+
var TEAMS_FILE_VERSION = 1;
|
|
8404
|
+
var SHARED_SEGMENT = "shared";
|
|
8405
|
+
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
8406
|
+
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
8407
|
+
var SCRUBBER_PATTERNS = [
|
|
8408
|
+
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
8409
|
+
{ name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
8410
|
+
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
8411
|
+
{ name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
|
|
8412
|
+
{ name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
|
|
8413
|
+
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
8414
|
+
// Matches bare JWTs (three base64url segments). May surface a JWE token in
|
|
8415
|
+
// legitimate docs; if that becomes noisy we can switch to warn-only.
|
|
8416
|
+
{ name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
8417
|
+
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
8418
|
+
// Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
|
|
8419
|
+
// xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
|
|
8420
|
+
{ name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i }
|
|
8421
|
+
];
|
|
8422
|
+
async function runShareInit(config, remoteUrl, options) {
|
|
8423
|
+
if (!remoteUrl.trim()) {
|
|
8424
|
+
throw new Error("Provide a git remote URL for the shared memories repo.");
|
|
8425
|
+
}
|
|
8426
|
+
const dryRun = options.dryRun === true;
|
|
8427
|
+
const teamName = normalizeTeamName(options.team);
|
|
8428
|
+
const teamsFile = await readTeamsFile(config);
|
|
8429
|
+
if (teamsFile.teams[teamName]) {
|
|
8430
|
+
throw new Error(
|
|
8431
|
+
`Team "${teamName}" is already configured (remote ${teamsFile.teams[teamName].remote}). Remove it first with: threadnote share remove --team ${teamName}`
|
|
8432
|
+
);
|
|
8433
|
+
}
|
|
8434
|
+
const worktree = teamWorktreePath(config, teamName);
|
|
8435
|
+
const gitdir = teamGitdirPath(config, teamName);
|
|
8436
|
+
await assertWorktreeUsable(worktree);
|
|
8437
|
+
if (await exists(gitdir)) {
|
|
8438
|
+
throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
|
|
8439
|
+
}
|
|
8440
|
+
await ensureDirectory((0, import_node_path6.dirname)(worktree), dryRun);
|
|
8441
|
+
await ensureDirectory((0, import_node_path6.dirname)(gitdir), dryRun);
|
|
8442
|
+
const git = await requiredExecutable("git");
|
|
8443
|
+
await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
|
|
8444
|
+
const newConfig = {
|
|
8445
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8446
|
+
gitdir,
|
|
8447
|
+
name: teamName,
|
|
8448
|
+
remote: remoteUrl,
|
|
8449
|
+
worktree
|
|
8450
|
+
};
|
|
8451
|
+
const updatedTeams = {
|
|
8452
|
+
defaultTeam: shouldSetDefault(options, teamsFile) ? teamName : teamsFile.defaultTeam ?? teamName,
|
|
8453
|
+
teams: { ...teamsFile.teams, [teamName]: newConfig },
|
|
8454
|
+
version: TEAMS_FILE_VERSION
|
|
8455
|
+
};
|
|
8456
|
+
if (dryRun) {
|
|
8457
|
+
console.log(`Would write teams file: ${teamsFilePath(config)}`);
|
|
8458
|
+
console.log(`Would set ${teamName} as default? ${updatedTeams.defaultTeam === teamName}`);
|
|
8459
|
+
} else {
|
|
8460
|
+
await writeTeamsFile(config, updatedTeams);
|
|
8461
|
+
console.log(`Configured shared team "${teamName}" -> ${portablePath(worktree)}`);
|
|
8462
|
+
}
|
|
8463
|
+
if (!dryRun) {
|
|
8464
|
+
await ensureSharedGitignore(worktree, git, options.push !== false);
|
|
8465
|
+
const ingested = await ingestWorktreeFiles(config, newConfig, "create");
|
|
8466
|
+
console.log(`Ingested ${ingested} shared memory file(s) into OpenViking.`);
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
|
|
8470
|
+
var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
|
|
8471
|
+
async function ensureSharedGitignore(worktree, git, push) {
|
|
8472
|
+
const gitignorePath = (0, import_node_path6.join)(worktree, ".gitignore");
|
|
8473
|
+
const existing = await readFileIfExists(gitignorePath) ?? "";
|
|
8474
|
+
const lines = existing.split("\n").map((line) => line.trim());
|
|
8475
|
+
const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
8476
|
+
if (missingPatterns.length === 0) {
|
|
8477
|
+
return;
|
|
8478
|
+
}
|
|
8479
|
+
const hasHeader = lines.includes(SHARED_GITIGNORE_HEADER);
|
|
8480
|
+
const segments = [];
|
|
8481
|
+
if (existing.length > 0 && !existing.endsWith("\n")) {
|
|
8482
|
+
segments.push("\n");
|
|
8483
|
+
}
|
|
8484
|
+
if (existing.length > 0) {
|
|
8485
|
+
segments.push("\n");
|
|
8486
|
+
}
|
|
8487
|
+
if (!hasHeader) {
|
|
8488
|
+
segments.push(SHARED_GITIGNORE_HEADER, "\n");
|
|
8489
|
+
}
|
|
8490
|
+
segments.push(missingPatterns.join("\n"), "\n");
|
|
8491
|
+
await (0, import_promises6.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
|
|
8492
|
+
console.log(`Added ${missingPatterns.join(", ")} to ${portablePath(gitignorePath)}`);
|
|
8493
|
+
await maybeRun(false, git, ["-C", worktree, "add", ".gitignore"]);
|
|
8494
|
+
const commitResult = await runCommand(
|
|
8495
|
+
git,
|
|
8496
|
+
["-C", worktree, "commit", "-m", "share: ignore OpenViking directory summaries"],
|
|
8497
|
+
{ allowFailure: true }
|
|
8498
|
+
);
|
|
8499
|
+
if (commitResult.exitCode !== 0) {
|
|
8500
|
+
const detail = commitResult.stderr.trim() || commitResult.stdout.trim();
|
|
8501
|
+
if (!/nothing to commit|no changes added/i.test(detail)) {
|
|
8502
|
+
console.warn(
|
|
8503
|
+
`.gitignore housekeeping commit was rejected (${detail || "unknown"}); it will be retried on the next share sync.`
|
|
8504
|
+
);
|
|
8505
|
+
return;
|
|
8506
|
+
}
|
|
8507
|
+
}
|
|
8508
|
+
if (push) {
|
|
8509
|
+
await maybeRun(false, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8510
|
+
}
|
|
8511
|
+
}
|
|
8512
|
+
async function runShareStatus(config, options) {
|
|
8513
|
+
const team = await resolveTeam(config, options.team);
|
|
8514
|
+
const git = await requiredExecutable("git");
|
|
8515
|
+
console.log(`Team: ${team.name}`);
|
|
8516
|
+
console.log(`Remote: ${team.config.remote}`);
|
|
8517
|
+
console.log(`Worktree: ${portablePath(team.config.worktree)}`);
|
|
8518
|
+
console.log(`Gitdir: ${portablePath(team.config.gitdir)}`);
|
|
8519
|
+
await maybeRun(options.dryRun === true, git, ["-C", team.config.worktree, "status", "--short", "--branch"]);
|
|
8520
|
+
await maybeRun(options.dryRun === true, git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
|
|
8521
|
+
allowFailure: true
|
|
8522
|
+
});
|
|
8523
|
+
const ahead = await gitOutput(team.config.worktree, ["rev-list", "--count", "@{u}..HEAD"], options.dryRun === true);
|
|
8524
|
+
const behind = await gitOutput(team.config.worktree, ["rev-list", "--count", "HEAD..@{u}"], options.dryRun === true);
|
|
8525
|
+
if (ahead !== void 0) {
|
|
8526
|
+
console.log(`Ahead of upstream: ${ahead}`);
|
|
8527
|
+
}
|
|
8528
|
+
if (behind !== void 0) {
|
|
8529
|
+
console.log(`Behind upstream: ${behind}`);
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
async function runShareSync(config, options) {
|
|
8533
|
+
const team = await resolveTeam(config, options.team);
|
|
8534
|
+
const dryRun = options.dryRun === true;
|
|
8535
|
+
const git = await requiredExecutable("git");
|
|
8536
|
+
const worktree = team.config.worktree;
|
|
8537
|
+
if (!dryRun) {
|
|
8538
|
+
await ensureSharedGitignore(worktree, git, false);
|
|
8539
|
+
}
|
|
8540
|
+
if (await hasUncommittedChanges(worktree)) {
|
|
8541
|
+
if (options.autoCommit === false) {
|
|
8542
|
+
throw new Error(
|
|
8543
|
+
`Worktree ${worktree} has uncommitted changes. Commit them yourself or rerun without --no-auto-commit.`
|
|
8544
|
+
);
|
|
8545
|
+
}
|
|
8546
|
+
const message = options.message ?? `share: sync ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
8547
|
+
await stageShareableChanges(dryRun, git, worktree);
|
|
8548
|
+
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
8549
|
+
}
|
|
8550
|
+
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], dryRun);
|
|
8551
|
+
await maybeRun(dryRun, git, ["-C", worktree, "fetch", DEFAULT_GIT_REMOTE_NAME]);
|
|
8552
|
+
const pullResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8553
|
+
if (dryRun) {
|
|
8554
|
+
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
8555
|
+
} else if (pullResult && pullResult.exitCode !== 0) {
|
|
8556
|
+
if (await exists((0, import_node_path6.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path6.join)(team.config.gitdir, "rebase-apply"))) {
|
|
8557
|
+
throw new Error(
|
|
8558
|
+
`git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
|
|
8559
|
+
Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
|
|
8560
|
+
);
|
|
8561
|
+
}
|
|
8562
|
+
throw new Error(
|
|
8563
|
+
`git pull --rebase failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
|
|
8564
|
+
);
|
|
8565
|
+
}
|
|
8566
|
+
const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], dryRun);
|
|
8567
|
+
if (!dryRun && beforeRev && afterRev && beforeRev !== afterRev) {
|
|
8568
|
+
const changes = await listChangedFiles(worktree, beforeRev, afterRev);
|
|
8569
|
+
await applyChangesToOpenViking(config, team.config, changes);
|
|
8570
|
+
console.log(`Reindexed ${changes.length} file change(s) into OpenViking.`);
|
|
8571
|
+
} else if (!dryRun) {
|
|
8572
|
+
console.log("No upstream changes to reindex.");
|
|
8573
|
+
}
|
|
8574
|
+
if (options.push !== false) {
|
|
8575
|
+
await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8578
|
+
async function stageShareableChanges(dryRun, git, worktree) {
|
|
8579
|
+
const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_MEMORY_KIND_DIRS.map((dir) => `:(top)${dir}`)];
|
|
8580
|
+
await maybeRun(dryRun, git, ["-C", worktree, "add", "--", ...pathspecs], { allowFailure: true });
|
|
8581
|
+
}
|
|
8582
|
+
async function runSharePublish(config, sourceUri, options) {
|
|
8583
|
+
assertVikingUri(sourceUri);
|
|
8584
|
+
const team = await resolveTeam(config, options.team);
|
|
8585
|
+
const dryRun = options.dryRun === true;
|
|
8586
|
+
if (isInSharedNamespace(config, sourceUri)) {
|
|
8587
|
+
throw new Error(`Memory ${sourceUri} is already in the shared namespace.`);
|
|
8588
|
+
}
|
|
8589
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
8590
|
+
const content = await readMemoryContent(config, ov, sourceUri, dryRun);
|
|
8591
|
+
const blocker = scrubberBlocker(content);
|
|
8592
|
+
if (blocker) {
|
|
8593
|
+
throw new Error(`Refusing to publish ${sourceUri}: possible ${blocker}. Strip the sensitive value, then retry.`);
|
|
8594
|
+
}
|
|
8595
|
+
const targetUri = sharedUriFor(config, sourceUri, team.name);
|
|
8596
|
+
if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
|
|
8597
|
+
throw new Error(
|
|
8598
|
+
`Refusing to publish: ${targetUri} already exists in the shared namespace. Inspect it via threadnote read; if it should be replaced, forget the existing shared copy first.`
|
|
8599
|
+
);
|
|
8600
|
+
}
|
|
8601
|
+
await ensureSharedDirectoryChain(config, ov, targetUri, dryRun);
|
|
8602
|
+
await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
|
|
8603
|
+
await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "publish");
|
|
8604
|
+
const git = await requiredExecutable("git");
|
|
8605
|
+
const worktree = team.config.worktree;
|
|
8606
|
+
const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
|
|
8607
|
+
const message = options.message ?? `share: publish ${relativePath}`;
|
|
8608
|
+
await maybeRun(dryRun, git, ["-C", worktree, "add", "--", relativePath]);
|
|
8609
|
+
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
8610
|
+
if (options.push !== false) {
|
|
8611
|
+
const pushResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8612
|
+
if (dryRun) {
|
|
8613
|
+
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
8614
|
+
} else if (pushResult && pushResult.exitCode !== 0) {
|
|
8615
|
+
const detail = pushResult.stderr.trim() || pushResult.stdout.trim() || "unknown error";
|
|
8616
|
+
throw new Error(
|
|
8617
|
+
`Memory was committed locally but git push failed: ${detail}
|
|
8618
|
+
Resolve the remote issue (auth, network, branch protection), then run: threadnote share sync`
|
|
8619
|
+
);
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8622
|
+
console.log(`Published ${sourceUri} -> ${targetUri}`);
|
|
8623
|
+
}
|
|
8624
|
+
async function runShareUnpublish(config, sourceUri, options) {
|
|
8625
|
+
assertVikingUri(sourceUri);
|
|
8626
|
+
const team = await resolveTeam(config, options.team);
|
|
8627
|
+
const dryRun = options.dryRun === true;
|
|
8628
|
+
if (!isInTeamNamespace(config, sourceUri, team.name)) {
|
|
8629
|
+
throw new Error(`Memory ${sourceUri} is not in team "${team.name}" shared namespace.`);
|
|
8630
|
+
}
|
|
8631
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
8632
|
+
const content = await readMemoryContent(config, ov, sourceUri, dryRun);
|
|
8633
|
+
const targetUri = personalUriFor(config, sourceUri, team.name);
|
|
8634
|
+
if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
|
|
8635
|
+
throw new Error(
|
|
8636
|
+
`Refusing to unpublish: a personal memory already exists at ${targetUri}. Move or forget it first, then retry.`
|
|
8637
|
+
);
|
|
8638
|
+
}
|
|
8639
|
+
await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
|
|
8640
|
+
await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "unpublish");
|
|
8641
|
+
const git = await requiredExecutable("git");
|
|
8642
|
+
const worktree = team.config.worktree;
|
|
8643
|
+
const relativePath = vikingUriToWorktreeRelative(config, sourceUri, team.name);
|
|
8644
|
+
const message = options.message ?? `share: unpublish ${relativePath}`;
|
|
8645
|
+
await maybeRun(dryRun, git, ["-C", worktree, "rm", relativePath], { allowFailure: true });
|
|
8646
|
+
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
8647
|
+
if (options.push !== false) {
|
|
8648
|
+
await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8649
|
+
}
|
|
8650
|
+
console.log(`Unpublished ${sourceUri} -> ${targetUri}`);
|
|
8651
|
+
}
|
|
8652
|
+
async function runShareList(config, _options) {
|
|
8653
|
+
const teams = await readTeamsFile(config);
|
|
8654
|
+
const entries = Object.values(teams.teams);
|
|
8655
|
+
if (entries.length === 0) {
|
|
8656
|
+
console.log("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
8657
|
+
return;
|
|
8658
|
+
}
|
|
8659
|
+
for (const team of entries) {
|
|
8660
|
+
const marker = team.name === teams.defaultTeam ? " (default)" : "";
|
|
8661
|
+
console.log(`- ${team.name}${marker}`);
|
|
8662
|
+
console.log(` remote: ${team.remote}`);
|
|
8663
|
+
console.log(` worktree: ${portablePath(team.worktree)}`);
|
|
8664
|
+
console.log(` gitdir: ${portablePath(team.gitdir)}`);
|
|
8665
|
+
console.log(` added: ${team.addedAt}`);
|
|
8666
|
+
}
|
|
8667
|
+
}
|
|
8668
|
+
async function runShareRemove(config, options) {
|
|
8669
|
+
const team = await resolveTeam(config, options.team);
|
|
8670
|
+
const dryRun = options.dryRun === true;
|
|
8671
|
+
const teamsFile = await readTeamsFile(config);
|
|
8672
|
+
const remaining = {};
|
|
8673
|
+
for (const [name, value] of Object.entries(teamsFile.teams)) {
|
|
8674
|
+
if (name !== team.name) {
|
|
8675
|
+
remaining[name] = value;
|
|
8676
|
+
}
|
|
8677
|
+
}
|
|
8678
|
+
const remainingNames = Object.keys(remaining);
|
|
8679
|
+
const nextDefault = teamsFile.defaultTeam === team.name ? remainingNames[0] : teamsFile.defaultTeam;
|
|
8680
|
+
const updated = { defaultTeam: nextDefault, teams: remaining, version: TEAMS_FILE_VERSION };
|
|
8681
|
+
if (dryRun) {
|
|
8682
|
+
console.log(`Would update teams file: ${teamsFilePath(config)}`);
|
|
8683
|
+
} else {
|
|
8684
|
+
await writeTeamsFile(config, updated);
|
|
8685
|
+
console.log(`Removed team "${team.name}" from teams.json.`);
|
|
8686
|
+
}
|
|
8687
|
+
if (options.keepFiles !== true) {
|
|
8688
|
+
await removePath(team.config.worktree, "shared worktree", dryRun);
|
|
8689
|
+
await removePath(team.config.gitdir, "shared gitdir", dryRun);
|
|
8690
|
+
} else {
|
|
8691
|
+
console.log(`Keeping files at ${portablePath(team.config.worktree)} and ${portablePath(team.config.gitdir)}`);
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
function normalizeTeamName(input2) {
|
|
8695
|
+
const candidate = (input2 ?? "default").trim();
|
|
8696
|
+
if (!candidate) {
|
|
8697
|
+
return "default";
|
|
8698
|
+
}
|
|
8699
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(candidate) || /^\.+$/.test(candidate)) {
|
|
8700
|
+
throw new Error(
|
|
8701
|
+
`Invalid team name "${input2}". Team names must start with a lowercase letter or digit and contain only [a-z0-9._-]. Single-dot or dot-only names are rejected so they don't collapse to the shared-root or parent directory.`
|
|
8702
|
+
);
|
|
8703
|
+
}
|
|
8704
|
+
return candidate;
|
|
8705
|
+
}
|
|
8706
|
+
function teamsFilePath(config) {
|
|
8707
|
+
return (0, import_node_path6.join)(config.agentContextHome, "share", "teams.json");
|
|
8708
|
+
}
|
|
8709
|
+
function teamWorktreePath(config, team) {
|
|
8710
|
+
return (0, import_node_path6.join)(
|
|
8711
|
+
config.agentContextHome,
|
|
8712
|
+
"data",
|
|
8713
|
+
"viking",
|
|
8714
|
+
config.account,
|
|
8715
|
+
"user",
|
|
8716
|
+
uriSegment(config.user),
|
|
8717
|
+
"memories",
|
|
8718
|
+
SHARED_SEGMENT,
|
|
8719
|
+
team
|
|
8720
|
+
);
|
|
8721
|
+
}
|
|
8722
|
+
function teamGitdirPath(config, team) {
|
|
8723
|
+
return (0, import_node_path6.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
8724
|
+
}
|
|
8725
|
+
async function readTeamsFile(config) {
|
|
8726
|
+
const path = teamsFilePath(config);
|
|
8727
|
+
const raw = await readFileIfExists(path);
|
|
8728
|
+
if (!raw) {
|
|
8729
|
+
return { teams: {}, version: TEAMS_FILE_VERSION };
|
|
8730
|
+
}
|
|
8731
|
+
const parsed = parseJsonConfigObject(raw);
|
|
8732
|
+
if (!parsed) {
|
|
8733
|
+
throw new Error(`Could not parse teams file ${path}`);
|
|
8734
|
+
}
|
|
8735
|
+
if (typeof parsed.version === "number" && parsed.version > TEAMS_FILE_VERSION) {
|
|
8736
|
+
throw new Error(
|
|
8737
|
+
`Teams file ${path} was written with version ${parsed.version}; this Threadnote binary understands up to version ${TEAMS_FILE_VERSION}. Upgrade Threadnote (\`threadnote update\`) before continuing.`
|
|
8738
|
+
);
|
|
8739
|
+
}
|
|
8740
|
+
const teams = {};
|
|
8741
|
+
if (typeof parsed.teams === "object" && parsed.teams !== null && !Array.isArray(parsed.teams)) {
|
|
8742
|
+
for (const [name, value] of Object.entries(parsed.teams)) {
|
|
8743
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
8744
|
+
console.warn(`Skipping non-object team entry "${name}" in ${path}.`);
|
|
8745
|
+
continue;
|
|
8746
|
+
}
|
|
8747
|
+
const entry = value;
|
|
8748
|
+
if (typeof entry.remote !== "string" || entry.remote.length === 0) {
|
|
8749
|
+
console.warn(`Skipping team entry "${name}" in ${path}: missing or empty "remote" field.`);
|
|
8750
|
+
continue;
|
|
8751
|
+
}
|
|
8752
|
+
teams[name] = {
|
|
8753
|
+
addedAt: typeof entry.addedAt === "string" ? entry.addedAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
8754
|
+
gitdir: typeof entry.gitdir === "string" ? entry.gitdir : teamGitdirPath(config, name),
|
|
8755
|
+
name,
|
|
8756
|
+
remote: entry.remote,
|
|
8757
|
+
worktree: typeof entry.worktree === "string" ? entry.worktree : teamWorktreePath(config, name)
|
|
8758
|
+
};
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8761
|
+
const defaultTeam = typeof parsed.defaultTeam === "string" ? parsed.defaultTeam : void 0;
|
|
8762
|
+
return { defaultTeam, teams, version: TEAMS_FILE_VERSION };
|
|
8763
|
+
}
|
|
8764
|
+
async function writeTeamsFile(config, contents) {
|
|
8765
|
+
const path = teamsFilePath(config);
|
|
8766
|
+
await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(path), { recursive: true });
|
|
8767
|
+
const serializable = {
|
|
8768
|
+
defaultTeam: contents.defaultTeam,
|
|
8769
|
+
teams: contents.teams,
|
|
8770
|
+
version: contents.version
|
|
8771
|
+
};
|
|
8772
|
+
await (0, import_promises6.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
|
|
8773
|
+
`, { encoding: "utf8", mode: 384 });
|
|
8774
|
+
}
|
|
8775
|
+
async function resolveTeam(config, requested) {
|
|
8776
|
+
const teamsFile = await readTeamsFile(config);
|
|
8777
|
+
const entries = Object.entries(teamsFile.teams);
|
|
8778
|
+
if (entries.length === 0) {
|
|
8779
|
+
throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
8780
|
+
}
|
|
8781
|
+
const wantName = requested ? normalizeTeamName(requested) : teamsFile.defaultTeam ?? entries[0][0];
|
|
8782
|
+
const found = teamsFile.teams[wantName];
|
|
8783
|
+
if (!found) {
|
|
8784
|
+
const known = entries.map(([name]) => name).join(", ");
|
|
8785
|
+
throw new Error(`Team "${wantName}" is not configured. Known teams: ${known}`);
|
|
8786
|
+
}
|
|
8787
|
+
return { config: found, name: wantName };
|
|
8788
|
+
}
|
|
8789
|
+
function shouldSetDefault(options, existing) {
|
|
8790
|
+
if (options.setDefault === true) {
|
|
8791
|
+
return true;
|
|
8792
|
+
}
|
|
8793
|
+
return existing.defaultTeam === void 0;
|
|
8794
|
+
}
|
|
8795
|
+
async function assertWorktreeUsable(worktree) {
|
|
8796
|
+
if (!await exists(worktree)) {
|
|
8797
|
+
return;
|
|
8798
|
+
}
|
|
8799
|
+
if (!await isDirectory(worktree)) {
|
|
8800
|
+
throw new Error(`Cannot use ${worktree} as a worktree: not a directory.`);
|
|
8801
|
+
}
|
|
8802
|
+
const entries = await (0, import_promises6.readdir)(worktree);
|
|
8803
|
+
if (entries.length > 0) {
|
|
8804
|
+
const preview = entries.slice(0, 5).join(", ");
|
|
8805
|
+
const suffix = entries.length > 5 ? `, +${entries.length - 5} more` : "";
|
|
8806
|
+
throw new Error(
|
|
8807
|
+
`Worktree ${worktree} is not empty (contains: ${preview}${suffix}). Move or remove its contents, then retry threadnote share init.`
|
|
8808
|
+
);
|
|
8809
|
+
}
|
|
8810
|
+
}
|
|
8811
|
+
async function ingestWorktreeFiles(config, team, initialMode) {
|
|
8812
|
+
const ov = await openVikingCliForMode(false);
|
|
8813
|
+
const files = await walkMemoryFiles(team.worktree);
|
|
8814
|
+
for (const file of files) {
|
|
8815
|
+
const uri = workfileToVikingUri(config, team, file);
|
|
8816
|
+
await ensureSharedDirectoryChain(config, ov, uri, false);
|
|
8817
|
+
await ingestSingleFile(ov, config, uri, file, initialMode);
|
|
8818
|
+
}
|
|
8819
|
+
return files.length;
|
|
8820
|
+
}
|
|
8821
|
+
async function walkMemoryFiles(root) {
|
|
8822
|
+
const out = [];
|
|
8823
|
+
async function visit(path, depth) {
|
|
8824
|
+
let entries;
|
|
8825
|
+
try {
|
|
8826
|
+
entries = await (0, import_promises6.readdir)(path, { withFileTypes: true });
|
|
8827
|
+
} catch (err) {
|
|
8828
|
+
console.warn(`Skipping ${path} during shared-tree walk: ${err instanceof Error ? err.message : String(err)}`);
|
|
8829
|
+
return;
|
|
8830
|
+
}
|
|
8831
|
+
for (const entry of entries) {
|
|
8832
|
+
if (entry.name === ".git") {
|
|
8833
|
+
continue;
|
|
8834
|
+
}
|
|
8835
|
+
const full = (0, import_node_path6.join)(path, entry.name);
|
|
8836
|
+
if (entry.isDirectory()) {
|
|
8837
|
+
if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
|
|
8838
|
+
continue;
|
|
8839
|
+
}
|
|
8840
|
+
await visit(full, depth + 1);
|
|
8841
|
+
continue;
|
|
8842
|
+
}
|
|
8843
|
+
if (!entry.isFile()) {
|
|
8844
|
+
continue;
|
|
8845
|
+
}
|
|
8846
|
+
if (depth === 0) {
|
|
8847
|
+
continue;
|
|
8848
|
+
}
|
|
8849
|
+
if (!entry.name.endsWith(".md")) {
|
|
8850
|
+
continue;
|
|
8851
|
+
}
|
|
8852
|
+
out.push(full);
|
|
8853
|
+
}
|
|
8854
|
+
}
|
|
8855
|
+
await visit(root, 0);
|
|
8856
|
+
return out;
|
|
8857
|
+
}
|
|
8858
|
+
function workfileToVikingUri(config, team, filePath) {
|
|
8859
|
+
const rel = (0, import_node_path6.relative)(team.worktree, filePath).split(import_node_path6.sep).join("/");
|
|
8860
|
+
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
8861
|
+
}
|
|
8862
|
+
function isInSharedNamespace(config, uri) {
|
|
8863
|
+
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`);
|
|
8864
|
+
}
|
|
8865
|
+
function isInTeamNamespace(config, uri, team) {
|
|
8866
|
+
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`);
|
|
8867
|
+
}
|
|
8868
|
+
function sharedUriFor(config, personalUri, team) {
|
|
8869
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/`;
|
|
8870
|
+
if (!personalUri.startsWith(prefix)) {
|
|
8871
|
+
throw new Error(`Refusing to publish memory outside the current user namespace: ${personalUri}`);
|
|
8872
|
+
}
|
|
8873
|
+
const rest = personalUri.slice(prefix.length);
|
|
8874
|
+
return `${prefix}${SHARED_SEGMENT}/${team}/${rest}`;
|
|
8875
|
+
}
|
|
8876
|
+
function personalUriFor(config, sharedUri, team) {
|
|
8877
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`;
|
|
8878
|
+
if (!sharedUri.startsWith(prefix)) {
|
|
8879
|
+
throw new Error(`Refusing to unpublish a URI outside team "${team}" shared namespace: ${sharedUri}`);
|
|
8880
|
+
}
|
|
8881
|
+
const rest = sharedUri.slice(prefix.length);
|
|
8882
|
+
return `viking://user/${uriSegment(config.user)}/memories/${rest}`;
|
|
8883
|
+
}
|
|
8884
|
+
function vikingUriToWorktreeRelative(config, uri, team) {
|
|
8885
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`;
|
|
8886
|
+
if (!uri.startsWith(prefix)) {
|
|
8887
|
+
throw new Error(`URI ${uri} is not inside team "${team}" shared subtree.`);
|
|
8888
|
+
}
|
|
8889
|
+
return uri.slice(prefix.length);
|
|
8890
|
+
}
|
|
8891
|
+
function scrubberBlocker(content) {
|
|
8892
|
+
return SCRUBBER_PATTERNS.find((pattern) => pattern.regex.test(content))?.name;
|
|
8893
|
+
}
|
|
8894
|
+
async function readMemoryContent(config, ov, uri, dryRun) {
|
|
8895
|
+
const args = withIdentity(config, ["read", uri]);
|
|
8896
|
+
if (dryRun) {
|
|
8897
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8898
|
+
return "<dry-run memory body>";
|
|
8899
|
+
}
|
|
8900
|
+
const result = await runCommand(ov, args);
|
|
8901
|
+
if (!result.stdout.trim()) {
|
|
8902
|
+
throw new Error(`Refusing to publish empty memory at ${uri}`);
|
|
8903
|
+
}
|
|
8904
|
+
return result.stdout;
|
|
8905
|
+
}
|
|
8906
|
+
async function ensureSharedDirectoryChain(config, ov, memoryUri, dryRun) {
|
|
8907
|
+
const directoryUri = parentUri(memoryUri);
|
|
8908
|
+
for (const uri of sharedDirectoryChain(config, directoryUri)) {
|
|
8909
|
+
const args = withIdentity(config, ["stat", uri]);
|
|
8910
|
+
if (dryRun) {
|
|
8911
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8912
|
+
continue;
|
|
8913
|
+
}
|
|
8914
|
+
const statResult = await runCommand(ov, args, { allowFailure: true });
|
|
8915
|
+
if (statResult.exitCode === 0) {
|
|
8916
|
+
continue;
|
|
8917
|
+
}
|
|
8918
|
+
await maybeRun(false, ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared memories."]));
|
|
8919
|
+
}
|
|
8920
|
+
}
|
|
8921
|
+
function parentUri(uri) {
|
|
8922
|
+
const lastSlash = uri.lastIndexOf("/");
|
|
8923
|
+
return lastSlash === -1 ? uri : uri.slice(0, lastSlash);
|
|
8924
|
+
}
|
|
8925
|
+
function sharedDirectoryChain(config, directoryUri) {
|
|
8926
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
|
|
8927
|
+
if (!directoryUri.startsWith(prefix)) {
|
|
8928
|
+
return [directoryUri];
|
|
8929
|
+
}
|
|
8930
|
+
const parts = directoryUri.slice(prefix.length).split("/").filter(Boolean);
|
|
8931
|
+
const chain = [];
|
|
8932
|
+
for (let index = 1; index <= parts.length; index += 1) {
|
|
8933
|
+
chain.push(`${prefix}${parts.slice(0, index).join("/")}`);
|
|
8934
|
+
}
|
|
8935
|
+
return chain;
|
|
8936
|
+
}
|
|
8937
|
+
async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun) {
|
|
8938
|
+
if (dryRun) {
|
|
8939
|
+
const args = withIdentity(config, [
|
|
8940
|
+
"write",
|
|
8941
|
+
uri,
|
|
8942
|
+
"--from-file",
|
|
8943
|
+
"<staged temp file>",
|
|
8944
|
+
"--mode",
|
|
8945
|
+
initialMode,
|
|
8946
|
+
"--wait",
|
|
8947
|
+
"--timeout",
|
|
8948
|
+
"120"
|
|
8949
|
+
]);
|
|
8950
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8951
|
+
return;
|
|
8952
|
+
}
|
|
8953
|
+
const stagingDir = await (0, import_promises6.mkdtemp)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
|
|
8954
|
+
const tempPath = (0, import_node_path6.join)(stagingDir, "body.txt");
|
|
8955
|
+
try {
|
|
8956
|
+
await (0, import_promises6.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
8957
|
+
await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode);
|
|
8958
|
+
} finally {
|
|
8959
|
+
await (0, import_promises6.rm)(stagingDir, { force: true, recursive: true });
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8962
|
+
async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode) {
|
|
8963
|
+
const maxAttempts = 4;
|
|
8964
|
+
const existedBeforeWrite = await vikingResourceExists2(ov, config, uri);
|
|
8965
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
8966
|
+
const existsNow = attempt === 0 ? existedBeforeWrite : await vikingResourceExists2(ov, config, uri);
|
|
8967
|
+
const ourWriteLanded = existsNow && !existedBeforeWrite;
|
|
8968
|
+
const mode = attempt === 0 ? initialMode : ourWriteLanded ? "replace" : initialMode;
|
|
8969
|
+
const args = withIdentity(config, [
|
|
8970
|
+
"write",
|
|
8971
|
+
uri,
|
|
8972
|
+
"--from-file",
|
|
8973
|
+
fromFile,
|
|
8974
|
+
"--mode",
|
|
8975
|
+
mode,
|
|
8976
|
+
"--wait",
|
|
8977
|
+
"--timeout",
|
|
8978
|
+
"120"
|
|
8979
|
+
]);
|
|
8980
|
+
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
8981
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
8982
|
+
if (result.exitCode === 0) {
|
|
8983
|
+
if (result.stdout.trim()) {
|
|
8984
|
+
console.log(result.stdout.trim());
|
|
8985
|
+
}
|
|
8986
|
+
if (result.stderr.trim()) {
|
|
8987
|
+
console.error(result.stderr.trim());
|
|
8988
|
+
}
|
|
8989
|
+
return;
|
|
8990
|
+
}
|
|
8991
|
+
if (isTransientOvFailure(result.stderr, result.stdout) && await vikingResourceExists2(ov, config, uri) && !existedBeforeWrite) {
|
|
8992
|
+
console.log("OpenViking accepted the write but returned an error before the wait completed; draining the queue.");
|
|
8993
|
+
await waitForOvQueue(ov, config);
|
|
8994
|
+
return;
|
|
8995
|
+
}
|
|
8996
|
+
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
8997
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
8998
|
+
}
|
|
8999
|
+
await sleep(1e3 * (attempt + 1));
|
|
9000
|
+
}
|
|
9001
|
+
}
|
|
9002
|
+
async function waitForOvQueue(ov, config) {
|
|
9003
|
+
const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
9004
|
+
if (result.stdout.trim()) {
|
|
9005
|
+
console.log(result.stdout.trim());
|
|
9006
|
+
}
|
|
9007
|
+
if (result.stderr.trim()) {
|
|
9008
|
+
console.error(result.stderr.trim());
|
|
9009
|
+
}
|
|
9010
|
+
}
|
|
9011
|
+
function isTransientOvFailure(stderr, stdout) {
|
|
9012
|
+
const output2 = `${stderr}
|
|
9013
|
+
${stdout}`.toLowerCase();
|
|
9014
|
+
return output2.includes("resource is busy") || output2.includes("resource is being processed") || output2.includes("network error") || output2.includes("error sending request") || output2.includes("http request failed") || output2.includes("connection refused") || output2.includes("connection reset") || output2.includes("timed out");
|
|
9015
|
+
}
|
|
9016
|
+
async function ingestSingleFile(ov, config, uri, filePath, initialMode) {
|
|
9017
|
+
const content = await (0, import_promises6.readFile)(filePath, "utf8");
|
|
9018
|
+
await writeMemoryFile(config, ov, uri, content, initialMode, false);
|
|
9019
|
+
}
|
|
9020
|
+
async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
|
|
9021
|
+
try {
|
|
9022
|
+
await removeMemoryUri(config, ov, sourceUri, dryRun);
|
|
9023
|
+
} catch (sourceErr) {
|
|
9024
|
+
if (dryRun) {
|
|
9025
|
+
throw sourceErr;
|
|
9026
|
+
}
|
|
9027
|
+
console.error(
|
|
9028
|
+
`Source removal failed during ${label}; rolling back ${rollbackUri} so the system is back to the pre-${label} state.`
|
|
9029
|
+
);
|
|
9030
|
+
try {
|
|
9031
|
+
await removeMemoryUri(config, ov, rollbackUri, false);
|
|
9032
|
+
} catch (rollbackErr) {
|
|
9033
|
+
console.error(
|
|
9034
|
+
`Rollback of ${rollbackUri} also failed. Manual cleanup needed via: threadnote forget ${rollbackUri}
|
|
9035
|
+
Rollback error: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`
|
|
9036
|
+
);
|
|
9037
|
+
}
|
|
9038
|
+
await bestEffortRemoveWorktreeFile(rollbackUri, worktree, label);
|
|
9039
|
+
throw sourceErr;
|
|
9040
|
+
}
|
|
9041
|
+
}
|
|
9042
|
+
async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
9043
|
+
if (label !== "publish") {
|
|
9044
|
+
return;
|
|
9045
|
+
}
|
|
9046
|
+
const prefix = "viking://";
|
|
9047
|
+
if (!rollbackUri.startsWith(prefix)) {
|
|
9048
|
+
return;
|
|
9049
|
+
}
|
|
9050
|
+
const parts = rollbackUri.slice(prefix.length).split("/");
|
|
9051
|
+
const sharedIndex = parts.indexOf("shared");
|
|
9052
|
+
if (sharedIndex === -1 || sharedIndex + 2 >= parts.length) {
|
|
9053
|
+
return;
|
|
9054
|
+
}
|
|
9055
|
+
const relative3 = parts.slice(sharedIndex + 2).join("/");
|
|
9056
|
+
if (!relative3) {
|
|
9057
|
+
return;
|
|
9058
|
+
}
|
|
9059
|
+
await (0, import_promises6.rm)((0, import_node_path6.join)(worktree, relative3), { force: true });
|
|
9060
|
+
}
|
|
9061
|
+
async function removeMemoryUri(config, ov, uri, dryRun) {
|
|
9062
|
+
const args = withIdentity(config, ["rm", uri]);
|
|
9063
|
+
if (dryRun) {
|
|
9064
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
9065
|
+
return;
|
|
9066
|
+
}
|
|
9067
|
+
const maxAttempts = 4;
|
|
9068
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
9069
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
9070
|
+
if (result.exitCode === 0) {
|
|
9071
|
+
if (result.stdout.trim()) {
|
|
9072
|
+
console.log(result.stdout.trim());
|
|
9073
|
+
}
|
|
9074
|
+
return;
|
|
9075
|
+
}
|
|
9076
|
+
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
9077
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
9078
|
+
}
|
|
9079
|
+
await sleep(1e3 * (attempt + 1));
|
|
9080
|
+
}
|
|
9081
|
+
}
|
|
9082
|
+
async function vikingResourceExists2(ov, config, uri) {
|
|
9083
|
+
const result = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
9084
|
+
return result.exitCode === 0;
|
|
9085
|
+
}
|
|
9086
|
+
async function hasUncommittedChanges(worktree) {
|
|
9087
|
+
const result = await runCommand("git", ["-C", worktree, "status", "--porcelain"], { allowFailure: true });
|
|
9088
|
+
return result.stdout.trim().length > 0;
|
|
9089
|
+
}
|
|
9090
|
+
async function gitOutput(worktree, args, dryRun) {
|
|
9091
|
+
if (dryRun) {
|
|
9092
|
+
return void 0;
|
|
9093
|
+
}
|
|
9094
|
+
const result = await runCommand("git", ["-C", worktree, ...args], { allowFailure: true });
|
|
9095
|
+
if (result.exitCode !== 0) {
|
|
9096
|
+
return void 0;
|
|
9097
|
+
}
|
|
9098
|
+
return result.stdout.trim();
|
|
9099
|
+
}
|
|
9100
|
+
async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
9101
|
+
const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
|
|
9102
|
+
allowFailure: true
|
|
9103
|
+
});
|
|
9104
|
+
if (result.exitCode !== 0) {
|
|
9105
|
+
return [];
|
|
9106
|
+
}
|
|
9107
|
+
const entries = result.stdout.split("\0").filter((part) => part.length > 0);
|
|
9108
|
+
const changes = [];
|
|
9109
|
+
for (let index = 0; index < entries.length; ) {
|
|
9110
|
+
const raw = entries[index];
|
|
9111
|
+
const head = raw.slice(0, 1);
|
|
9112
|
+
if (head === "R" || head === "C") {
|
|
9113
|
+
const oldRel = entries[index + 1];
|
|
9114
|
+
const newRel = entries[index + 2];
|
|
9115
|
+
if (oldRel && newRel) {
|
|
9116
|
+
changes.push({ path: (0, import_node_path6.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
9117
|
+
changes.push({ path: (0, import_node_path6.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
9118
|
+
}
|
|
9119
|
+
index += 3;
|
|
9120
|
+
continue;
|
|
9121
|
+
}
|
|
9122
|
+
const rel = entries[index + 1];
|
|
9123
|
+
if (rel) {
|
|
9124
|
+
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
9125
|
+
changes.push({ path: (0, import_node_path6.join)(worktree, rel), relativePath: rel, status });
|
|
9126
|
+
}
|
|
9127
|
+
index += 2;
|
|
9128
|
+
}
|
|
9129
|
+
return changes;
|
|
9130
|
+
}
|
|
9131
|
+
async function applyChangesToOpenViking(config, team, changes) {
|
|
9132
|
+
const ov = await openVikingCliForMode(false);
|
|
9133
|
+
for (const change of changes) {
|
|
9134
|
+
if (!change.relativePath.endsWith(".md")) {
|
|
9135
|
+
continue;
|
|
9136
|
+
}
|
|
9137
|
+
const firstSegment = change.relativePath.split("/")[0];
|
|
9138
|
+
if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
|
|
9139
|
+
continue;
|
|
9140
|
+
}
|
|
9141
|
+
const uri = workfileToVikingUri(config, team, change.path);
|
|
9142
|
+
if (change.status === "removed") {
|
|
9143
|
+
await removeMemoryUri(config, ov, uri, false);
|
|
9144
|
+
continue;
|
|
9145
|
+
}
|
|
9146
|
+
if (!await isFile(change.path)) {
|
|
9147
|
+
continue;
|
|
9148
|
+
}
|
|
9149
|
+
if (change.status === "modified" && await vikingResourceExists2(ov, config, uri)) {
|
|
9150
|
+
console.warn(
|
|
9151
|
+
`share sync: overwriting local ${uri} with the upstream version (local edits to the shared subtree are not preserved across sync).`
|
|
9152
|
+
);
|
|
9153
|
+
}
|
|
9154
|
+
await ensureSharedDirectoryChain(config, ov, uri, false);
|
|
9155
|
+
const writeMode = change.status === "modified" ? "replace" : "create";
|
|
9156
|
+
await ingestSingleFile(ov, config, uri, change.path, writeMode);
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9159
|
+
|
|
8225
9160
|
// src/lifecycle.ts
|
|
8226
9161
|
var import_node_child_process2 = require("node:child_process");
|
|
9162
|
+
var import_node_fs5 = require("node:fs");
|
|
9163
|
+
var import_promises9 = require("node:fs/promises");
|
|
9164
|
+
var import_node_os6 = require("node:os");
|
|
9165
|
+
var import_node_path8 = require("node:path");
|
|
9166
|
+
|
|
9167
|
+
// src/update.ts
|
|
8227
9168
|
var import_node_fs4 = require("node:fs");
|
|
8228
|
-
var
|
|
9169
|
+
var import_promises7 = require("node:fs/promises");
|
|
8229
9170
|
var import_node_os5 = require("node:os");
|
|
8230
9171
|
var import_node_path7 = require("node:path");
|
|
8231
|
-
|
|
8232
|
-
// src/update.ts
|
|
8233
|
-
var import_node_fs3 = require("node:fs");
|
|
8234
|
-
var import_promises6 = require("node:fs/promises");
|
|
8235
|
-
var import_node_os4 = require("node:os");
|
|
8236
|
-
var import_node_path6 = require("node:path");
|
|
8237
|
-
var import_promises7 = require("node:readline/promises");
|
|
9172
|
+
var import_promises8 = require("node:readline/promises");
|
|
8238
9173
|
var import_node_process = require("node:process");
|
|
8239
9174
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
8240
9175
|
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
@@ -8321,7 +9256,9 @@ async function runUpdate(config, options) {
|
|
|
8321
9256
|
} else {
|
|
8322
9257
|
console.log("Skipping post-update migration prompts because --no-post-update was provided.");
|
|
8323
9258
|
}
|
|
8324
|
-
console.log(
|
|
9259
|
+
console.log(
|
|
9260
|
+
"Update complete. Restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload."
|
|
9261
|
+
);
|
|
8325
9262
|
}
|
|
8326
9263
|
async function runPostUpdate(config, options) {
|
|
8327
9264
|
if (!options.fromVersion || !options.toVersion) {
|
|
@@ -8380,7 +9317,7 @@ async function getUpdateInfo(config, options) {
|
|
|
8380
9317
|
};
|
|
8381
9318
|
}
|
|
8382
9319
|
async function currentPackageVersion() {
|
|
8383
|
-
const rawPackage = await (0,
|
|
9320
|
+
const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
|
|
8384
9321
|
const parsed = JSON.parse(rawPackage);
|
|
8385
9322
|
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8386
9323
|
throw new Error("Could not read current threadnote package version.");
|
|
@@ -8430,11 +9367,11 @@ async function readFreshCache(config, registry) {
|
|
|
8430
9367
|
}
|
|
8431
9368
|
async function writeUpdateCache(config, cache) {
|
|
8432
9369
|
await ensureDirectory(config.agentContextHome, false);
|
|
8433
|
-
await (0,
|
|
9370
|
+
await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
|
|
8434
9371
|
`, { encoding: "utf8", mode: 384 });
|
|
8435
9372
|
}
|
|
8436
9373
|
function updateCachePath(config) {
|
|
8437
|
-
return (0,
|
|
9374
|
+
return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
|
|
8438
9375
|
}
|
|
8439
9376
|
async function runApplicablePostUpdateMigrations(config, options) {
|
|
8440
9377
|
const state = await readPostUpdateState(config);
|
|
@@ -8498,7 +9435,7 @@ async function applicablePostUpdateMigrations(config, options) {
|
|
|
8498
9435
|
return applicable;
|
|
8499
9436
|
}
|
|
8500
9437
|
async function readPostUpdateMigrations() {
|
|
8501
|
-
const raw = await readFileIfExists((0,
|
|
9438
|
+
const raw = await readFileIfExists((0, import_node_path7.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
|
|
8502
9439
|
if (!raw) {
|
|
8503
9440
|
return [];
|
|
8504
9441
|
}
|
|
@@ -8537,7 +9474,7 @@ function printPostUpdateMigration(migration) {
|
|
|
8537
9474
|
}
|
|
8538
9475
|
}
|
|
8539
9476
|
async function confirmPostUpdateMigration(prompt) {
|
|
8540
|
-
const readline = (0,
|
|
9477
|
+
const readline = (0, import_promises8.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
8541
9478
|
try {
|
|
8542
9479
|
const answer = (await readline.question(prompt)).trim().toLowerCase();
|
|
8543
9480
|
return answer === "y" || answer === "yes";
|
|
@@ -8569,11 +9506,11 @@ async function readPostUpdateState(config) {
|
|
|
8569
9506
|
}
|
|
8570
9507
|
async function writePostUpdateState(config, state) {
|
|
8571
9508
|
await ensureDirectory(config.agentContextHome, false);
|
|
8572
|
-
await (0,
|
|
9509
|
+
await (0, import_promises7.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
|
|
8573
9510
|
`, { encoding: "utf8", mode: 384 });
|
|
8574
9511
|
}
|
|
8575
9512
|
function postUpdateStatePath(config) {
|
|
8576
|
-
return (0,
|
|
9513
|
+
return (0, import_node_path7.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
|
|
8577
9514
|
}
|
|
8578
9515
|
async function resolveUpdateRuntime(runtime) {
|
|
8579
9516
|
if (runtime !== "auto") {
|
|
@@ -8603,18 +9540,18 @@ async function runtimeThreadnoteBin(runtime) {
|
|
|
8603
9540
|
if (runtime === "npm") {
|
|
8604
9541
|
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
8605
9542
|
const prefix = result.stdout.trim();
|
|
8606
|
-
return prefix ? (0,
|
|
9543
|
+
return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
8607
9544
|
}
|
|
8608
9545
|
if (runtime === "bun") {
|
|
8609
9546
|
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
8610
9547
|
const binDir = result.stdout.trim();
|
|
8611
|
-
return binDir ? (0,
|
|
9548
|
+
return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
8612
9549
|
}
|
|
8613
|
-
return (0,
|
|
9550
|
+
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);
|
|
8614
9551
|
}
|
|
8615
9552
|
async function isExecutable(path) {
|
|
8616
9553
|
try {
|
|
8617
|
-
await (0,
|
|
9554
|
+
await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
|
|
8618
9555
|
return true;
|
|
8619
9556
|
} catch (_err) {
|
|
8620
9557
|
return false;
|
|
@@ -8692,7 +9629,7 @@ function safeVersionNumber(value) {
|
|
|
8692
9629
|
async function runDoctor(config, options) {
|
|
8693
9630
|
const checks = [];
|
|
8694
9631
|
checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
|
|
8695
|
-
checks.push({ name: "platform", status: (0,
|
|
9632
|
+
checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
|
|
8696
9633
|
checks.push(await commandCheck("node", ["--version"]));
|
|
8697
9634
|
checks.push(await commandCheck("python3", ["--version"]));
|
|
8698
9635
|
checks.push(await commandCheck("openviking-server", ["--help"]));
|
|
@@ -8705,9 +9642,9 @@ async function runDoctor(config, options) {
|
|
|
8705
9642
|
checks.push(await commandShimCheck());
|
|
8706
9643
|
checks.push(...await userAgentInstructionsChecks());
|
|
8707
9644
|
checks.push(await manifestCheck(config.manifestPath));
|
|
8708
|
-
checks.push(await fileCheck((0,
|
|
8709
|
-
checks.push(await fileCheck((0,
|
|
8710
|
-
checks.push(await fileCheck((0,
|
|
9645
|
+
checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
|
|
9646
|
+
checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
|
|
9647
|
+
checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
|
|
8711
9648
|
checks.push(await healthCheck(config));
|
|
8712
9649
|
for (const check of checks) {
|
|
8713
9650
|
console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
|
|
@@ -8724,9 +9661,9 @@ async function runInstall(config, options) {
|
|
|
8724
9661
|
const repairInvalidConfigs = options.repairInvalidConfigs === true;
|
|
8725
9662
|
const dryRun = options.dryRun === true;
|
|
8726
9663
|
await ensureDirectory(config.agentContextHome, dryRun);
|
|
8727
|
-
await ensureDirectory((0,
|
|
8728
|
-
await ensureDirectory((0,
|
|
8729
|
-
await ensureDirectory((0,
|
|
9664
|
+
await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "logs"), dryRun);
|
|
9665
|
+
await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "redacted"), dryRun);
|
|
9666
|
+
await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "mcp"), dryRun);
|
|
8730
9667
|
await installCommandShim(dryRun);
|
|
8731
9668
|
await installUserAgentInstructions(dryRun);
|
|
8732
9669
|
const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
@@ -8752,17 +9689,17 @@ async function runInstall(config, options) {
|
|
|
8752
9689
|
}
|
|
8753
9690
|
await writeTemplateIfMissing({
|
|
8754
9691
|
config,
|
|
8755
|
-
destinationPath: (0,
|
|
9692
|
+
destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ov.conf"),
|
|
8756
9693
|
dryRun,
|
|
8757
9694
|
shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
8758
|
-
templatePath: (0,
|
|
9695
|
+
templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json")
|
|
8759
9696
|
});
|
|
8760
9697
|
await writeTemplateIfMissing({
|
|
8761
9698
|
config,
|
|
8762
|
-
destinationPath: (0,
|
|
9699
|
+
destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ovcli.conf"),
|
|
8763
9700
|
dryRun,
|
|
8764
9701
|
shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
8765
|
-
templatePath: (0,
|
|
9702
|
+
templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json")
|
|
8766
9703
|
});
|
|
8767
9704
|
if (options.start !== false) {
|
|
8768
9705
|
const healthy = await repairServerHealth(config, dryRun);
|
|
@@ -8812,7 +9749,7 @@ async function runUninstall(config, options) {
|
|
|
8812
9749
|
}
|
|
8813
9750
|
console.log("Uninstalling local Threadnote setup.");
|
|
8814
9751
|
await runStop(config, { dryRun });
|
|
8815
|
-
await removePathIfExists((0,
|
|
9752
|
+
await removePathIfExists((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
|
|
8816
9753
|
await removeLaunchAgent(dryRun);
|
|
8817
9754
|
await removeMcpConfigs(options.mcp ?? "available", dryRun);
|
|
8818
9755
|
await removeMcpSnippets(config, dryRun);
|
|
@@ -8866,16 +9803,16 @@ async function repairManifest(config, dryRun) {
|
|
|
8866
9803
|
console.log(output2.trimEnd());
|
|
8867
9804
|
return;
|
|
8868
9805
|
}
|
|
8869
|
-
await ensureDirectory((0,
|
|
9806
|
+
await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
|
|
8870
9807
|
const currentContent = await readFileIfExists(config.manifestPath);
|
|
8871
9808
|
if (currentContent !== void 0) {
|
|
8872
9809
|
const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
|
|
8873
|
-
await (0,
|
|
8874
|
-
await (0,
|
|
9810
|
+
await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
9811
|
+
await (0, import_promises9.chmod)(backupPath, 384);
|
|
8875
9812
|
console.log(`Backup: ${backupPath}`);
|
|
8876
9813
|
}
|
|
8877
|
-
await (0,
|
|
8878
|
-
await (0,
|
|
9814
|
+
await (0, import_promises9.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
9815
|
+
await (0, import_promises9.chmod)(config.manifestPath, 384);
|
|
8879
9816
|
console.log(`Wrote replacement manifest: ${config.manifestPath}`);
|
|
8880
9817
|
}
|
|
8881
9818
|
async function repairServerHealth(config, dryRun) {
|
|
@@ -8916,16 +9853,16 @@ async function runStart(config, options) {
|
|
|
8916
9853
|
);
|
|
8917
9854
|
}
|
|
8918
9855
|
const logPath = openVikingLogPath(config);
|
|
8919
|
-
await ensureDirectory((0,
|
|
9856
|
+
await ensureDirectory((0, import_node_path8.dirname)(logPath), false);
|
|
8920
9857
|
if (options.foreground === true) {
|
|
8921
9858
|
const result = await runInteractive(server, args);
|
|
8922
9859
|
process.exitCode = result;
|
|
8923
9860
|
return;
|
|
8924
9861
|
}
|
|
8925
|
-
const logFd = (0,
|
|
9862
|
+
const logFd = (0, import_node_fs5.openSync)(logPath, "a");
|
|
8926
9863
|
const child = spawnDetachedServerWithLog(server, args, logFd);
|
|
8927
9864
|
child.unref();
|
|
8928
|
-
await (0,
|
|
9865
|
+
await (0, import_promises9.writeFile)((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
8929
9866
|
`, "utf8");
|
|
8930
9867
|
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
8931
9868
|
if (health) {
|
|
@@ -8940,7 +9877,7 @@ function spawnDetachedServerWithLog(server, args, logFd) {
|
|
|
8940
9877
|
try {
|
|
8941
9878
|
return spawnDetachedServer(server, args, logFd);
|
|
8942
9879
|
} finally {
|
|
8943
|
-
(0,
|
|
9880
|
+
(0, import_node_fs5.closeSync)(logFd);
|
|
8944
9881
|
}
|
|
8945
9882
|
}
|
|
8946
9883
|
function spawnDetachedServer(server, args, logFd) {
|
|
@@ -8951,14 +9888,14 @@ function spawnDetachedServer(server, args, logFd) {
|
|
|
8951
9888
|
}
|
|
8952
9889
|
async function runStop(config, options) {
|
|
8953
9890
|
const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
8954
|
-
if ((0,
|
|
9891
|
+
if ((0, import_node_os6.platform)() === "darwin") {
|
|
8955
9892
|
if (options.dryRun === true || await exists(launchAgentPath)) {
|
|
8956
9893
|
await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
|
|
8957
9894
|
} else {
|
|
8958
9895
|
console.log(`No LaunchAgent found: ${launchAgentPath}`);
|
|
8959
9896
|
}
|
|
8960
9897
|
}
|
|
8961
|
-
const pidPath = (0,
|
|
9898
|
+
const pidPath = (0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid");
|
|
8962
9899
|
const pidText = await readFileIfExists(pidPath);
|
|
8963
9900
|
if (!pidText) {
|
|
8964
9901
|
console.log("No pid file found for detached OpenViking server.");
|
|
@@ -9054,7 +9991,7 @@ async function pythonSystemCertificatesCheck() {
|
|
|
9054
9991
|
};
|
|
9055
9992
|
}
|
|
9056
9993
|
async function commandShimCheck() {
|
|
9057
|
-
const shimPath = (0,
|
|
9994
|
+
const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
9058
9995
|
const content = await readFileIfExists(shimPath);
|
|
9059
9996
|
if (content === void 0) {
|
|
9060
9997
|
return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
|
|
@@ -9076,9 +10013,9 @@ async function commandShimCheck() {
|
|
|
9076
10013
|
return { name: "threadnote shim", status: "ok", detail: shimPath };
|
|
9077
10014
|
}
|
|
9078
10015
|
async function userAgentInstructionsChecks() {
|
|
9079
|
-
const expectedBlock = await renderUserAgentInstructionsBlock();
|
|
9080
10016
|
return Promise.all(
|
|
9081
10017
|
USER_AGENT_INSTRUCTION_TARGETS.map(async (target) => {
|
|
10018
|
+
const expectedInstructions = await renderUserAgentInstructions(target);
|
|
9082
10019
|
const targetPath = expandPath(target.path);
|
|
9083
10020
|
const content = await readFileIfExists(targetPath);
|
|
9084
10021
|
if (content === void 0) {
|
|
@@ -9092,7 +10029,7 @@ async function userAgentInstructionsChecks() {
|
|
|
9092
10029
|
detail: `${targetPath} missing threadnote block; install will add it`
|
|
9093
10030
|
};
|
|
9094
10031
|
}
|
|
9095
|
-
if (existingBlock !==
|
|
10032
|
+
if (target.kind === "file" && content !== expectedInstructions || target.kind === "block" && existingBlock !== expectedInstructions) {
|
|
9096
10033
|
return {
|
|
9097
10034
|
name: target.label,
|
|
9098
10035
|
status: "warn",
|
|
@@ -9120,11 +10057,11 @@ async function hasPythonModule(serverPath, moduleName) {
|
|
|
9120
10057
|
async function siblingPythonForExecutable(executablePath) {
|
|
9121
10058
|
let resolvedPath;
|
|
9122
10059
|
try {
|
|
9123
|
-
resolvedPath = await (0,
|
|
10060
|
+
resolvedPath = await (0, import_promises9.realpath)(executablePath);
|
|
9124
10061
|
} catch (_err) {
|
|
9125
10062
|
return void 0;
|
|
9126
10063
|
}
|
|
9127
|
-
const pythonPath = (0,
|
|
10064
|
+
const pythonPath = (0, import_node_path8.join)((0, import_node_path8.dirname)(resolvedPath), "python");
|
|
9128
10065
|
return await exists(pythonPath) ? pythonPath : void 0;
|
|
9129
10066
|
}
|
|
9130
10067
|
async function manifestCheck(path) {
|
|
@@ -9246,38 +10183,38 @@ function printInstallNextSteps(options) {
|
|
|
9246
10183
|
}
|
|
9247
10184
|
async function writeTemplateIfMissing(options) {
|
|
9248
10185
|
if (await exists(options.destinationPath)) {
|
|
9249
|
-
const currentContent = await (0,
|
|
10186
|
+
const currentContent = await (0, import_promises9.readFile)(options.destinationPath, "utf8");
|
|
9250
10187
|
if (options.shouldRepair?.(currentContent) !== true) {
|
|
9251
10188
|
console.log(`Already exists: ${options.destinationPath}`);
|
|
9252
10189
|
return;
|
|
9253
10190
|
}
|
|
9254
|
-
const rendered2 = renderTemplate(await (0,
|
|
10191
|
+
const rendered2 = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
|
|
9255
10192
|
if (options.dryRun) {
|
|
9256
10193
|
console.log(`Would repair generated config: ${options.destinationPath}`);
|
|
9257
10194
|
return;
|
|
9258
10195
|
}
|
|
9259
10196
|
const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
|
|
9260
|
-
await (0,
|
|
9261
|
-
await (0,
|
|
9262
|
-
await (0,
|
|
9263
|
-
await (0,
|
|
10197
|
+
await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
10198
|
+
await (0, import_promises9.chmod)(backupPath, 384);
|
|
10199
|
+
await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
|
|
10200
|
+
await (0, import_promises9.chmod)(options.destinationPath, 384);
|
|
9264
10201
|
console.log(`Repaired generated config: ${options.destinationPath}`);
|
|
9265
10202
|
console.log(`Backup: ${backupPath}`);
|
|
9266
10203
|
return;
|
|
9267
10204
|
}
|
|
9268
|
-
const rendered = renderTemplate(await (0,
|
|
10205
|
+
const rendered = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
|
|
9269
10206
|
if (options.dryRun) {
|
|
9270
10207
|
console.log(`Would write ${options.destinationPath}`);
|
|
9271
10208
|
return;
|
|
9272
10209
|
}
|
|
9273
|
-
await ensureDirectory((0,
|
|
9274
|
-
await (0,
|
|
9275
|
-
await (0,
|
|
10210
|
+
await ensureDirectory((0, import_node_path8.dirname)(options.destinationPath), false);
|
|
10211
|
+
await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
|
|
10212
|
+
await (0, import_promises9.chmod)(options.destinationPath, 384);
|
|
9276
10213
|
console.log(`Wrote ${options.destinationPath}`);
|
|
9277
10214
|
}
|
|
9278
10215
|
async function installCommandShim(dryRun) {
|
|
9279
10216
|
const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
|
|
9280
|
-
const shimPath = (0,
|
|
10217
|
+
const shimPath = (0, import_node_path8.join)(binDir, "threadnote");
|
|
9281
10218
|
const existingContent = await readFileIfExists(shimPath);
|
|
9282
10219
|
if (existingContent && !isManagedCommandShim(existingContent)) {
|
|
9283
10220
|
console.log(`WARN not overwriting existing command shim: ${shimPath}`);
|
|
@@ -9293,12 +10230,12 @@ async function installCommandShim(dryRun) {
|
|
|
9293
10230
|
return;
|
|
9294
10231
|
}
|
|
9295
10232
|
await ensureDirectory(binDir, false);
|
|
9296
|
-
await (0,
|
|
9297
|
-
await (0,
|
|
10233
|
+
await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
|
|
10234
|
+
await (0, import_promises9.chmod)(shimPath, 493);
|
|
9298
10235
|
console.log(`Wrote command shim: ${shimPath}`);
|
|
9299
10236
|
}
|
|
9300
10237
|
async function removeCommandShim(dryRun) {
|
|
9301
|
-
const shimPath = (0,
|
|
10238
|
+
const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
9302
10239
|
const content = await readFileIfExists(shimPath);
|
|
9303
10240
|
if (content === void 0) {
|
|
9304
10241
|
console.log(`Already absent: ${shimPath}`);
|
|
@@ -9311,11 +10248,15 @@ async function removeCommandShim(dryRun) {
|
|
|
9311
10248
|
await removePath(shimPath, "command shim", dryRun);
|
|
9312
10249
|
}
|
|
9313
10250
|
async function installUserAgentInstructions(dryRun) {
|
|
9314
|
-
const block = await renderUserAgentInstructionsBlock();
|
|
9315
10251
|
for (const target of USER_AGENT_INSTRUCTION_TARGETS) {
|
|
10252
|
+
const instructions = await renderUserAgentInstructions(target);
|
|
9316
10253
|
const targetPath = expandPath(target.path);
|
|
9317
10254
|
const currentContent = await readFileIfExists(targetPath);
|
|
9318
|
-
|
|
10255
|
+
if (target.kind === "file" && currentContent !== void 0 && extractManagedBlock(currentContent) === void 0) {
|
|
10256
|
+
console.log(`WARN ${targetPath} is not managed by threadnote; not modifying it`);
|
|
10257
|
+
continue;
|
|
10258
|
+
}
|
|
10259
|
+
const nextContent = target.kind === "file" ? instructions : upsertManagedBlock(currentContent ?? "", instructions);
|
|
9319
10260
|
if (nextContent === void 0) {
|
|
9320
10261
|
console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
|
|
9321
10262
|
continue;
|
|
@@ -9328,8 +10269,8 @@ async function installUserAgentInstructions(dryRun) {
|
|
|
9328
10269
|
console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
|
|
9329
10270
|
continue;
|
|
9330
10271
|
}
|
|
9331
|
-
await ensureDirectory((0,
|
|
9332
|
-
await (0,
|
|
10272
|
+
await ensureDirectory((0, import_node_path8.dirname)(targetPath), false);
|
|
10273
|
+
await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
9333
10274
|
console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
|
|
9334
10275
|
}
|
|
9335
10276
|
}
|
|
@@ -9341,6 +10282,14 @@ async function removeUserAgentInstructions(dryRun) {
|
|
|
9341
10282
|
console.log(`Already absent: ${targetPath}`);
|
|
9342
10283
|
continue;
|
|
9343
10284
|
}
|
|
10285
|
+
if (target.kind === "file") {
|
|
10286
|
+
if (extractManagedBlock(currentContent) === void 0) {
|
|
10287
|
+
console.log(`WARN ${targetPath} is not managed by threadnote; not removing it`);
|
|
10288
|
+
continue;
|
|
10289
|
+
}
|
|
10290
|
+
await removePath(targetPath, target.label, dryRun);
|
|
10291
|
+
continue;
|
|
10292
|
+
}
|
|
9344
10293
|
const nextContent = removeManagedBlock(currentContent);
|
|
9345
10294
|
if (nextContent === void 0) {
|
|
9346
10295
|
console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
|
|
@@ -9358,12 +10307,27 @@ async function removeUserAgentInstructions(dryRun) {
|
|
|
9358
10307
|
console.log(`Would update ${targetPath}`);
|
|
9359
10308
|
continue;
|
|
9360
10309
|
}
|
|
9361
|
-
await (0,
|
|
10310
|
+
await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
9362
10311
|
console.log(`Updated ${targetPath}`);
|
|
9363
10312
|
}
|
|
9364
10313
|
}
|
|
10314
|
+
async function renderUserAgentInstructions(target) {
|
|
10315
|
+
const block = await renderUserAgentInstructionsBlock();
|
|
10316
|
+
if (target.kind === "block") {
|
|
10317
|
+
return block;
|
|
10318
|
+
}
|
|
10319
|
+
return [
|
|
10320
|
+
"---",
|
|
10321
|
+
"name: Threadnote",
|
|
10322
|
+
"description: Shared local context and handoffs through Threadnote",
|
|
10323
|
+
'applyTo: "**"',
|
|
10324
|
+
"---",
|
|
10325
|
+
"",
|
|
10326
|
+
block
|
|
10327
|
+
].join("\n");
|
|
10328
|
+
}
|
|
9365
10329
|
async function renderUserAgentInstructionsBlock() {
|
|
9366
|
-
const instructions = (await (0,
|
|
10330
|
+
const instructions = (await (0, import_promises9.readFile)((0, import_node_path8.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
|
|
9367
10331
|
return `${USER_INSTRUCTIONS_START_MARKER}
|
|
9368
10332
|
${instructions}
|
|
9369
10333
|
${USER_INSTRUCTIONS_END_MARKER}`;
|
|
@@ -9438,12 +10402,12 @@ function isManagedCommandShim(content) {
|
|
|
9438
10402
|
return content.includes(SHIM_MARKER);
|
|
9439
10403
|
}
|
|
9440
10404
|
async function installLaunchAgent(config, dryRun) {
|
|
9441
|
-
if ((0,
|
|
10405
|
+
if ((0, import_node_os6.platform)() !== "darwin") {
|
|
9442
10406
|
throw new Error("launchd autostart is only supported on macOS.");
|
|
9443
10407
|
}
|
|
9444
|
-
const source = (0,
|
|
10408
|
+
const source = (0, import_node_path8.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
|
|
9445
10409
|
const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
9446
|
-
const rendered = renderTemplate(await (0,
|
|
10410
|
+
const rendered = renderTemplate(await (0, import_promises9.readFile)(source, "utf8"), config);
|
|
9447
10411
|
if (dryRun) {
|
|
9448
10412
|
console.log(`Would write ${destination}`);
|
|
9449
10413
|
console.log(`Would run: launchctl unload ${destination}`);
|
|
@@ -9451,9 +10415,9 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
9451
10415
|
console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
|
|
9452
10416
|
return;
|
|
9453
10417
|
}
|
|
9454
|
-
await ensureDirectory((0,
|
|
9455
|
-
await ensureDirectory((0,
|
|
9456
|
-
await (0,
|
|
10418
|
+
await ensureDirectory((0, import_node_path8.dirname)(destination), false);
|
|
10419
|
+
await ensureDirectory((0, import_node_path8.dirname)(openVikingLogPath(config)), false);
|
|
10420
|
+
await (0, import_promises9.writeFile)(destination, rendered, "utf8");
|
|
9457
10421
|
await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
|
|
9458
10422
|
await maybeRun(false, "launchctl", ["load", destination]);
|
|
9459
10423
|
await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
|
|
@@ -9516,7 +10480,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
|
|
|
9516
10480
|
if (typeof parsed.default_user !== "string") {
|
|
9517
10481
|
return false;
|
|
9518
10482
|
}
|
|
9519
|
-
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0,
|
|
10483
|
+
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path8.join)(config.agentContextHome, "data")) {
|
|
9520
10484
|
return false;
|
|
9521
10485
|
}
|
|
9522
10486
|
return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
|
|
@@ -9557,7 +10521,7 @@ async function main() {
|
|
|
9557
10521
|
});
|
|
9558
10522
|
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(
|
|
9559
10523
|
"--mcp <clients>",
|
|
9560
|
-
"MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
|
|
10524
|
+
"MCP clients to repair: available, all, none, codex, claude, cursor, copilot, or comma-separated list",
|
|
9561
10525
|
"available"
|
|
9562
10526
|
).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) => {
|
|
9563
10527
|
const config = getRuntimeConfig(program2);
|
|
@@ -9574,7 +10538,7 @@ async function main() {
|
|
|
9574
10538
|
});
|
|
9575
10539
|
program2.command("uninstall").description("Remove Threadnote setup and optionally erase local memories").option("--dry-run", "Print uninstall actions without making changes").option(
|
|
9576
10540
|
"--mcp <clients>",
|
|
9577
|
-
"MCP clients to remove: available, all, none, codex, claude, cursor, or comma-separated list",
|
|
10541
|
+
"MCP clients to remove: available, all, none, codex, claude, cursor, copilot, or comma-separated list",
|
|
9578
10542
|
"available"
|
|
9579
10543
|
).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
|
|
9580
10544
|
await runUninstall(getRuntimeConfig(program2), options);
|
|
@@ -9588,10 +10552,10 @@ async function main() {
|
|
|
9588
10552
|
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) => {
|
|
9589
10553
|
await runSeedSkills(getRuntimeConfig(program2, options.manifest), options);
|
|
9590
10554
|
});
|
|
9591
|
-
program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, or
|
|
10555
|
+
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) => {
|
|
9592
10556
|
await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
|
|
9593
10557
|
});
|
|
9594
|
-
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,
|
|
10558
|
+
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) => {
|
|
9595
10559
|
await runRemember(getRuntimeConfig(program2), options);
|
|
9596
10560
|
});
|
|
9597
10561
|
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(
|
|
@@ -9614,7 +10578,7 @@ async function main() {
|
|
|
9614
10578
|
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) => {
|
|
9615
10579
|
await runList(getRuntimeConfig(program2), uri, options);
|
|
9616
10580
|
});
|
|
9617
|
-
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,
|
|
10581
|
+
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) => {
|
|
9618
10582
|
await runHandoff(getRuntimeConfig(program2), options);
|
|
9619
10583
|
});
|
|
9620
10584
|
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) => {
|
|
@@ -9623,6 +10587,31 @@ async function main() {
|
|
|
9623
10587
|
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) => {
|
|
9624
10588
|
await runForget(getRuntimeConfig(program2), uri, options);
|
|
9625
10589
|
});
|
|
10590
|
+
const share = program2.command("share").description("Share durable memories with teammates through a git-backed repository");
|
|
10591
|
+
share.command("init").description("Configure a shared memories repo for a team (clones the remote into the local memory tree)").argument("<remote-url>", "git remote URL of the shared memories repo").option("--team <name>", 'Team name; defaults to "default"').option("--set-default", "Mark this team as the default for future share commands").option(
|
|
10592
|
+
"--no-push",
|
|
10593
|
+
"Do not push the auto-generated .gitignore housekeeping commit. Does not affect future publishes."
|
|
10594
|
+
).option("--dry-run", "Print actions without running them").action(async (remoteUrl, options) => {
|
|
10595
|
+
await runShareInit(getRuntimeConfig(program2), remoteUrl, options);
|
|
10596
|
+
});
|
|
10597
|
+
share.command("status").description("Show git status and ahead/behind counts for a shared team").option("--team <name>", "Team name; defaults to the configured default team").option("--dry-run", "Print git commands without running them").action(async (options) => {
|
|
10598
|
+
await runShareStatus(getRuntimeConfig(program2), options);
|
|
10599
|
+
});
|
|
10600
|
+
share.command("sync").description("Pull, reindex, and push the shared memories repo for a team").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message when auto-committing local edits").option("--no-auto-commit", "Refuse to sync if there are uncommitted local changes").option("--no-push", "Skip the push step after pulling and reindexing").option("--dry-run", "Print actions without running them").action(async (options) => {
|
|
10601
|
+
await runShareSync(getRuntimeConfig(program2), options);
|
|
10602
|
+
});
|
|
10603
|
+
share.command("publish").description("Move a personal memory into the shared team namespace, commit and push").argument("<viking-uri>", "viking:// memory URI to publish").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").action(async (uri, options) => {
|
|
10604
|
+
await runSharePublish(getRuntimeConfig(program2), uri, options);
|
|
10605
|
+
});
|
|
10606
|
+
share.command("unpublish").description("Pull a shared memory back into the personal namespace, commit removal and push").argument("<viking-uri>", "viking:// memory URI inside a team shared subtree").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").action(async (uri, options) => {
|
|
10607
|
+
await runShareUnpublish(getRuntimeConfig(program2), uri, options);
|
|
10608
|
+
});
|
|
10609
|
+
share.command("list").description("List configured shared teams").option("--dry-run", "Print without side effects (no-op for list)").action(async (options) => {
|
|
10610
|
+
await runShareList(getRuntimeConfig(program2), options);
|
|
10611
|
+
});
|
|
10612
|
+
share.command("remove").description("Forget a configured team and optionally delete its worktree/gitdir").option("--team <name>", "Team name; defaults to the configured default team").option("--keep-files", "Keep the worktree and gitdir on disk; only forget the team entry").option("--dry-run", "Print actions without running them").action(async (options) => {
|
|
10613
|
+
await runShareRemove(getRuntimeConfig(program2), options);
|
|
10614
|
+
});
|
|
9626
10615
|
program2.command("export-pack").description("Export local OpenViking context to an .ovpack").option("--dry-run", "Print ov command without exporting").option("--path <path>", "Output .ovpack path").option("--uri <uri>", "Source viking:// URI to export", "viking://").action(async (options) => {
|
|
9627
10616
|
await runExportPack(getRuntimeConfig(program2), options);
|
|
9628
10617
|
});
|