threadnote 0.4.1 → 0.6.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 +39 -0
- package/dist/mcp_server.cjs +103 -31
- package/dist/threadnote.cjs +522 -193
- package/docs/.nojekyll +0 -0
- package/docs/index.html +1543 -0
- package/docs/share.md +26 -5
- package/docs/threadnote-logo-inverted.svg +1 -0
- package/docs/threadnote-logo.svg +1 -0
- package/package.json +1 -1
package/dist/threadnote.cjs
CHANGED
|
@@ -3498,6 +3498,12 @@ var USER_AGENT_INSTRUCTION_TARGETS = [
|
|
|
3498
3498
|
path: "~/.copilot/instructions/threadnote.instructions.md"
|
|
3499
3499
|
}
|
|
3500
3500
|
];
|
|
3501
|
+
var CLAUDE_SETTINGS_PATH = "~/.claude/settings.json";
|
|
3502
|
+
var THREADNOTE_HOOK_MARKER = "_threadnote";
|
|
3503
|
+
var THREADNOTE_HOOK_MARKER_VALUE = "managed";
|
|
3504
|
+
var HOOK_PRE_COMPACT_COMMAND = "threadnote pre-compact-hook";
|
|
3505
|
+
var HOOK_SESSION_START_COMMAND = "threadnote session-start-hook";
|
|
3506
|
+
var HOOK_AUTO_PRECOMPACT_TOPIC = "auto-precompact";
|
|
3501
3507
|
var DEFAULT_SEED_PATTERNS = [
|
|
3502
3508
|
"AGENTS.md",
|
|
3503
3509
|
"CLAUDE.md",
|
|
@@ -3515,6 +3521,16 @@ var DEFAULT_SEED_PATTERNS = [
|
|
|
3515
3521
|
"docs/**/*.md"
|
|
3516
3522
|
];
|
|
3517
3523
|
|
|
3524
|
+
// src/hooks.ts
|
|
3525
|
+
var import_promises5 = require("node:fs/promises");
|
|
3526
|
+
var import_node_path5 = require("node:path");
|
|
3527
|
+
|
|
3528
|
+
// src/mcp.ts
|
|
3529
|
+
var import_node_fs2 = require("node:fs");
|
|
3530
|
+
var import_promises2 = require("node:fs/promises");
|
|
3531
|
+
var import_node_path2 = require("node:path");
|
|
3532
|
+
var import_node_os2 = require("node:os");
|
|
3533
|
+
|
|
3518
3534
|
// src/utils.ts
|
|
3519
3535
|
var import_node_child_process = require("node:child_process");
|
|
3520
3536
|
var import_node_crypto = require("node:crypto");
|
|
@@ -3986,10 +4002,6 @@ function errorMessage(err) {
|
|
|
3986
4002
|
}
|
|
3987
4003
|
|
|
3988
4004
|
// src/mcp.ts
|
|
3989
|
-
var import_node_fs2 = require("node:fs");
|
|
3990
|
-
var import_promises2 = require("node:fs/promises");
|
|
3991
|
-
var import_node_path2 = require("node:path");
|
|
3992
|
-
var import_node_os2 = require("node:os");
|
|
3993
4005
|
async function runMcpInstall(config, agent, options) {
|
|
3994
4006
|
const name = options.name ?? OPENVIKING_MCP_NAME;
|
|
3995
4007
|
const url = options.url ?? `http://${config.host}:${config.port}/mcp`;
|
|
@@ -4470,50 +4482,6 @@ function existsSyncDirectory(path) {
|
|
|
4470
4482
|
}
|
|
4471
4483
|
}
|
|
4472
4484
|
|
|
4473
|
-
// src/runtime.ts
|
|
4474
|
-
var import_node_fs3 = require("node:fs");
|
|
4475
|
-
var import_node_os3 = require("node:os");
|
|
4476
|
-
var import_node_path3 = require("node:path");
|
|
4477
|
-
function getRuntimeConfig(program2, manifestOverride) {
|
|
4478
|
-
const options = program2.opts();
|
|
4479
|
-
const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
|
|
4480
|
-
const manifestPath = expandPath(
|
|
4481
|
-
manifestOverride ?? options.manifest ?? process.env.THREADNOTE_MANIFEST ?? defaultManifestPath(threadnoteHome)
|
|
4482
|
-
);
|
|
4483
|
-
return {
|
|
4484
|
-
account: process.env.THREADNOTE_ACCOUNT ?? DEFAULT_ACCOUNT,
|
|
4485
|
-
agentContextHome: threadnoteHome,
|
|
4486
|
-
agentId: process.env.THREADNOTE_AGENT_ID ?? DEFAULT_AGENT_ID,
|
|
4487
|
-
host: options.host ?? process.env.THREADNOTE_HOST ?? DEFAULT_HOST,
|
|
4488
|
-
manifestPath,
|
|
4489
|
-
openVikingVersion: process.env.THREADNOTE_OPENVIKING_VERSION ?? DEFAULT_OPENVIKING_VERSION,
|
|
4490
|
-
port: options.port ?? parsePort(process.env.THREADNOTE_PORT ?? String(DEFAULT_PORT)),
|
|
4491
|
-
user: process.env.THREADNOTE_USER ?? (0, import_node_os3.userInfo)().username
|
|
4492
|
-
};
|
|
4493
|
-
}
|
|
4494
|
-
function defaultManifestPath(agentContextHome) {
|
|
4495
|
-
const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
|
|
4496
|
-
return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
|
|
4497
|
-
}
|
|
4498
|
-
function builtInExampleManifestPath() {
|
|
4499
|
-
return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
|
|
4500
|
-
}
|
|
4501
|
-
function openVikingHealthUrl(config) {
|
|
4502
|
-
return `http://${config.host}:${config.port}/health`;
|
|
4503
|
-
}
|
|
4504
|
-
function openVikingLogPath(config) {
|
|
4505
|
-
return (0, import_node_path3.join)(config.agentContextHome, "logs", "server.log");
|
|
4506
|
-
}
|
|
4507
|
-
function openVikingServerArgs(config) {
|
|
4508
|
-
return ["--config", (0, import_node_path3.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
|
|
4509
|
-
}
|
|
4510
|
-
function withIdentity(config, args) {
|
|
4511
|
-
return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
|
|
4512
|
-
}
|
|
4513
|
-
function renderTemplate(template, config) {
|
|
4514
|
-
return template.replaceAll("{{THREADNOTE_HOME}}", config.agentContextHome).replaceAll("{{OPENVIKING_ACCOUNT}}", config.account).replaceAll("{{OPENVIKING_AGENT_ID}}", config.agentId).replaceAll("{{OPENVIKING_HOST}}", config.host).replaceAll("{{OPENVIKING_PORT}}", String(config.port)).replaceAll("{{OPENVIKING_USER}}", config.user);
|
|
4515
|
-
}
|
|
4516
|
-
|
|
4517
4485
|
// src/memory.ts
|
|
4518
4486
|
var import_promises4 = require("node:fs/promises");
|
|
4519
4487
|
var import_node_path4 = require("node:path");
|
|
@@ -7205,6 +7173,50 @@ function readStringArray(object, key) {
|
|
|
7205
7173
|
return value;
|
|
7206
7174
|
}
|
|
7207
7175
|
|
|
7176
|
+
// src/runtime.ts
|
|
7177
|
+
var import_node_fs3 = require("node:fs");
|
|
7178
|
+
var import_node_os3 = require("node:os");
|
|
7179
|
+
var import_node_path3 = require("node:path");
|
|
7180
|
+
function getRuntimeConfig(program2, manifestOverride) {
|
|
7181
|
+
const options = program2.opts();
|
|
7182
|
+
const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
|
|
7183
|
+
const manifestPath = expandPath(
|
|
7184
|
+
manifestOverride ?? options.manifest ?? process.env.THREADNOTE_MANIFEST ?? defaultManifestPath(threadnoteHome)
|
|
7185
|
+
);
|
|
7186
|
+
return {
|
|
7187
|
+
account: process.env.THREADNOTE_ACCOUNT ?? DEFAULT_ACCOUNT,
|
|
7188
|
+
agentContextHome: threadnoteHome,
|
|
7189
|
+
agentId: process.env.THREADNOTE_AGENT_ID ?? DEFAULT_AGENT_ID,
|
|
7190
|
+
host: options.host ?? process.env.THREADNOTE_HOST ?? DEFAULT_HOST,
|
|
7191
|
+
manifestPath,
|
|
7192
|
+
openVikingVersion: process.env.THREADNOTE_OPENVIKING_VERSION ?? DEFAULT_OPENVIKING_VERSION,
|
|
7193
|
+
port: options.port ?? parsePort(process.env.THREADNOTE_PORT ?? String(DEFAULT_PORT)),
|
|
7194
|
+
user: process.env.THREADNOTE_USER ?? (0, import_node_os3.userInfo)().username
|
|
7195
|
+
};
|
|
7196
|
+
}
|
|
7197
|
+
function defaultManifestPath(agentContextHome) {
|
|
7198
|
+
const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
|
|
7199
|
+
return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
|
|
7200
|
+
}
|
|
7201
|
+
function builtInExampleManifestPath() {
|
|
7202
|
+
return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
|
|
7203
|
+
}
|
|
7204
|
+
function openVikingHealthUrl(config) {
|
|
7205
|
+
return `http://${config.host}:${config.port}/health`;
|
|
7206
|
+
}
|
|
7207
|
+
function openVikingLogPath(config) {
|
|
7208
|
+
return (0, import_node_path3.join)(config.agentContextHome, "logs", "server.log");
|
|
7209
|
+
}
|
|
7210
|
+
function openVikingServerArgs(config) {
|
|
7211
|
+
return ["--config", (0, import_node_path3.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
|
|
7212
|
+
}
|
|
7213
|
+
function withIdentity(config, args) {
|
|
7214
|
+
return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
|
|
7215
|
+
}
|
|
7216
|
+
function renderTemplate(template, config) {
|
|
7217
|
+
return template.replaceAll("{{THREADNOTE_HOME}}", config.agentContextHome).replaceAll("{{OPENVIKING_ACCOUNT}}", config.account).replaceAll("{{OPENVIKING_AGENT_ID}}", config.agentId).replaceAll("{{OPENVIKING_HOST}}", config.host).replaceAll("{{OPENVIKING_PORT}}", String(config.port)).replaceAll("{{OPENVIKING_USER}}", config.user);
|
|
7218
|
+
}
|
|
7219
|
+
|
|
7208
7220
|
// src/memory.ts
|
|
7209
7221
|
function parseMemoryKind(value) {
|
|
7210
7222
|
if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
|
|
@@ -7228,12 +7240,16 @@ async function runRemember(config, options) {
|
|
|
7228
7240
|
project: normalizeOptionalMetadata(options.project),
|
|
7229
7241
|
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
7230
7242
|
status: options.status ?? "active",
|
|
7231
|
-
supersedes: options.replace,
|
|
7232
7243
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7233
7244
|
topic: normalizeOptionalMetadata(options.topic)
|
|
7234
7245
|
};
|
|
7235
|
-
|
|
7236
|
-
|
|
7246
|
+
await storeMemory(config, {
|
|
7247
|
+
bodyText: text.trim(),
|
|
7248
|
+
dryRun: options.dryRun === true,
|
|
7249
|
+
metadata,
|
|
7250
|
+
replaceUri: options.replace,
|
|
7251
|
+
title: "MEMORY"
|
|
7252
|
+
});
|
|
7237
7253
|
}
|
|
7238
7254
|
async function runMigrateMemories(config, options) {
|
|
7239
7255
|
const dryRun = options.dryRun === true;
|
|
@@ -7406,8 +7422,14 @@ async function runList(config, uri, options) {
|
|
|
7406
7422
|
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
7407
7423
|
}
|
|
7408
7424
|
async function runHandoff(config, options) {
|
|
7409
|
-
const {
|
|
7410
|
-
await storeMemory(config,
|
|
7425
|
+
const { bodyText, metadata } = await buildHandoff(options);
|
|
7426
|
+
await storeMemory(config, {
|
|
7427
|
+
bodyText,
|
|
7428
|
+
dryRun: options.dryRun === true,
|
|
7429
|
+
metadata,
|
|
7430
|
+
replaceUri: options.replace,
|
|
7431
|
+
title: "HANDOFF"
|
|
7432
|
+
});
|
|
7411
7433
|
}
|
|
7412
7434
|
async function runArchive(config, uri, options) {
|
|
7413
7435
|
assertVikingUri(uri);
|
|
@@ -7424,12 +7446,12 @@ async function runArchive(config, uri, options) {
|
|
|
7424
7446
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7425
7447
|
topic: normalizeOptionalMetadata(options.topic)
|
|
7426
7448
|
};
|
|
7427
|
-
|
|
7428
|
-
"
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7449
|
+
await storeMemory(config, {
|
|
7450
|
+
bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
|
|
7451
|
+
dryRun: true,
|
|
7452
|
+
metadata: fallbackMetadata,
|
|
7453
|
+
title: "MEMORY"
|
|
7454
|
+
});
|
|
7433
7455
|
console.log(formatShellCommand(ov, withIdentity(config, ["rm", uri])));
|
|
7434
7456
|
return;
|
|
7435
7457
|
}
|
|
@@ -7446,12 +7468,12 @@ async function runArchive(config, uri, options) {
|
|
|
7446
7468
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7447
7469
|
topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
|
|
7448
7470
|
};
|
|
7449
|
-
|
|
7450
|
-
"
|
|
7471
|
+
await storeMemory(config, {
|
|
7472
|
+
bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
|
|
7473
|
+
dryRun: false,
|
|
7451
7474
|
metadata,
|
|
7452
|
-
|
|
7453
|
-
);
|
|
7454
|
-
await storeMemory(config, archiveMemory, { dryRun: false, metadata });
|
|
7475
|
+
title: "MEMORY"
|
|
7476
|
+
});
|
|
7455
7477
|
const removedOriginal = await removeVikingResourceWithRetry(ov, config, uri);
|
|
7456
7478
|
if (removedOriginal) {
|
|
7457
7479
|
console.log(`Archived original memory: ${uri}`);
|
|
@@ -7532,14 +7554,19 @@ async function printExactMemoryMatches(config, ov, query, options) {
|
|
|
7532
7554
|
console.log("\nExact durable memory matches:");
|
|
7533
7555
|
console.log(outputs.join("\n\n"));
|
|
7534
7556
|
}
|
|
7535
|
-
async function storeMemory(config,
|
|
7557
|
+
async function storeMemory(config, options) {
|
|
7536
7558
|
if (options.replaceUri) {
|
|
7537
7559
|
assertVikingUri(options.replaceUri);
|
|
7538
7560
|
}
|
|
7539
7561
|
const ov = await openVikingCliForMode(options.dryRun);
|
|
7540
7562
|
const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
|
|
7541
|
-
const
|
|
7542
|
-
const
|
|
7563
|
+
const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
|
|
7564
|
+
const candidateMemory = formatMemoryDocument(options.title, candidateMetadata, options.bodyText);
|
|
7565
|
+
const memoryUri = memoryUriFor(config, candidateMemory, candidateMetadata);
|
|
7566
|
+
const isInPlaceUpdate = options.replaceUri !== void 0 && options.replaceUri === memoryUri;
|
|
7567
|
+
const finalMetadata = isInPlaceUpdate ? { ...options.metadata, supersedes: void 0 } : candidateMetadata;
|
|
7568
|
+
const memory = isInPlaceUpdate ? formatMemoryDocument(options.title, finalMetadata, options.bodyText) : candidateMemory;
|
|
7569
|
+
const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
|
|
7543
7570
|
if (options.dryRun) {
|
|
7544
7571
|
console.log(memory);
|
|
7545
7572
|
console.log("\nWould run:");
|
|
@@ -7559,17 +7586,17 @@ async function storeMemory(config, memory, options) {
|
|
|
7559
7586
|
])
|
|
7560
7587
|
)
|
|
7561
7588
|
);
|
|
7562
|
-
if (options.replaceUri &&
|
|
7589
|
+
if (options.replaceUri && !isInPlaceUpdate) {
|
|
7563
7590
|
console.log(formatShellCommand(ov, withIdentity(config, ["rm", options.replaceUri])));
|
|
7564
7591
|
}
|
|
7565
7592
|
return;
|
|
7566
7593
|
}
|
|
7567
7594
|
await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
|
|
7568
7595
|
await (0, import_promises4.chmod)(memoryPath, 384);
|
|
7569
|
-
await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config,
|
|
7596
|
+
await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, finalMetadata));
|
|
7570
7597
|
await writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode);
|
|
7571
7598
|
console.log(`Stored memory: ${memoryUri}`);
|
|
7572
|
-
if (options.replaceUri &&
|
|
7599
|
+
if (options.replaceUri && !isInPlaceUpdate) {
|
|
7573
7600
|
const removedReplacedMemory = await removeVikingResourceWithRetry(ov, config, options.replaceUri);
|
|
7574
7601
|
if (removedReplacedMemory) {
|
|
7575
7602
|
console.log(`Forgot replaced memory: ${options.replaceUri}`);
|
|
@@ -7578,7 +7605,7 @@ async function storeMemory(config, memory, options) {
|
|
|
7578
7605
|
`Replacement stored, but the superseded memory is still processing. Retry later: threadnote forget ${options.replaceUri}`
|
|
7579
7606
|
);
|
|
7580
7607
|
}
|
|
7581
|
-
} else if (
|
|
7608
|
+
} else if (isInPlaceUpdate) {
|
|
7582
7609
|
console.log(`Updated existing memory in place: ${memoryUri}`);
|
|
7583
7610
|
}
|
|
7584
7611
|
}
|
|
@@ -8031,11 +8058,10 @@ async function buildHandoff(options) {
|
|
|
8031
8058
|
project: normalizeOptionalMetadata(options.project) ?? repoName,
|
|
8032
8059
|
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
8033
8060
|
status: "active",
|
|
8034
|
-
supersedes: options.replace,
|
|
8035
8061
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8036
8062
|
topic: normalizeOptionalMetadata(options.topic)
|
|
8037
8063
|
};
|
|
8038
|
-
const
|
|
8064
|
+
const bodyText = [
|
|
8039
8065
|
`repo: ${repoName}`,
|
|
8040
8066
|
`repo_path: ${repoRoot}`,
|
|
8041
8067
|
`branch: ${branch || "unknown"}`,
|
|
@@ -8059,7 +8085,7 @@ async function buildHandoff(options) {
|
|
|
8059
8085
|
"next_step:",
|
|
8060
8086
|
options.nextStep ?? "- inspect the current repo state and continue from this handoff"
|
|
8061
8087
|
].join("\n");
|
|
8062
|
-
return {
|
|
8088
|
+
return { bodyText, metadata };
|
|
8063
8089
|
}
|
|
8064
8090
|
async function gitTouchedFiles(cwd) {
|
|
8065
8091
|
const changedFiles = await gitValue(["diff", "--name-only", "HEAD"], cwd);
|
|
@@ -8083,9 +8109,201 @@ function formatBlock(value, emptyValue) {
|
|
|
8083
8109
|
return trimmed.split("\n").map((line) => `- ${line}`).join("\n");
|
|
8084
8110
|
}
|
|
8085
8111
|
|
|
8112
|
+
// src/hooks.ts
|
|
8113
|
+
var MANAGED_HOOKS = [
|
|
8114
|
+
{
|
|
8115
|
+
event: "PreCompact",
|
|
8116
|
+
command: HOOK_PRE_COMPACT_COMMAND,
|
|
8117
|
+
description: "Auto-store a handoff snapshot before context compaction so the next turn can recall it."
|
|
8118
|
+
},
|
|
8119
|
+
{
|
|
8120
|
+
event: "SessionStart",
|
|
8121
|
+
command: HOOK_SESSION_START_COMMAND,
|
|
8122
|
+
description: "Pre-load the latest handoff for the current repo into the new session context."
|
|
8123
|
+
}
|
|
8124
|
+
];
|
|
8125
|
+
async function runHooksInstall(config, agent, options) {
|
|
8126
|
+
const apply = options.apply === true && options.dryRun !== true;
|
|
8127
|
+
const remove = options.remove === true;
|
|
8128
|
+
switch (agent) {
|
|
8129
|
+
case "claude":
|
|
8130
|
+
await runClaudeHooksInstall({ apply, remove });
|
|
8131
|
+
return;
|
|
8132
|
+
case "codex":
|
|
8133
|
+
printCodexHooksNotice(remove);
|
|
8134
|
+
return;
|
|
8135
|
+
case "cursor":
|
|
8136
|
+
printNoHooksSupported("cursor", remove);
|
|
8137
|
+
return;
|
|
8138
|
+
case "copilot":
|
|
8139
|
+
printNoHooksSupported("copilot", remove);
|
|
8140
|
+
return;
|
|
8141
|
+
}
|
|
8142
|
+
}
|
|
8143
|
+
async function runClaudeHooksInstall(options) {
|
|
8144
|
+
const path = expandPath(CLAUDE_SETTINGS_PATH);
|
|
8145
|
+
const existingRaw = await exists(path) ? await (0, import_promises5.readFile)(path, "utf8") : "{}";
|
|
8146
|
+
const parsed = parseJsonConfigObject(existingRaw) ?? {};
|
|
8147
|
+
const next = options.remove ? withoutThreadnoteHooks(parsed) : withThreadnoteHooks(parsed);
|
|
8148
|
+
const before = JSON.stringify(parsed);
|
|
8149
|
+
const after = JSON.stringify(next);
|
|
8150
|
+
if (before === after) {
|
|
8151
|
+
console.log(`Claude hooks already ${options.remove ? "absent" : "managed"} in ${path}.`);
|
|
8152
|
+
return;
|
|
8153
|
+
}
|
|
8154
|
+
console.log(`${options.apply ? "Updating" : "Would update"} ${path}:`);
|
|
8155
|
+
for (const entry of MANAGED_HOOKS) {
|
|
8156
|
+
console.log(` ${options.remove ? "-" : "+"} ${entry.event}: ${entry.command}`);
|
|
8157
|
+
}
|
|
8158
|
+
if (!options.apply) {
|
|
8159
|
+
console.log("\nRe-run with --apply to actually modify the file.");
|
|
8160
|
+
return;
|
|
8161
|
+
}
|
|
8162
|
+
await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
8163
|
+
const serialized = `${JSON.stringify(next, void 0, 2)}
|
|
8164
|
+
`;
|
|
8165
|
+
await (0, import_promises5.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
|
|
8166
|
+
await (0, import_promises5.chmod)(path, 384);
|
|
8167
|
+
console.log(`${options.remove ? "Removed" : "Installed"} threadnote-managed Claude hooks.`);
|
|
8168
|
+
}
|
|
8169
|
+
function withThreadnoteHooks(input2) {
|
|
8170
|
+
const hooks = ensureMutableObject(input2.hooks);
|
|
8171
|
+
for (const entry of MANAGED_HOOKS) {
|
|
8172
|
+
const list = ensureMutableArray(hooks[entry.event]).filter((item) => !isManagedThreadnoteEntry(item));
|
|
8173
|
+
list.push({
|
|
8174
|
+
[THREADNOTE_HOOK_MARKER]: THREADNOTE_HOOK_MARKER_VALUE,
|
|
8175
|
+
matcher: "",
|
|
8176
|
+
hooks: [{ type: "command", command: entry.command }]
|
|
8177
|
+
});
|
|
8178
|
+
hooks[entry.event] = list;
|
|
8179
|
+
}
|
|
8180
|
+
return { ...input2, hooks };
|
|
8181
|
+
}
|
|
8182
|
+
function withoutThreadnoteHooks(input2) {
|
|
8183
|
+
if (!isJsonObject(input2.hooks)) {
|
|
8184
|
+
return input2;
|
|
8185
|
+
}
|
|
8186
|
+
const hooks = {};
|
|
8187
|
+
for (const [event, value] of Object.entries(input2.hooks)) {
|
|
8188
|
+
if (!Array.isArray(value)) {
|
|
8189
|
+
hooks[event] = value;
|
|
8190
|
+
continue;
|
|
8191
|
+
}
|
|
8192
|
+
const filtered = value.filter((item) => !isManagedThreadnoteEntry(item));
|
|
8193
|
+
if (filtered.length > 0) {
|
|
8194
|
+
hooks[event] = filtered;
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8197
|
+
if (Object.keys(hooks).length === 0) {
|
|
8198
|
+
const next = { ...input2 };
|
|
8199
|
+
delete next.hooks;
|
|
8200
|
+
return next;
|
|
8201
|
+
}
|
|
8202
|
+
return { ...input2, hooks };
|
|
8203
|
+
}
|
|
8204
|
+
function isManagedThreadnoteEntry(value) {
|
|
8205
|
+
return isJsonObject(value) && value[THREADNOTE_HOOK_MARKER] === THREADNOTE_HOOK_MARKER_VALUE;
|
|
8206
|
+
}
|
|
8207
|
+
function ensureMutableObject(value) {
|
|
8208
|
+
return isJsonObject(value) ? { ...value } : {};
|
|
8209
|
+
}
|
|
8210
|
+
function ensureMutableArray(value) {
|
|
8211
|
+
return Array.isArray(value) ? [...value] : [];
|
|
8212
|
+
}
|
|
8213
|
+
function printCodexHooksNotice(remove) {
|
|
8214
|
+
if (remove) {
|
|
8215
|
+
console.log("Codex CLI does not expose a managed hook surface today, so there is nothing to remove.");
|
|
8216
|
+
return;
|
|
8217
|
+
}
|
|
8218
|
+
console.log(
|
|
8219
|
+
[
|
|
8220
|
+
"Codex CLI does not currently expose lifecycle hooks (no PreCompact or SessionStart analog).",
|
|
8221
|
+
"Threadnote already installs Codex user instructions at ~/.codex/AGENTS.md; that remains the active guidance surface.",
|
|
8222
|
+
"If a future Codex release adds hook events, threadnote will pick them up via `install-hooks codex`."
|
|
8223
|
+
].join("\n")
|
|
8224
|
+
);
|
|
8225
|
+
}
|
|
8226
|
+
function printNoHooksSupported(agent, remove) {
|
|
8227
|
+
if (remove) {
|
|
8228
|
+
console.log(`${agent} does not expose a hook surface; nothing to remove.`);
|
|
8229
|
+
return;
|
|
8230
|
+
}
|
|
8231
|
+
console.log(
|
|
8232
|
+
[
|
|
8233
|
+
`${agent} does not expose agent-mode hooks today.`,
|
|
8234
|
+
"Threadnote already installs user-level instructions for this agent; that remains the active guidance surface.",
|
|
8235
|
+
"Hooks support will be added if the agent gains a hook surface upstream."
|
|
8236
|
+
].join("\n")
|
|
8237
|
+
);
|
|
8238
|
+
}
|
|
8239
|
+
async function hasManagedClaudeHooks() {
|
|
8240
|
+
const path = expandPath(CLAUDE_SETTINGS_PATH);
|
|
8241
|
+
if (!await exists(path)) {
|
|
8242
|
+
return false;
|
|
8243
|
+
}
|
|
8244
|
+
const raw = await (0, import_promises5.readFile)(path, "utf8");
|
|
8245
|
+
const parsed = parseJsonConfigObject(raw);
|
|
8246
|
+
if (!parsed || !isJsonObject(parsed.hooks)) {
|
|
8247
|
+
return false;
|
|
8248
|
+
}
|
|
8249
|
+
for (const value of Object.values(parsed.hooks)) {
|
|
8250
|
+
if (!Array.isArray(value)) {
|
|
8251
|
+
continue;
|
|
8252
|
+
}
|
|
8253
|
+
if (value.some((item) => isManagedThreadnoteEntry(item))) {
|
|
8254
|
+
return true;
|
|
8255
|
+
}
|
|
8256
|
+
}
|
|
8257
|
+
return false;
|
|
8258
|
+
}
|
|
8259
|
+
async function runPreCompactHook(config, options = {}) {
|
|
8260
|
+
try {
|
|
8261
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
8262
|
+
const project = repoRoot ? (0, import_node_path5.basename)(repoRoot) : "general";
|
|
8263
|
+
await runHandoff(config, {
|
|
8264
|
+
blockers: "- none recorded",
|
|
8265
|
+
dryRun: options.dryRun === true,
|
|
8266
|
+
nextStep: "Continue from this auto-snapshot. A manual `threadnote handoff` will produce a richer write-up if you have more context.",
|
|
8267
|
+
project,
|
|
8268
|
+
sourceAgentClient: "claude",
|
|
8269
|
+
task: "Auto-snapshot captured at Claude PreCompact (deterministic safety net before context compaction).",
|
|
8270
|
+
tests: "- not recorded (auto-snapshot)",
|
|
8271
|
+
topic: HOOK_AUTO_PRECOMPACT_TOPIC
|
|
8272
|
+
});
|
|
8273
|
+
} catch (err) {
|
|
8274
|
+
process.stderr.write(
|
|
8275
|
+
`threadnote pre-compact-hook: snapshot skipped (${err instanceof Error ? err.message : String(err)})
|
|
8276
|
+
`
|
|
8277
|
+
);
|
|
8278
|
+
}
|
|
8279
|
+
}
|
|
8280
|
+
async function runSessionStartHook(config, options = {}) {
|
|
8281
|
+
try {
|
|
8282
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
8283
|
+
if (!repoRoot) {
|
|
8284
|
+
return;
|
|
8285
|
+
}
|
|
8286
|
+
const project = (0, import_node_path5.basename)(repoRoot);
|
|
8287
|
+
process.stdout.write(`## Threadnote \u2014 latest context for ${project}
|
|
8288
|
+
|
|
8289
|
+
`);
|
|
8290
|
+
await runRecall(config, {
|
|
8291
|
+
dryRun: options.dryRun === true,
|
|
8292
|
+
inferScope: true,
|
|
8293
|
+
nodeLimit: "5",
|
|
8294
|
+
query: `${project} latest handoff durable feature memory`
|
|
8295
|
+
});
|
|
8296
|
+
} catch (err) {
|
|
8297
|
+
process.stderr.write(
|
|
8298
|
+
`threadnote session-start-hook: recall skipped (${err instanceof Error ? err.message : String(err)})
|
|
8299
|
+
`
|
|
8300
|
+
);
|
|
8301
|
+
}
|
|
8302
|
+
}
|
|
8303
|
+
|
|
8086
8304
|
// src/seeding.ts
|
|
8087
|
-
var
|
|
8088
|
-
var
|
|
8305
|
+
var import_promises6 = require("node:fs/promises");
|
|
8306
|
+
var import_node_path6 = require("node:path");
|
|
8089
8307
|
async function runSeed(config, options) {
|
|
8090
8308
|
const manifest = await readSeedManifest(config.manifestPath);
|
|
8091
8309
|
const ignorePatterns = await loadIgnorePatterns();
|
|
@@ -8114,7 +8332,7 @@ async function runSeed(config, options) {
|
|
|
8114
8332
|
}
|
|
8115
8333
|
async function runInitManifest(config, options) {
|
|
8116
8334
|
const manifestPath = expandPath(
|
|
8117
|
-
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0,
|
|
8335
|
+
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path6.join)(config.agentContextHome, USER_MANIFEST_NAME)
|
|
8118
8336
|
);
|
|
8119
8337
|
const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
|
|
8120
8338
|
const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
|
|
@@ -8157,9 +8375,9 @@ async function runInitManifest(config, options) {
|
|
|
8157
8375
|
console.log(output2.trimEnd());
|
|
8158
8376
|
return;
|
|
8159
8377
|
}
|
|
8160
|
-
await ensureDirectory((0,
|
|
8161
|
-
await (0,
|
|
8162
|
-
await (0,
|
|
8378
|
+
await ensureDirectory((0, import_node_path6.dirname)(manifestPath), false);
|
|
8379
|
+
await (0, import_promises6.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
8380
|
+
await (0, import_promises6.chmod)(manifestPath, 384);
|
|
8163
8381
|
console.log(`Wrote manifest: ${manifestPath}`);
|
|
8164
8382
|
console.log("Seed with:");
|
|
8165
8383
|
console.log(" threadnote seed --dry-run");
|
|
@@ -8189,13 +8407,13 @@ async function resolveRepoRoot(repoInput) {
|
|
|
8189
8407
|
async function projectIdentity(path) {
|
|
8190
8408
|
const expanded = expandPath(path);
|
|
8191
8409
|
try {
|
|
8192
|
-
return await (0,
|
|
8410
|
+
return await (0, import_promises6.realpath)(expanded);
|
|
8193
8411
|
} catch (_err) {
|
|
8194
8412
|
return expanded;
|
|
8195
8413
|
}
|
|
8196
8414
|
}
|
|
8197
8415
|
function projectManifestForRepo(repoRoot, existingProjects) {
|
|
8198
|
-
const baseName = uriSegment((0,
|
|
8416
|
+
const baseName = uriSegment((0, import_node_path6.basename)(repoRoot));
|
|
8199
8417
|
const usedNames = new Set(existingProjects.map((project) => project.name));
|
|
8200
8418
|
const usedUris = new Set(existingProjects.map((project) => project.uri));
|
|
8201
8419
|
let name = baseName;
|
|
@@ -8217,7 +8435,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
|
8217
8435
|
for (const pattern of project.seed) {
|
|
8218
8436
|
const files = await resolveProjectPattern(projectRoot, pattern);
|
|
8219
8437
|
for (const filePath of files) {
|
|
8220
|
-
const relativePath = toPosixPath((0,
|
|
8438
|
+
const relativePath = toPosixPath((0, import_node_path6.relative)(projectRoot, filePath));
|
|
8221
8439
|
if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
|
|
8222
8440
|
continue;
|
|
8223
8441
|
}
|
|
@@ -8235,20 +8453,20 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
|
8235
8453
|
async function resolveProjectPattern(projectRoot, pattern) {
|
|
8236
8454
|
const normalizedPattern = toPosixPath(pattern);
|
|
8237
8455
|
if (!hasGlob(normalizedPattern)) {
|
|
8238
|
-
const filePath = (0,
|
|
8456
|
+
const filePath = (0, import_node_path6.join)(projectRoot, normalizedPattern);
|
|
8239
8457
|
return await isFile(filePath) ? [filePath] : [];
|
|
8240
8458
|
}
|
|
8241
8459
|
const globBase = getGlobBase(normalizedPattern);
|
|
8242
|
-
const basePath = (0,
|
|
8460
|
+
const basePath = (0, import_node_path6.join)(projectRoot, globBase);
|
|
8243
8461
|
if (!await exists(basePath)) {
|
|
8244
8462
|
return [];
|
|
8245
8463
|
}
|
|
8246
8464
|
const regex = globToRegExp(normalizedPattern);
|
|
8247
8465
|
const files = await walkFiles(basePath);
|
|
8248
|
-
return files.filter((filePath) => regex.test(toPosixPath((0,
|
|
8466
|
+
return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path6.relative)(projectRoot, filePath))));
|
|
8249
8467
|
}
|
|
8250
8468
|
async function prepareSeedFile(config, candidate, dryRun) {
|
|
8251
|
-
const content = await (0,
|
|
8469
|
+
const content = await (0, import_promises6.readFile)(candidate.filePath, "utf8");
|
|
8252
8470
|
const redactedContent = shouldRedactPath(candidate.relativePath) ? redactContent(candidate.relativePath, content) : content;
|
|
8253
8471
|
const secretMatches = detectSecretMatches(redactedContent);
|
|
8254
8472
|
if (secretMatches.length > 0) {
|
|
@@ -8260,14 +8478,14 @@ async function prepareSeedFile(config, candidate, dryRun) {
|
|
|
8260
8478
|
if (redactedContent === content) {
|
|
8261
8479
|
return candidate.filePath;
|
|
8262
8480
|
}
|
|
8263
|
-
const redactedPath = (0,
|
|
8481
|
+
const redactedPath = (0, import_node_path6.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
|
|
8264
8482
|
if (dryRun) {
|
|
8265
8483
|
console.log(`Would write redacted copy: ${redactedPath}`);
|
|
8266
8484
|
return redactedPath;
|
|
8267
8485
|
}
|
|
8268
|
-
await ensureDirectory((0,
|
|
8269
|
-
await (0,
|
|
8270
|
-
await (0,
|
|
8486
|
+
await ensureDirectory((0, import_node_path6.dirname)(redactedPath), false);
|
|
8487
|
+
await (0, import_promises6.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
|
|
8488
|
+
await (0, import_promises6.chmod)(redactedPath, 384);
|
|
8271
8489
|
return redactedPath;
|
|
8272
8490
|
}
|
|
8273
8491
|
async function collectSkillCandidates(config) {
|
|
@@ -8292,7 +8510,7 @@ async function collectSkillCandidates(config) {
|
|
|
8292
8510
|
for (const source of sources) {
|
|
8293
8511
|
const files = await resolveAbsolutePattern(expandPath(source.pattern));
|
|
8294
8512
|
for (const filePath of files) {
|
|
8295
|
-
const content = await (0,
|
|
8513
|
+
const content = await (0, import_promises6.readFile)(filePath, "utf8");
|
|
8296
8514
|
const matches = detectSecretMatches(content);
|
|
8297
8515
|
if (matches.length > 0) {
|
|
8298
8516
|
console.log(`SKIP skill with possible secret: ${filePath}`);
|
|
@@ -8323,10 +8541,10 @@ async function resolveAbsolutePattern(pattern) {
|
|
|
8323
8541
|
return files.filter((filePath) => regex.test(toPosixPath(filePath)));
|
|
8324
8542
|
}
|
|
8325
8543
|
function skillResourceUri(skill) {
|
|
8326
|
-
return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0,
|
|
8544
|
+
return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path6.basename)((0, import_node_path6.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
|
|
8327
8545
|
}
|
|
8328
8546
|
async function loadIgnorePatterns() {
|
|
8329
|
-
const raw = await (0,
|
|
8547
|
+
const raw = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), ".threadnoteignore"), "utf8");
|
|
8330
8548
|
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
8331
8549
|
}
|
|
8332
8550
|
function matchesIgnore(relativePath, patterns) {
|
|
@@ -8397,14 +8615,16 @@ function detectSecretMatches(content) {
|
|
|
8397
8615
|
}
|
|
8398
8616
|
|
|
8399
8617
|
// src/share.ts
|
|
8400
|
-
var
|
|
8618
|
+
var import_promises7 = require("node:fs/promises");
|
|
8401
8619
|
var import_node_os4 = require("node:os");
|
|
8402
|
-
var
|
|
8620
|
+
var import_node_path7 = require("node:path");
|
|
8403
8621
|
var TEAMS_FILE_VERSION = 1;
|
|
8404
8622
|
var SHARED_SEGMENT = "shared";
|
|
8405
8623
|
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
8406
8624
|
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
8407
8625
|
var SCRUBBER_PATTERNS = [
|
|
8626
|
+
// Credentials: never redactable. Blocking is the only safe response —
|
|
8627
|
+
// automated redaction risks false negatives that leave material in git.
|
|
8408
8628
|
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
8409
8629
|
{ name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
8410
8630
|
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
@@ -8417,8 +8637,35 @@ var SCRUBBER_PATTERNS = [
|
|
|
8417
8637
|
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
8418
8638
|
// Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
|
|
8419
8639
|
// 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 }
|
|
8640
|
+
{ name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i },
|
|
8641
|
+
// Soft leaks: block by default (so the agent sees them and decides), but
|
|
8642
|
+
// allow opt-in redaction so curated memories with incidental matches can
|
|
8643
|
+
// ship without a manual rewrite. Local home paths are the recurring
|
|
8644
|
+
// real-world leak; the regexes greedily consume the whole path segment
|
|
8645
|
+
// (including subdirectories) up to whitespace or common closing punctuation
|
|
8646
|
+
// so redaction collapses an entire path to a single placeholder rather than
|
|
8647
|
+
// leaving the subpath visible.
|
|
8648
|
+
{ name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
|
|
8649
|
+
{ name: "linux home path", placeholder: "<local-path>", regex: /\b\/home\/[^\s)>"'`,]+/ }
|
|
8421
8650
|
];
|
|
8651
|
+
function applyScrubber(content, { redact }) {
|
|
8652
|
+
let cleaned = content;
|
|
8653
|
+
const redactions = [];
|
|
8654
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
8655
|
+
if (!pattern.regex.test(cleaned)) {
|
|
8656
|
+
continue;
|
|
8657
|
+
}
|
|
8658
|
+
if (!pattern.placeholder || !redact) {
|
|
8659
|
+
return { blocker: pattern.name, cleaned: content, redactions: [] };
|
|
8660
|
+
}
|
|
8661
|
+
const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
|
|
8662
|
+
const globalRegex = new RegExp(pattern.regex.source, flags);
|
|
8663
|
+
const matches = cleaned.match(globalRegex) ?? [];
|
|
8664
|
+
cleaned = cleaned.replace(globalRegex, pattern.placeholder);
|
|
8665
|
+
redactions.push({ count: matches.length, name: pattern.name });
|
|
8666
|
+
}
|
|
8667
|
+
return { cleaned, redactions };
|
|
8668
|
+
}
|
|
8422
8669
|
async function runShareInit(config, remoteUrl, options) {
|
|
8423
8670
|
if (!remoteUrl.trim()) {
|
|
8424
8671
|
throw new Error("Provide a git remote URL for the shared memories repo.");
|
|
@@ -8437,8 +8684,8 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
8437
8684
|
if (await exists(gitdir)) {
|
|
8438
8685
|
throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
|
|
8439
8686
|
}
|
|
8440
|
-
await ensureDirectory((0,
|
|
8441
|
-
await ensureDirectory((0,
|
|
8687
|
+
await ensureDirectory((0, import_node_path7.dirname)(worktree), dryRun);
|
|
8688
|
+
await ensureDirectory((0, import_node_path7.dirname)(gitdir), dryRun);
|
|
8442
8689
|
const git = await requiredExecutable("git");
|
|
8443
8690
|
await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
|
|
8444
8691
|
const newConfig = {
|
|
@@ -8469,7 +8716,7 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
8469
8716
|
var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
|
|
8470
8717
|
var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
|
|
8471
8718
|
async function ensureSharedGitignore(worktree, git, push) {
|
|
8472
|
-
const gitignorePath = (0,
|
|
8719
|
+
const gitignorePath = (0, import_node_path7.join)(worktree, ".gitignore");
|
|
8473
8720
|
const existing = await readFileIfExists(gitignorePath) ?? "";
|
|
8474
8721
|
const lines = existing.split("\n").map((line) => line.trim());
|
|
8475
8722
|
const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
@@ -8488,7 +8735,7 @@ async function ensureSharedGitignore(worktree, git, push) {
|
|
|
8488
8735
|
segments.push(SHARED_GITIGNORE_HEADER, "\n");
|
|
8489
8736
|
}
|
|
8490
8737
|
segments.push(missingPatterns.join("\n"), "\n");
|
|
8491
|
-
await (0,
|
|
8738
|
+
await (0, import_promises7.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
|
|
8492
8739
|
console.log(`Added ${missingPatterns.join(", ")} to ${portablePath(gitignorePath)}`);
|
|
8493
8740
|
await maybeRun(false, git, ["-C", worktree, "add", ".gitignore"]);
|
|
8494
8741
|
const commitResult = await runCommand(
|
|
@@ -8553,7 +8800,7 @@ async function runShareSync(config, options) {
|
|
|
8553
8800
|
if (dryRun) {
|
|
8554
8801
|
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
8555
8802
|
} else if (pullResult && pullResult.exitCode !== 0) {
|
|
8556
|
-
if (await exists((0,
|
|
8803
|
+
if (await exists((0, import_node_path7.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path7.join)(team.config.gitdir, "rebase-apply"))) {
|
|
8557
8804
|
throw new Error(
|
|
8558
8805
|
`git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
|
|
8559
8806
|
Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
|
|
@@ -8583,16 +8830,41 @@ async function runSharePublish(config, sourceUri, options) {
|
|
|
8583
8830
|
assertVikingUri(sourceUri);
|
|
8584
8831
|
const team = await resolveTeam(config, options.team);
|
|
8585
8832
|
const dryRun = options.dryRun === true;
|
|
8833
|
+
const preview = options.preview === true;
|
|
8586
8834
|
if (isInSharedNamespace(config, sourceUri)) {
|
|
8587
8835
|
throw new Error(`Memory ${sourceUri} is already in the shared namespace.`);
|
|
8588
8836
|
}
|
|
8589
8837
|
const ov = await openVikingCliForMode(dryRun);
|
|
8590
|
-
const
|
|
8591
|
-
const
|
|
8592
|
-
|
|
8593
|
-
throw new Error(`Refusing to publish ${sourceUri}: possible ${blocker}. Strip the sensitive value, then retry.`);
|
|
8594
|
-
}
|
|
8838
|
+
const rawContent = await readMemoryContent(config, ov, sourceUri, dryRun);
|
|
8839
|
+
const stripped = stripPersonalProvenance(rawContent);
|
|
8840
|
+
const scrub = applyScrubber(stripped, { redact: options.redact === true });
|
|
8595
8841
|
const targetUri = sharedUriFor(config, sourceUri, team.name);
|
|
8842
|
+
if (preview) {
|
|
8843
|
+
console.log(`PREVIEW source: ${sourceUri}`);
|
|
8844
|
+
console.log(`PREVIEW destination: ${targetUri}`);
|
|
8845
|
+
if (scrub.blocker) {
|
|
8846
|
+
console.log(
|
|
8847
|
+
`PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or rerun with --redact for soft-leak patterns.`
|
|
8848
|
+
);
|
|
8849
|
+
return;
|
|
8850
|
+
}
|
|
8851
|
+
for (const redaction of scrub.redactions) {
|
|
8852
|
+
console.log(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
|
|
8853
|
+
}
|
|
8854
|
+
console.log("-----BEGIN PREVIEW-----");
|
|
8855
|
+
console.log(scrub.cleaned);
|
|
8856
|
+
console.log("-----END PREVIEW-----");
|
|
8857
|
+
return;
|
|
8858
|
+
}
|
|
8859
|
+
if (scrub.blocker) {
|
|
8860
|
+
throw new Error(
|
|
8861
|
+
`Refusing to publish ${sourceUri}: possible ${scrub.blocker}. Strip the sensitive value or pass --redact for soft-leak patterns.`
|
|
8862
|
+
);
|
|
8863
|
+
}
|
|
8864
|
+
for (const redaction of scrub.redactions) {
|
|
8865
|
+
console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before publish.`);
|
|
8866
|
+
}
|
|
8867
|
+
const content = scrub.cleaned;
|
|
8596
8868
|
if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
|
|
8597
8869
|
throw new Error(
|
|
8598
8870
|
`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.`
|
|
@@ -8704,10 +8976,10 @@ function normalizeTeamName(input2) {
|
|
|
8704
8976
|
return candidate;
|
|
8705
8977
|
}
|
|
8706
8978
|
function teamsFilePath(config) {
|
|
8707
|
-
return (0,
|
|
8979
|
+
return (0, import_node_path7.join)(config.agentContextHome, "share", "teams.json");
|
|
8708
8980
|
}
|
|
8709
8981
|
function teamWorktreePath(config, team) {
|
|
8710
|
-
return (0,
|
|
8982
|
+
return (0, import_node_path7.join)(
|
|
8711
8983
|
config.agentContextHome,
|
|
8712
8984
|
"data",
|
|
8713
8985
|
"viking",
|
|
@@ -8720,7 +8992,7 @@ function teamWorktreePath(config, team) {
|
|
|
8720
8992
|
);
|
|
8721
8993
|
}
|
|
8722
8994
|
function teamGitdirPath(config, team) {
|
|
8723
|
-
return (0,
|
|
8995
|
+
return (0, import_node_path7.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
8724
8996
|
}
|
|
8725
8997
|
async function readTeamsFile(config) {
|
|
8726
8998
|
const path = teamsFilePath(config);
|
|
@@ -8763,13 +9035,13 @@ async function readTeamsFile(config) {
|
|
|
8763
9035
|
}
|
|
8764
9036
|
async function writeTeamsFile(config, contents) {
|
|
8765
9037
|
const path = teamsFilePath(config);
|
|
8766
|
-
await (0,
|
|
9038
|
+
await (0, import_promises7.mkdir)((0, import_node_path7.dirname)(path), { recursive: true });
|
|
8767
9039
|
const serializable = {
|
|
8768
9040
|
defaultTeam: contents.defaultTeam,
|
|
8769
9041
|
teams: contents.teams,
|
|
8770
9042
|
version: contents.version
|
|
8771
9043
|
};
|
|
8772
|
-
await (0,
|
|
9044
|
+
await (0, import_promises7.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
|
|
8773
9045
|
`, { encoding: "utf8", mode: 384 });
|
|
8774
9046
|
}
|
|
8775
9047
|
async function resolveTeam(config, requested) {
|
|
@@ -8799,7 +9071,7 @@ async function assertWorktreeUsable(worktree) {
|
|
|
8799
9071
|
if (!await isDirectory(worktree)) {
|
|
8800
9072
|
throw new Error(`Cannot use ${worktree} as a worktree: not a directory.`);
|
|
8801
9073
|
}
|
|
8802
|
-
const entries = await (0,
|
|
9074
|
+
const entries = await (0, import_promises7.readdir)(worktree);
|
|
8803
9075
|
if (entries.length > 0) {
|
|
8804
9076
|
const preview = entries.slice(0, 5).join(", ");
|
|
8805
9077
|
const suffix = entries.length > 5 ? `, +${entries.length - 5} more` : "";
|
|
@@ -8823,7 +9095,7 @@ async function walkMemoryFiles(root) {
|
|
|
8823
9095
|
async function visit(path, depth) {
|
|
8824
9096
|
let entries;
|
|
8825
9097
|
try {
|
|
8826
|
-
entries = await (0,
|
|
9098
|
+
entries = await (0, import_promises7.readdir)(path, { withFileTypes: true });
|
|
8827
9099
|
} catch (err) {
|
|
8828
9100
|
console.warn(`Skipping ${path} during shared-tree walk: ${err instanceof Error ? err.message : String(err)}`);
|
|
8829
9101
|
return;
|
|
@@ -8832,7 +9104,7 @@ async function walkMemoryFiles(root) {
|
|
|
8832
9104
|
if (entry.name === ".git") {
|
|
8833
9105
|
continue;
|
|
8834
9106
|
}
|
|
8835
|
-
const full = (0,
|
|
9107
|
+
const full = (0, import_node_path7.join)(path, entry.name);
|
|
8836
9108
|
if (entry.isDirectory()) {
|
|
8837
9109
|
if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
|
|
8838
9110
|
continue;
|
|
@@ -8856,7 +9128,7 @@ async function walkMemoryFiles(root) {
|
|
|
8856
9128
|
return out;
|
|
8857
9129
|
}
|
|
8858
9130
|
function workfileToVikingUri(config, team, filePath) {
|
|
8859
|
-
const rel = (0,
|
|
9131
|
+
const rel = (0, import_node_path7.relative)(team.worktree, filePath).split(import_node_path7.sep).join("/");
|
|
8860
9132
|
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
8861
9133
|
}
|
|
8862
9134
|
function isInSharedNamespace(config, uri) {
|
|
@@ -8888,8 +9160,26 @@ function vikingUriToWorktreeRelative(config, uri, team) {
|
|
|
8888
9160
|
}
|
|
8889
9161
|
return uri.slice(prefix.length);
|
|
8890
9162
|
}
|
|
8891
|
-
function
|
|
8892
|
-
|
|
9163
|
+
function stripPersonalProvenance(content) {
|
|
9164
|
+
const lines = content.split("\n");
|
|
9165
|
+
let headerEnd = lines.length;
|
|
9166
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
9167
|
+
if (lines[index].trim() === "") {
|
|
9168
|
+
headerEnd = index;
|
|
9169
|
+
break;
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
const cleaned = [];
|
|
9173
|
+
for (let index = 0; index < headerEnd; index += 1) {
|
|
9174
|
+
if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
|
|
9175
|
+
continue;
|
|
9176
|
+
}
|
|
9177
|
+
cleaned.push(lines[index]);
|
|
9178
|
+
}
|
|
9179
|
+
for (let index = headerEnd; index < lines.length; index += 1) {
|
|
9180
|
+
cleaned.push(lines[index]);
|
|
9181
|
+
}
|
|
9182
|
+
return cleaned.join("\n");
|
|
8893
9183
|
}
|
|
8894
9184
|
async function readMemoryContent(config, ov, uri, dryRun) {
|
|
8895
9185
|
const args = withIdentity(config, ["read", uri]);
|
|
@@ -8950,13 +9240,13 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun) {
|
|
|
8950
9240
|
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8951
9241
|
return;
|
|
8952
9242
|
}
|
|
8953
|
-
const stagingDir = await (0,
|
|
8954
|
-
const tempPath = (0,
|
|
9243
|
+
const stagingDir = await (0, import_promises7.mkdtemp)((0, import_node_path7.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
|
|
9244
|
+
const tempPath = (0, import_node_path7.join)(stagingDir, "body.txt");
|
|
8955
9245
|
try {
|
|
8956
|
-
await (0,
|
|
9246
|
+
await (0, import_promises7.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
8957
9247
|
await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode);
|
|
8958
9248
|
} finally {
|
|
8959
|
-
await (0,
|
|
9249
|
+
await (0, import_promises7.rm)(stagingDir, { force: true, recursive: true });
|
|
8960
9250
|
}
|
|
8961
9251
|
}
|
|
8962
9252
|
async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode) {
|
|
@@ -9014,7 +9304,7 @@ ${stdout}`.toLowerCase();
|
|
|
9014
9304
|
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
9305
|
}
|
|
9016
9306
|
async function ingestSingleFile(ov, config, uri, filePath, initialMode) {
|
|
9017
|
-
const content = await (0,
|
|
9307
|
+
const content = await (0, import_promises7.readFile)(filePath, "utf8");
|
|
9018
9308
|
await writeMemoryFile(config, ov, uri, content, initialMode, false);
|
|
9019
9309
|
}
|
|
9020
9310
|
async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
|
|
@@ -9056,7 +9346,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
|
9056
9346
|
if (!relative3) {
|
|
9057
9347
|
return;
|
|
9058
9348
|
}
|
|
9059
|
-
await (0,
|
|
9349
|
+
await (0, import_promises7.rm)((0, import_node_path7.join)(worktree, relative3), { force: true });
|
|
9060
9350
|
}
|
|
9061
9351
|
async function removeMemoryUri(config, ov, uri, dryRun) {
|
|
9062
9352
|
const args = withIdentity(config, ["rm", uri]);
|
|
@@ -9113,8 +9403,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
9113
9403
|
const oldRel = entries[index + 1];
|
|
9114
9404
|
const newRel = entries[index + 2];
|
|
9115
9405
|
if (oldRel && newRel) {
|
|
9116
|
-
changes.push({ path: (0,
|
|
9117
|
-
changes.push({ path: (0,
|
|
9406
|
+
changes.push({ path: (0, import_node_path7.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
9407
|
+
changes.push({ path: (0, import_node_path7.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
9118
9408
|
}
|
|
9119
9409
|
index += 3;
|
|
9120
9410
|
continue;
|
|
@@ -9122,7 +9412,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
9122
9412
|
const rel = entries[index + 1];
|
|
9123
9413
|
if (rel) {
|
|
9124
9414
|
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
9125
|
-
changes.push({ path: (0,
|
|
9415
|
+
changes.push({ path: (0, import_node_path7.join)(worktree, rel), relativePath: rel, status });
|
|
9126
9416
|
}
|
|
9127
9417
|
index += 2;
|
|
9128
9418
|
}
|
|
@@ -9160,18 +9450,18 @@ async function applyChangesToOpenViking(config, team, changes) {
|
|
|
9160
9450
|
// src/lifecycle.ts
|
|
9161
9451
|
var import_node_child_process2 = require("node:child_process");
|
|
9162
9452
|
var import_node_fs5 = require("node:fs");
|
|
9163
|
-
var
|
|
9453
|
+
var import_promises10 = require("node:fs/promises");
|
|
9164
9454
|
var import_node_os6 = require("node:os");
|
|
9165
|
-
var
|
|
9455
|
+
var import_node_path9 = require("node:path");
|
|
9166
9456
|
var import_node_process2 = require("node:process");
|
|
9167
|
-
var
|
|
9457
|
+
var import_promises11 = require("node:readline/promises");
|
|
9168
9458
|
|
|
9169
9459
|
// src/update.ts
|
|
9170
9460
|
var import_node_fs4 = require("node:fs");
|
|
9171
|
-
var
|
|
9461
|
+
var import_promises8 = require("node:fs/promises");
|
|
9172
9462
|
var import_node_os5 = require("node:os");
|
|
9173
|
-
var
|
|
9174
|
-
var
|
|
9463
|
+
var import_node_path8 = require("node:path");
|
|
9464
|
+
var import_promises9 = require("node:readline/promises");
|
|
9175
9465
|
var import_node_process = require("node:process");
|
|
9176
9466
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
9177
9467
|
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
@@ -9319,7 +9609,7 @@ async function getUpdateInfo(config, options) {
|
|
|
9319
9609
|
};
|
|
9320
9610
|
}
|
|
9321
9611
|
async function currentPackageVersion() {
|
|
9322
|
-
const rawPackage = await (0,
|
|
9612
|
+
const rawPackage = await (0, import_promises8.readFile)((0, import_node_path8.join)(toolRoot(), "package.json"), "utf8");
|
|
9323
9613
|
const parsed = JSON.parse(rawPackage);
|
|
9324
9614
|
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
9325
9615
|
throw new Error("Could not read current threadnote package version.");
|
|
@@ -9369,11 +9659,11 @@ async function readFreshCache(config, registry) {
|
|
|
9369
9659
|
}
|
|
9370
9660
|
async function writeUpdateCache(config, cache) {
|
|
9371
9661
|
await ensureDirectory(config.agentContextHome, false);
|
|
9372
|
-
await (0,
|
|
9662
|
+
await (0, import_promises8.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
|
|
9373
9663
|
`, { encoding: "utf8", mode: 384 });
|
|
9374
9664
|
}
|
|
9375
9665
|
function updateCachePath(config) {
|
|
9376
|
-
return (0,
|
|
9666
|
+
return (0, import_node_path8.join)(config.agentContextHome, "update-check.json");
|
|
9377
9667
|
}
|
|
9378
9668
|
async function runApplicablePostUpdateMigrations(config, options) {
|
|
9379
9669
|
const state = await readPostUpdateState(config);
|
|
@@ -9437,7 +9727,7 @@ async function applicablePostUpdateMigrations(config, options) {
|
|
|
9437
9727
|
return applicable;
|
|
9438
9728
|
}
|
|
9439
9729
|
async function readPostUpdateMigrations() {
|
|
9440
|
-
const raw = await readFileIfExists((0,
|
|
9730
|
+
const raw = await readFileIfExists((0, import_node_path8.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
|
|
9441
9731
|
if (!raw) {
|
|
9442
9732
|
return [];
|
|
9443
9733
|
}
|
|
@@ -9476,7 +9766,7 @@ function printPostUpdateMigration(migration) {
|
|
|
9476
9766
|
}
|
|
9477
9767
|
}
|
|
9478
9768
|
async function confirmPostUpdateMigration(prompt) {
|
|
9479
|
-
const readline = (0,
|
|
9769
|
+
const readline = (0, import_promises9.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
9480
9770
|
try {
|
|
9481
9771
|
const answer = (await readline.question(prompt)).trim().toLowerCase();
|
|
9482
9772
|
return answer === "y" || answer === "yes";
|
|
@@ -9508,11 +9798,11 @@ async function readPostUpdateState(config) {
|
|
|
9508
9798
|
}
|
|
9509
9799
|
async function writePostUpdateState(config, state) {
|
|
9510
9800
|
await ensureDirectory(config.agentContextHome, false);
|
|
9511
|
-
await (0,
|
|
9801
|
+
await (0, import_promises8.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
|
|
9512
9802
|
`, { encoding: "utf8", mode: 384 });
|
|
9513
9803
|
}
|
|
9514
9804
|
function postUpdateStatePath(config) {
|
|
9515
|
-
return (0,
|
|
9805
|
+
return (0, import_node_path8.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
|
|
9516
9806
|
}
|
|
9517
9807
|
async function resolveUpdateRuntime(runtime) {
|
|
9518
9808
|
if (runtime !== "auto") {
|
|
@@ -9542,18 +9832,18 @@ async function runtimeThreadnoteBin(runtime) {
|
|
|
9542
9832
|
if (runtime === "npm") {
|
|
9543
9833
|
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
9544
9834
|
const prefix = result.stdout.trim();
|
|
9545
|
-
return prefix ? (0,
|
|
9835
|
+
return prefix ? (0, import_node_path8.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
9546
9836
|
}
|
|
9547
9837
|
if (runtime === "bun") {
|
|
9548
9838
|
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
9549
9839
|
const binDir = result.stdout.trim();
|
|
9550
|
-
return binDir ? (0,
|
|
9840
|
+
return binDir ? (0, import_node_path8.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
9551
9841
|
}
|
|
9552
|
-
return (0,
|
|
9842
|
+
return (0, import_node_path8.join)(process.env.DENO_INSTALL ?? (0, import_node_path8.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
|
|
9553
9843
|
}
|
|
9554
9844
|
async function isExecutable(path) {
|
|
9555
9845
|
try {
|
|
9556
|
-
await (0,
|
|
9846
|
+
await (0, import_promises8.access)(path, import_node_fs4.constants.X_OK);
|
|
9557
9847
|
return true;
|
|
9558
9848
|
} catch (_err) {
|
|
9559
9849
|
return false;
|
|
@@ -9644,9 +9934,9 @@ async function runDoctor(config, options) {
|
|
|
9644
9934
|
checks.push(await commandShimCheck());
|
|
9645
9935
|
checks.push(...await userAgentInstructionsChecks());
|
|
9646
9936
|
checks.push(await manifestCheck(config.manifestPath));
|
|
9647
|
-
checks.push(await fileCheck((0,
|
|
9648
|
-
checks.push(await fileCheck((0,
|
|
9649
|
-
checks.push(await fileCheck((0,
|
|
9937
|
+
checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
|
|
9938
|
+
checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
|
|
9939
|
+
checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
|
|
9650
9940
|
checks.push(await healthCheck(config));
|
|
9651
9941
|
for (const check of checks) {
|
|
9652
9942
|
console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
|
|
@@ -9663,9 +9953,9 @@ async function runInstall(config, options) {
|
|
|
9663
9953
|
const repairInvalidConfigs = options.repairInvalidConfigs === true;
|
|
9664
9954
|
const dryRun = options.dryRun === true;
|
|
9665
9955
|
await ensureDirectory(config.agentContextHome, dryRun);
|
|
9666
|
-
await ensureDirectory((0,
|
|
9667
|
-
await ensureDirectory((0,
|
|
9668
|
-
await ensureDirectory((0,
|
|
9956
|
+
await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "logs"), dryRun);
|
|
9957
|
+
await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "redacted"), dryRun);
|
|
9958
|
+
await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "mcp"), dryRun);
|
|
9669
9959
|
await installCommandShim(dryRun);
|
|
9670
9960
|
await installUserAgentInstructions(dryRun);
|
|
9671
9961
|
const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
@@ -9691,17 +9981,17 @@ async function runInstall(config, options) {
|
|
|
9691
9981
|
}
|
|
9692
9982
|
await writeTemplateIfMissing({
|
|
9693
9983
|
config,
|
|
9694
|
-
destinationPath: (0,
|
|
9984
|
+
destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ov.conf"),
|
|
9695
9985
|
dryRun,
|
|
9696
9986
|
shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
9697
|
-
templatePath: (0,
|
|
9987
|
+
templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json")
|
|
9698
9988
|
});
|
|
9699
9989
|
await writeTemplateIfMissing({
|
|
9700
9990
|
config,
|
|
9701
|
-
destinationPath: (0,
|
|
9991
|
+
destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ovcli.conf"),
|
|
9702
9992
|
dryRun,
|
|
9703
9993
|
shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
9704
|
-
templatePath: (0,
|
|
9994
|
+
templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json")
|
|
9705
9995
|
});
|
|
9706
9996
|
if (options.start !== false) {
|
|
9707
9997
|
const healthy = await repairServerHealth(config, dryRun);
|
|
@@ -9738,6 +10028,10 @@ async function runRepair(config, options) {
|
|
|
9738
10028
|
await runMcpInstall(config, client, { apply: !dryRun, name: OPENVIKING_MCP_NAME });
|
|
9739
10029
|
}
|
|
9740
10030
|
}
|
|
10031
|
+
if (await hasManagedClaudeHooks()) {
|
|
10032
|
+
console.log("\nRepairing claude hooks (re-asserting threadnote-managed entries).");
|
|
10033
|
+
await runHooksInstall(config, "claude", { apply: !dryRun, dryRun });
|
|
10034
|
+
}
|
|
9741
10035
|
console.log("\nPost-repair doctor:");
|
|
9742
10036
|
await runDoctor(config, { dryRun, strict: false });
|
|
9743
10037
|
if (options.postUpdate !== false) {
|
|
@@ -9751,10 +10045,13 @@ async function runUninstall(config, options) {
|
|
|
9751
10045
|
}
|
|
9752
10046
|
console.log("Uninstalling local Threadnote setup.");
|
|
9753
10047
|
await runStop(config, { dryRun });
|
|
9754
|
-
await removePathIfExists((0,
|
|
10048
|
+
await removePathIfExists((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
|
|
9755
10049
|
await removeLaunchAgent(dryRun);
|
|
9756
10050
|
await removeMcpConfigs(options.mcp ?? "available", dryRun);
|
|
9757
10051
|
await removeMcpSnippets(config, dryRun);
|
|
10052
|
+
if (await hasManagedClaudeHooks()) {
|
|
10053
|
+
await runHooksInstall(config, "claude", { apply: !dryRun, dryRun, remove: true });
|
|
10054
|
+
}
|
|
9758
10055
|
await removeCommandShim(dryRun);
|
|
9759
10056
|
await removeUserAgentInstructions(dryRun);
|
|
9760
10057
|
if (options.eraseMemories === true) {
|
|
@@ -9805,16 +10102,16 @@ async function repairManifest(config, dryRun) {
|
|
|
9805
10102
|
console.log(output2.trimEnd());
|
|
9806
10103
|
return;
|
|
9807
10104
|
}
|
|
9808
|
-
await ensureDirectory((0,
|
|
10105
|
+
await ensureDirectory((0, import_node_path9.dirname)(config.manifestPath), false);
|
|
9809
10106
|
const currentContent = await readFileIfExists(config.manifestPath);
|
|
9810
10107
|
if (currentContent !== void 0) {
|
|
9811
10108
|
const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
|
|
9812
|
-
await (0,
|
|
9813
|
-
await (0,
|
|
10109
|
+
await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
10110
|
+
await (0, import_promises10.chmod)(backupPath, 384);
|
|
9814
10111
|
console.log(`Backup: ${backupPath}`);
|
|
9815
10112
|
}
|
|
9816
|
-
await (0,
|
|
9817
|
-
await (0,
|
|
10113
|
+
await (0, import_promises10.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
10114
|
+
await (0, import_promises10.chmod)(config.manifestPath, 384);
|
|
9818
10115
|
console.log(`Wrote replacement manifest: ${config.manifestPath}`);
|
|
9819
10116
|
}
|
|
9820
10117
|
async function repairServerHealth(config, dryRun) {
|
|
@@ -9855,7 +10152,7 @@ async function runStart(config, options) {
|
|
|
9855
10152
|
);
|
|
9856
10153
|
}
|
|
9857
10154
|
const logPath = openVikingLogPath(config);
|
|
9858
|
-
await ensureDirectory((0,
|
|
10155
|
+
await ensureDirectory((0, import_node_path9.dirname)(logPath), false);
|
|
9859
10156
|
if (options.foreground === true) {
|
|
9860
10157
|
const result = await runInteractive(server, args);
|
|
9861
10158
|
process.exitCode = result;
|
|
@@ -9864,7 +10161,7 @@ async function runStart(config, options) {
|
|
|
9864
10161
|
const logFd = (0, import_node_fs5.openSync)(logPath, "a");
|
|
9865
10162
|
const child = spawnDetachedServerWithLog(server, args, logFd);
|
|
9866
10163
|
child.unref();
|
|
9867
|
-
await (0,
|
|
10164
|
+
await (0, import_promises10.writeFile)((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
9868
10165
|
`, "utf8");
|
|
9869
10166
|
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
9870
10167
|
if (health) {
|
|
@@ -9897,7 +10194,7 @@ async function runStop(config, options) {
|
|
|
9897
10194
|
console.log(`No LaunchAgent found: ${launchAgentPath}`);
|
|
9898
10195
|
}
|
|
9899
10196
|
}
|
|
9900
|
-
const pidPath = (0,
|
|
10197
|
+
const pidPath = (0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid");
|
|
9901
10198
|
const pidText = await readFileIfExists(pidPath);
|
|
9902
10199
|
if (!pidText) {
|
|
9903
10200
|
console.log("No pid file found for detached OpenViking server.");
|
|
@@ -9993,7 +10290,7 @@ async function pythonSystemCertificatesCheck() {
|
|
|
9993
10290
|
};
|
|
9994
10291
|
}
|
|
9995
10292
|
async function commandShimCheck() {
|
|
9996
|
-
const shimPath = (0,
|
|
10293
|
+
const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
9997
10294
|
const content = await readFileIfExists(shimPath);
|
|
9998
10295
|
if (content === void 0) {
|
|
9999
10296
|
return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
|
|
@@ -10059,11 +10356,11 @@ async function hasPythonModule(serverPath, moduleName) {
|
|
|
10059
10356
|
async function siblingPythonForExecutable(executablePath) {
|
|
10060
10357
|
let resolvedPath;
|
|
10061
10358
|
try {
|
|
10062
|
-
resolvedPath = await (0,
|
|
10359
|
+
resolvedPath = await (0, import_promises10.realpath)(executablePath);
|
|
10063
10360
|
} catch (_err) {
|
|
10064
10361
|
return void 0;
|
|
10065
10362
|
}
|
|
10066
|
-
const pythonPath = (0,
|
|
10363
|
+
const pythonPath = (0, import_node_path9.join)((0, import_node_path9.dirname)(resolvedPath), "python");
|
|
10067
10364
|
return await exists(pythonPath) ? pythonPath : void 0;
|
|
10068
10365
|
}
|
|
10069
10366
|
async function manifestCheck(path) {
|
|
@@ -10134,7 +10431,7 @@ async function offerToInstallUv() {
|
|
|
10134
10431
|
);
|
|
10135
10432
|
return false;
|
|
10136
10433
|
}
|
|
10137
|
-
const readline = (0,
|
|
10434
|
+
const readline = (0, import_promises11.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
10138
10435
|
let answer;
|
|
10139
10436
|
try {
|
|
10140
10437
|
answer = (await readline.question(
|
|
@@ -10257,38 +10554,38 @@ function printInstallNextSteps(options) {
|
|
|
10257
10554
|
}
|
|
10258
10555
|
async function writeTemplateIfMissing(options) {
|
|
10259
10556
|
if (await exists(options.destinationPath)) {
|
|
10260
|
-
const currentContent = await (0,
|
|
10557
|
+
const currentContent = await (0, import_promises10.readFile)(options.destinationPath, "utf8");
|
|
10261
10558
|
if (options.shouldRepair?.(currentContent) !== true) {
|
|
10262
10559
|
console.log(`Already exists: ${options.destinationPath}`);
|
|
10263
10560
|
return;
|
|
10264
10561
|
}
|
|
10265
|
-
const rendered2 = renderTemplate(await (0,
|
|
10562
|
+
const rendered2 = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
|
|
10266
10563
|
if (options.dryRun) {
|
|
10267
10564
|
console.log(`Would repair generated config: ${options.destinationPath}`);
|
|
10268
10565
|
return;
|
|
10269
10566
|
}
|
|
10270
10567
|
const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
|
|
10271
|
-
await (0,
|
|
10272
|
-
await (0,
|
|
10273
|
-
await (0,
|
|
10274
|
-
await (0,
|
|
10568
|
+
await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
10569
|
+
await (0, import_promises10.chmod)(backupPath, 384);
|
|
10570
|
+
await (0, import_promises10.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
|
|
10571
|
+
await (0, import_promises10.chmod)(options.destinationPath, 384);
|
|
10275
10572
|
console.log(`Repaired generated config: ${options.destinationPath}`);
|
|
10276
10573
|
console.log(`Backup: ${backupPath}`);
|
|
10277
10574
|
return;
|
|
10278
10575
|
}
|
|
10279
|
-
const rendered = renderTemplate(await (0,
|
|
10576
|
+
const rendered = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
|
|
10280
10577
|
if (options.dryRun) {
|
|
10281
10578
|
console.log(`Would write ${options.destinationPath}`);
|
|
10282
10579
|
return;
|
|
10283
10580
|
}
|
|
10284
|
-
await ensureDirectory((0,
|
|
10285
|
-
await (0,
|
|
10286
|
-
await (0,
|
|
10581
|
+
await ensureDirectory((0, import_node_path9.dirname)(options.destinationPath), false);
|
|
10582
|
+
await (0, import_promises10.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
|
|
10583
|
+
await (0, import_promises10.chmod)(options.destinationPath, 384);
|
|
10287
10584
|
console.log(`Wrote ${options.destinationPath}`);
|
|
10288
10585
|
}
|
|
10289
10586
|
async function installCommandShim(dryRun) {
|
|
10290
10587
|
const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
|
|
10291
|
-
const shimPath = (0,
|
|
10588
|
+
const shimPath = (0, import_node_path9.join)(binDir, "threadnote");
|
|
10292
10589
|
const existingContent = await readFileIfExists(shimPath);
|
|
10293
10590
|
if (existingContent && !isManagedCommandShim(existingContent)) {
|
|
10294
10591
|
console.log(`WARN not overwriting existing command shim: ${shimPath}`);
|
|
@@ -10304,12 +10601,12 @@ async function installCommandShim(dryRun) {
|
|
|
10304
10601
|
return;
|
|
10305
10602
|
}
|
|
10306
10603
|
await ensureDirectory(binDir, false);
|
|
10307
|
-
await (0,
|
|
10308
|
-
await (0,
|
|
10604
|
+
await (0, import_promises10.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
|
|
10605
|
+
await (0, import_promises10.chmod)(shimPath, 493);
|
|
10309
10606
|
console.log(`Wrote command shim: ${shimPath}`);
|
|
10310
10607
|
}
|
|
10311
10608
|
async function removeCommandShim(dryRun) {
|
|
10312
|
-
const shimPath = (0,
|
|
10609
|
+
const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
10313
10610
|
const content = await readFileIfExists(shimPath);
|
|
10314
10611
|
if (content === void 0) {
|
|
10315
10612
|
console.log(`Already absent: ${shimPath}`);
|
|
@@ -10343,8 +10640,8 @@ async function installUserAgentInstructions(dryRun) {
|
|
|
10343
10640
|
console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
|
|
10344
10641
|
continue;
|
|
10345
10642
|
}
|
|
10346
|
-
await ensureDirectory((0,
|
|
10347
|
-
await (0,
|
|
10643
|
+
await ensureDirectory((0, import_node_path9.dirname)(targetPath), false);
|
|
10644
|
+
await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
10348
10645
|
console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
|
|
10349
10646
|
}
|
|
10350
10647
|
}
|
|
@@ -10381,7 +10678,7 @@ async function removeUserAgentInstructions(dryRun) {
|
|
|
10381
10678
|
console.log(`Would update ${targetPath}`);
|
|
10382
10679
|
continue;
|
|
10383
10680
|
}
|
|
10384
|
-
await (0,
|
|
10681
|
+
await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
10385
10682
|
console.log(`Updated ${targetPath}`);
|
|
10386
10683
|
}
|
|
10387
10684
|
}
|
|
@@ -10401,7 +10698,7 @@ async function renderUserAgentInstructions(target) {
|
|
|
10401
10698
|
].join("\n");
|
|
10402
10699
|
}
|
|
10403
10700
|
async function renderUserAgentInstructionsBlock() {
|
|
10404
|
-
const instructions = (await (0,
|
|
10701
|
+
const instructions = (await (0, import_promises10.readFile)((0, import_node_path9.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
|
|
10405
10702
|
return `${USER_INSTRUCTIONS_START_MARKER}
|
|
10406
10703
|
${instructions}
|
|
10407
10704
|
${USER_INSTRUCTIONS_END_MARKER}`;
|
|
@@ -10479,9 +10776,9 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
10479
10776
|
if ((0, import_node_os6.platform)() !== "darwin") {
|
|
10480
10777
|
throw new Error("launchd autostart is only supported on macOS.");
|
|
10481
10778
|
}
|
|
10482
|
-
const source = (0,
|
|
10779
|
+
const source = (0, import_node_path9.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
|
|
10483
10780
|
const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
10484
|
-
const rendered = renderTemplate(await (0,
|
|
10781
|
+
const rendered = renderTemplate(await (0, import_promises10.readFile)(source, "utf8"), config);
|
|
10485
10782
|
if (dryRun) {
|
|
10486
10783
|
console.log(`Would write ${destination}`);
|
|
10487
10784
|
console.log(`Would run: launchctl unload ${destination}`);
|
|
@@ -10489,9 +10786,9 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
10489
10786
|
console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
|
|
10490
10787
|
return;
|
|
10491
10788
|
}
|
|
10492
|
-
await ensureDirectory((0,
|
|
10493
|
-
await ensureDirectory((0,
|
|
10494
|
-
await (0,
|
|
10789
|
+
await ensureDirectory((0, import_node_path9.dirname)(destination), false);
|
|
10790
|
+
await ensureDirectory((0, import_node_path9.dirname)(openVikingLogPath(config)), false);
|
|
10791
|
+
await (0, import_promises10.writeFile)(destination, rendered, "utf8");
|
|
10495
10792
|
await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
|
|
10496
10793
|
await maybeRun(false, "launchctl", ["load", destination]);
|
|
10497
10794
|
await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
|
|
@@ -10554,7 +10851,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
|
|
|
10554
10851
|
if (typeof parsed.default_user !== "string") {
|
|
10555
10852
|
return false;
|
|
10556
10853
|
}
|
|
10557
|
-
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0,
|
|
10854
|
+
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path9.join)(config.agentContextHome, "data")) {
|
|
10558
10855
|
return false;
|
|
10559
10856
|
}
|
|
10560
10857
|
return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
|
|
@@ -10582,9 +10879,20 @@ async function main() {
|
|
|
10582
10879
|
await runDoctor(config, options);
|
|
10583
10880
|
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
10584
10881
|
});
|
|
10585
|
-
program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).
|
|
10882
|
+
program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).option(
|
|
10883
|
+
"--with-hooks",
|
|
10884
|
+
"Also install agent-side hooks (Claude PreCompact + SessionStart) for deterministic handoff snapshots and context preload"
|
|
10885
|
+
).action(async (options) => {
|
|
10586
10886
|
const config = getRuntimeConfig(program2);
|
|
10587
10887
|
await runInstall(config, options);
|
|
10888
|
+
if (options.withHooks === true) {
|
|
10889
|
+
const dryRun = options.dryRun === true;
|
|
10890
|
+
for (const agent of ["claude", "codex", "cursor", "copilot"]) {
|
|
10891
|
+
console.log(`
|
|
10892
|
+
--- ${agent} hooks ---`);
|
|
10893
|
+
await runHooksInstall(config, agent, { apply: !dryRun, dryRun });
|
|
10894
|
+
}
|
|
10895
|
+
}
|
|
10588
10896
|
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
10589
10897
|
});
|
|
10590
10898
|
program2.command("update").description("Update the published Threadnote package, then repair local shims and MCP config").option("--check", "Only check whether a newer version is available").option("--dry-run", "Print update and repair commands without running them").option("--force", "Run package-manager update even if this version is already current").option("--registry <url>", "npm registry URL", process.env.THREADNOTE_NPM_REGISTRY).option("--runtime <runtime>", "auto, npm, bun, or deno", parseUpdateRuntime, "auto").option("--no-repair", "Skip threadnote repair after updating the package").option("--no-post-update", "Skip post-update migration prompts").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
|
|
@@ -10629,6 +10937,21 @@ async function main() {
|
|
|
10629
10937
|
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) => {
|
|
10630
10938
|
await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
|
|
10631
10939
|
});
|
|
10940
|
+
program2.command("install-hooks").description(
|
|
10941
|
+
"Install deterministic agent hooks (Claude PreCompact + SessionStart). Soft instruction files remain the cross-agent guidance surface; hooks add a deterministic safety net where the agent supports it."
|
|
10942
|
+
).argument("<agent>", "codex, claude, cursor, or copilot").option("--apply", "Actually modify the selected agent config").option("--dry-run", "Print the planned change without applying it").option("--remove", "Remove threadnote-managed hook entries instead of adding them").action(async (agent, options) => {
|
|
10943
|
+
await runHooksInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
|
|
10944
|
+
});
|
|
10945
|
+
program2.command("pre-compact-hook", { hidden: true }).description(
|
|
10946
|
+
"Hook entry point: store a handoff snapshot before context compaction. Used by `install-hooks claude`."
|
|
10947
|
+
).option("--dry-run", "Print the handoff payload without writing it").action(async (options) => {
|
|
10948
|
+
await runPreCompactHook(getRuntimeConfig(program2), options);
|
|
10949
|
+
});
|
|
10950
|
+
program2.command("session-start-hook", { hidden: true }).description(
|
|
10951
|
+
"Hook entry point: print the latest threadnote handoff/feature memory for the current repo so Claude can preload it."
|
|
10952
|
+
).option("--dry-run", "Print the planned ov command without running it").action(async (options) => {
|
|
10953
|
+
await runSessionStartHook(getRuntimeConfig(program2), options);
|
|
10954
|
+
});
|
|
10632
10955
|
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) => {
|
|
10633
10956
|
await runRemember(getRuntimeConfig(program2), options);
|
|
10634
10957
|
});
|
|
@@ -10674,7 +10997,13 @@ async function main() {
|
|
|
10674
10997
|
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) => {
|
|
10675
10998
|
await runShareSync(getRuntimeConfig(program2), options);
|
|
10676
10999
|
});
|
|
10677
|
-
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").
|
|
11000
|
+
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").option(
|
|
11001
|
+
"--preview",
|
|
11002
|
+
"Print the exact bytes that would land in the shared git repo (after frontmatter strip and scrubber redaction) without writing, committing, or pushing"
|
|
11003
|
+
).option(
|
|
11004
|
+
"--redact",
|
|
11005
|
+
"Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
|
|
11006
|
+
).action(async (uri, options) => {
|
|
10678
11007
|
await runSharePublish(getRuntimeConfig(program2), uri, options);
|
|
10679
11008
|
});
|
|
10680
11009
|
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) => {
|