threadnote 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/dist/mcp_server.cjs +143 -42
- package/dist/threadnote.cjs +412 -98
- package/docs/agent-instructions.md +5 -2
- package/docs/index.html +1 -1
- package/docs/share.md +18 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,8 @@ which memory to keep/update, which old handoffs to archive, and which exact dupl
|
|
|
43
43
|
|
|
44
44
|
**Still working on the same issue?**
|
|
45
45
|
Use `threadnote remember --replace <uri>` or `threadnote handoff --replace <uri>` to keep one current-state memory fresh
|
|
46
|
-
instead of accumulating near-duplicate progress notes.
|
|
46
|
+
instead of accumulating near-duplicate progress notes. Replacing a shared `durable/` URI updates that shared memory in
|
|
47
|
+
place and pushes the shared repo, so you do not need a separate `share publish` step.
|
|
47
48
|
|
|
48
49
|
## Memory Lifecycle
|
|
49
50
|
|
|
@@ -179,10 +180,15 @@ threadnote update
|
|
|
179
180
|
Check without changing anything:
|
|
180
181
|
|
|
181
182
|
```bash
|
|
183
|
+
threadnote version
|
|
182
184
|
threadnote update --check
|
|
183
185
|
threadnote update --dry-run
|
|
184
186
|
```
|
|
185
187
|
|
|
188
|
+
`threadnote version` prints the installed version, latest npm version, and GitHub release notes. When no update is
|
|
189
|
+
available, it shows the current release notes; when an update is available, it shows every newer version. `threadnote
|
|
190
|
+
update` prints the same full "What's new" diff when an update is available.
|
|
191
|
+
|
|
186
192
|
After updating, restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload.
|
|
187
193
|
|
|
188
194
|
Some releases include post-update memory migrations. `threadnote update` runs the new package's migration prompt after
|
|
@@ -333,8 +339,9 @@ This is it! Start working with your agents as usual. The agent will automaticall
|
|
|
333
339
|
- `install`: installs `openviking[local-embed]==0.3.12` if missing, creates `~/.openviking` config files if absent,
|
|
334
340
|
writes the command shim, upserts user-level agent instructions, and starts/checks OpenViking health by default. Use
|
|
335
341
|
`--no-start` to skip the health check.
|
|
342
|
+
- `version`: prints the installed Threadnote version, latest npm version, and release notes for newer GitHub releases.
|
|
336
343
|
- `update`: updates the published Threadnote package, then runs `repair` so shims and MCP config point at the new
|
|
337
|
-
version.
|
|
344
|
+
version. When an update is available, it prints release notes for the full version diff.
|
|
338
345
|
- `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude/Cursor/Copilot MCP configs
|
|
339
346
|
from the current checkout.
|
|
340
347
|
- `start`: starts `openviking-server` on `127.0.0.1:1933`.
|
|
@@ -347,8 +354,9 @@ This is it! Start working with your agents as usual. The agent will automaticall
|
|
|
347
354
|
reusable workflow guidance. Use `seed-skills --native` only after configuring a working VLM provider.
|
|
348
355
|
- `mcp-install codex|claude|cursor|copilot`: installs or prints OpenViking MCP configuration for Codex, Claude, Cursor,
|
|
349
356
|
or GitHub Copilot in VS Code.
|
|
350
|
-
- `remember`: stores a durable memory. Use `--replace <uri>` to store an updated memory and remove a superseded
|
|
351
|
-
after the new memory succeeds
|
|
357
|
+
- `remember`: stores a durable memory. Use `--replace <uri>` to store an updated memory and remove a superseded personal
|
|
358
|
+
memory after the new memory succeeds; if `<uri>` is shared, Threadnote updates that shared memory in place and pushes
|
|
359
|
+
the shared repo. Use `--kind`, `--project`, and `--topic` to store lifecycle-aware current knowledge.
|
|
352
360
|
- `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
|
|
353
361
|
`migrate-memories --dry-run` first; use `--all-accounts` when importing from older local OpenViking accounts.
|
|
354
362
|
- `migrate-lifecycle`: moves clear legacy handoff memories from the old events path into archived lifecycle handoff
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -28477,25 +28477,25 @@ var Protocol = class {
|
|
|
28477
28477
|
});
|
|
28478
28478
|
}
|
|
28479
28479
|
_resetTimeout(messageId) {
|
|
28480
|
-
const
|
|
28481
|
-
if (!
|
|
28480
|
+
const info2 = this._timeoutInfo.get(messageId);
|
|
28481
|
+
if (!info2)
|
|
28482
28482
|
return false;
|
|
28483
|
-
const totalElapsed = Date.now() -
|
|
28484
|
-
if (
|
|
28483
|
+
const totalElapsed = Date.now() - info2.startTime;
|
|
28484
|
+
if (info2.maxTotalTimeout && totalElapsed >= info2.maxTotalTimeout) {
|
|
28485
28485
|
this._timeoutInfo.delete(messageId);
|
|
28486
28486
|
throw McpError.fromError(ErrorCode.RequestTimeout, "Maximum total timeout exceeded", {
|
|
28487
|
-
maxTotalTimeout:
|
|
28487
|
+
maxTotalTimeout: info2.maxTotalTimeout,
|
|
28488
28488
|
totalElapsed
|
|
28489
28489
|
});
|
|
28490
28490
|
}
|
|
28491
|
-
clearTimeout(
|
|
28492
|
-
|
|
28491
|
+
clearTimeout(info2.timeoutId);
|
|
28492
|
+
info2.timeoutId = setTimeout(info2.onTimeout, info2.timeout);
|
|
28493
28493
|
return true;
|
|
28494
28494
|
}
|
|
28495
28495
|
_cleanupTimeout(messageId) {
|
|
28496
|
-
const
|
|
28497
|
-
if (
|
|
28498
|
-
clearTimeout(
|
|
28496
|
+
const info2 = this._timeoutInfo.get(messageId);
|
|
28497
|
+
if (info2) {
|
|
28498
|
+
clearTimeout(info2.timeoutId);
|
|
28499
28499
|
this._timeoutInfo.delete(messageId);
|
|
28500
28500
|
}
|
|
28501
28501
|
}
|
|
@@ -28540,8 +28540,8 @@ var Protocol = class {
|
|
|
28540
28540
|
this._progressHandlers.clear();
|
|
28541
28541
|
this._taskProgressTokens.clear();
|
|
28542
28542
|
this._pendingDebouncedNotifications.clear();
|
|
28543
|
-
for (const
|
|
28544
|
-
clearTimeout(
|
|
28543
|
+
for (const info2 of this._timeoutInfo.values()) {
|
|
28544
|
+
clearTimeout(info2.timeoutId);
|
|
28545
28545
|
}
|
|
28546
28546
|
this._timeoutInfo.clear();
|
|
28547
28547
|
for (const controller of this._requestHandlerAbortControllers.values()) {
|
|
@@ -30036,8 +30036,8 @@ function validateToolName(name) {
|
|
|
30036
30036
|
function issueToolNameWarning(name, warnings) {
|
|
30037
30037
|
if (warnings.length > 0) {
|
|
30038
30038
|
console.warn(`Tool name validation warning for "${name}":`);
|
|
30039
|
-
for (const
|
|
30040
|
-
console.warn(` - ${
|
|
30039
|
+
for (const warning2 of warnings) {
|
|
30040
|
+
console.warn(` - ${warning2}`);
|
|
30041
30041
|
}
|
|
30042
30042
|
console.warn("Tool registration will proceed, but this may cause compatibility issues.");
|
|
30043
30043
|
console.warn("Consider updating the tool name to conform to the MCP tool naming standard.");
|
|
@@ -33591,6 +33591,39 @@ var import_node_crypto = require("node:crypto");
|
|
|
33591
33591
|
var import_promises = require("node:fs/promises");
|
|
33592
33592
|
var import_node_os = require("node:os");
|
|
33593
33593
|
var import_node_path = require("node:path");
|
|
33594
|
+
|
|
33595
|
+
// src/cli_ui.ts
|
|
33596
|
+
var import_node_process2 = require("node:process");
|
|
33597
|
+
var ANSI = {
|
|
33598
|
+
blue: "\x1B[34m",
|
|
33599
|
+
bold: "\x1B[1m",
|
|
33600
|
+
cyan: "\x1B[36m",
|
|
33601
|
+
dim: "\x1B[2m",
|
|
33602
|
+
green: "\x1B[32m",
|
|
33603
|
+
red: "\x1B[31m",
|
|
33604
|
+
reset: "\x1B[0m",
|
|
33605
|
+
yellow: "\x1B[33m"
|
|
33606
|
+
};
|
|
33607
|
+
function color(name, text) {
|
|
33608
|
+
if (!shouldUseColor()) {
|
|
33609
|
+
return text;
|
|
33610
|
+
}
|
|
33611
|
+
return `${ANSI[name]}${text}${ANSI.reset}`;
|
|
33612
|
+
}
|
|
33613
|
+
function command(text) {
|
|
33614
|
+
return color("cyan", text);
|
|
33615
|
+
}
|
|
33616
|
+
function info(text) {
|
|
33617
|
+
return color("cyan", text);
|
|
33618
|
+
}
|
|
33619
|
+
function warning(text) {
|
|
33620
|
+
return color("yellow", text);
|
|
33621
|
+
}
|
|
33622
|
+
function shouldUseColor() {
|
|
33623
|
+
return import_node_process2.stdout.isTTY === true && process.env.NO_COLOR === void 0 && process.env.CI === void 0 && process.env.TERM !== "dumb";
|
|
33624
|
+
}
|
|
33625
|
+
|
|
33626
|
+
// src/utils.ts
|
|
33594
33627
|
function isJsonObject(value) {
|
|
33595
33628
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33596
33629
|
}
|
|
@@ -33609,11 +33642,11 @@ function redactText(content) {
|
|
|
33609
33642
|
).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh_[REDACTED]");
|
|
33610
33643
|
}
|
|
33611
33644
|
async function requiredOpenVikingCli() {
|
|
33612
|
-
const
|
|
33613
|
-
if (!
|
|
33645
|
+
const command2 = await findExecutable(["ov", "openviking"]);
|
|
33646
|
+
if (!command2) {
|
|
33614
33647
|
throw new Error("Neither ov nor openviking was found in PATH. Run threadnote install first.");
|
|
33615
33648
|
}
|
|
33616
|
-
return
|
|
33649
|
+
return command2;
|
|
33617
33650
|
}
|
|
33618
33651
|
async function openVikingCliForMode(dryRun) {
|
|
33619
33652
|
if (dryRun) {
|
|
@@ -33621,16 +33654,16 @@ async function openVikingCliForMode(dryRun) {
|
|
|
33621
33654
|
}
|
|
33622
33655
|
return requiredOpenVikingCli();
|
|
33623
33656
|
}
|
|
33624
|
-
async function requiredExecutable(
|
|
33625
|
-
const executable = await findExecutable([
|
|
33657
|
+
async function requiredExecutable(command2) {
|
|
33658
|
+
const executable = await findExecutable([command2]);
|
|
33626
33659
|
if (!executable) {
|
|
33627
|
-
throw new Error(`${
|
|
33660
|
+
throw new Error(`${command2} was not found in PATH.`);
|
|
33628
33661
|
}
|
|
33629
33662
|
return executable;
|
|
33630
33663
|
}
|
|
33631
33664
|
async function findExecutable(commands) {
|
|
33632
|
-
for (const
|
|
33633
|
-
const result = await runCommand("which", [
|
|
33665
|
+
for (const command2 of commands) {
|
|
33666
|
+
const result = await runCommand("which", [command2], { allowFailure: true });
|
|
33634
33667
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
33635
33668
|
return result.stdout.trim();
|
|
33636
33669
|
}
|
|
@@ -33639,7 +33672,8 @@ async function findExecutable(commands) {
|
|
|
33639
33672
|
}
|
|
33640
33673
|
async function maybeRun(dryRun, executable, args, options = {}) {
|
|
33641
33674
|
const cwdSuffix = options.cwd ? ` (cwd: ${options.cwd})` : "";
|
|
33642
|
-
|
|
33675
|
+
const label = dryRun ? warning("Would run") : info("Running");
|
|
33676
|
+
console.log(`${label}: ${command(formatShellCommand(executable, args))}${cwdSuffix}`);
|
|
33643
33677
|
if (dryRun) {
|
|
33644
33678
|
return void 0;
|
|
33645
33679
|
}
|
|
@@ -34445,9 +34479,9 @@ async function syncSharedReposBeforeAgentRead(config2) {
|
|
|
34445
34479
|
const remainingBehind = new Set(state.behindTeams);
|
|
34446
34480
|
for (const team of syncTeams) {
|
|
34447
34481
|
try {
|
|
34448
|
-
const
|
|
34449
|
-
if (
|
|
34450
|
-
warnings.push(
|
|
34482
|
+
const warning2 = await runShareSyncQuiet(config2, state, { team });
|
|
34483
|
+
if (warning2) {
|
|
34484
|
+
warnings.push(warning2);
|
|
34451
34485
|
} else {
|
|
34452
34486
|
remainingBehind.delete(team);
|
|
34453
34487
|
syncedTeams.push(team);
|
|
@@ -34529,8 +34563,8 @@ async function refreshShareUpdateState(config2, options) {
|
|
|
34529
34563
|
state,
|
|
34530
34564
|
async () => refreshShareUpdateStateLocked(config2, state, options)
|
|
34531
34565
|
);
|
|
34532
|
-
for (const
|
|
34533
|
-
console.error(
|
|
34566
|
+
for (const warning2 of warnings) {
|
|
34567
|
+
console.error(warning2);
|
|
34534
34568
|
}
|
|
34535
34569
|
}
|
|
34536
34570
|
async function refreshShareUpdateStateLocked(config2, state, options) {
|
|
@@ -34726,7 +34760,35 @@ function workfileToVikingUri(config2, team, filePath) {
|
|
|
34726
34760
|
return `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
34727
34761
|
}
|
|
34728
34762
|
function isInSharedNamespace(config2, uri) {
|
|
34729
|
-
return
|
|
34763
|
+
return sharedTeamNameForUri(config2, uri) !== void 0;
|
|
34764
|
+
}
|
|
34765
|
+
function sharedTeamNameForUri(config2, uri) {
|
|
34766
|
+
const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
|
|
34767
|
+
if (!uri.startsWith(prefix)) {
|
|
34768
|
+
return void 0;
|
|
34769
|
+
}
|
|
34770
|
+
const [team] = uri.slice(prefix.length).split("/");
|
|
34771
|
+
return team || void 0;
|
|
34772
|
+
}
|
|
34773
|
+
function sharedMemoryUriParts(config2, uri) {
|
|
34774
|
+
const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
|
|
34775
|
+
if (!uri.startsWith(prefix)) {
|
|
34776
|
+
return void 0;
|
|
34777
|
+
}
|
|
34778
|
+
const [team, kind, scope, project, ...topicParts] = uri.slice(prefix.length).split("/");
|
|
34779
|
+
if (!team) {
|
|
34780
|
+
return void 0;
|
|
34781
|
+
}
|
|
34782
|
+
if (kind !== "durable" || scope !== "projects" || !project || topicParts.length === 0) {
|
|
34783
|
+
return { team };
|
|
34784
|
+
}
|
|
34785
|
+
const topicPath = topicParts.join("/");
|
|
34786
|
+
return {
|
|
34787
|
+
kind,
|
|
34788
|
+
project,
|
|
34789
|
+
team,
|
|
34790
|
+
topic: topicPath.endsWith(".md") ? topicPath.slice(0, -".md".length) : topicPath
|
|
34791
|
+
};
|
|
34730
34792
|
}
|
|
34731
34793
|
function sharedUriFor(config2, personalUri, team) {
|
|
34732
34794
|
const prefix = `viking://user/${uriSegment(config2.user)}/memories/`;
|
|
@@ -34889,14 +34951,14 @@ async function waitForOvQueue(ov, config2, options = {}) {
|
|
|
34889
34951
|
console.error(result.stderr.trim());
|
|
34890
34952
|
}
|
|
34891
34953
|
}
|
|
34892
|
-
function isTransientOvFailure(stderr,
|
|
34954
|
+
function isTransientOvFailure(stderr, stdout2) {
|
|
34893
34955
|
const output = `${stderr}
|
|
34894
|
-
${
|
|
34956
|
+
${stdout2}`.toLowerCase();
|
|
34895
34957
|
return output.includes("resource is busy") || output.includes("resource is being processed") || output.includes("network error") || output.includes("error sending request") || output.includes("http request failed") || output.includes("connection refused") || output.includes("connection reset") || output.includes("timed out");
|
|
34896
34958
|
}
|
|
34897
|
-
function isResourceBusyFailure(stderr,
|
|
34959
|
+
function isResourceBusyFailure(stderr, stdout2) {
|
|
34898
34960
|
const output = `${stderr}
|
|
34899
|
-
${
|
|
34961
|
+
${stdout2}`.toLowerCase();
|
|
34900
34962
|
return output.includes("resource is busy") || output.includes("resource is being processed");
|
|
34901
34963
|
}
|
|
34902
34964
|
async function ingestSingleFile(ov, config2, uri, filePath, initialMode, options = {}) {
|
|
@@ -35400,8 +35462,8 @@ ${exactMatches}`);
|
|
|
35400
35462
|
if (syncedTeams.length > 0) {
|
|
35401
35463
|
sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
|
|
35402
35464
|
}
|
|
35403
|
-
for (const
|
|
35404
|
-
sections.push(`Auto-sync warning: ${
|
|
35465
|
+
for (const warning2 of syncWarnings) {
|
|
35466
|
+
sections.push(`Auto-sync warning: ${warning2}`);
|
|
35405
35467
|
}
|
|
35406
35468
|
if (sections.length <= 1) {
|
|
35407
35469
|
return semanticResult;
|
|
@@ -35499,7 +35561,7 @@ function registerReadTool(server, config2, name, description) {
|
|
|
35499
35561
|
}
|
|
35500
35562
|
const syncMessages = [
|
|
35501
35563
|
syncedTeams.length > 0 ? `Auto-synced shared memories: ${syncedTeams.join(", ")}` : void 0,
|
|
35502
|
-
...syncWarnings.map((
|
|
35564
|
+
...syncWarnings.map((warning2) => `Auto-sync warning: ${warning2}`)
|
|
35503
35565
|
].filter((part) => part !== void 0);
|
|
35504
35566
|
return {
|
|
35505
35567
|
...result,
|
|
@@ -35547,7 +35609,9 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
35547
35609
|
inputSchema: {
|
|
35548
35610
|
kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional().describe("Memory lifecycle kind; durable facts and handoffs are most common"),
|
|
35549
35611
|
project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
|
|
35550
|
-
replaceUri: external_exports.string().optional().describe(
|
|
35612
|
+
replaceUri: external_exports.string().optional().describe(
|
|
35613
|
+
"Optional viking:// memory URI to replace. Shared URIs are updated in place and pushed; personal URIs are forgotten after the replacement is safely stored."
|
|
35614
|
+
),
|
|
35551
35615
|
text: external_exports.string().optional().describe("Required memory text to store"),
|
|
35552
35616
|
sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, copilot, codex, or claude"),
|
|
35553
35617
|
status: external_exports.enum(["active", "archived", "superseded"]).optional().describe("Memory lifecycle status"),
|
|
@@ -35866,6 +35930,9 @@ async function readTextIfExists(path) {
|
|
|
35866
35930
|
async function writeDurableMemory(config2, params) {
|
|
35867
35931
|
try {
|
|
35868
35932
|
const ov = await requiredOpenVikingCli2();
|
|
35933
|
+
if (params.replaceUri && isInSharedNamespace(config2, params.replaceUri)) {
|
|
35934
|
+
return await writeSharedMemoryReplacement(config2, ov, params, params.replaceUri);
|
|
35935
|
+
}
|
|
35869
35936
|
const directoryUri = memoryDirectoryUri(config2, params.metadata);
|
|
35870
35937
|
await ensureMemoryDirectory(ov, config2, directoryUri);
|
|
35871
35938
|
const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
|
|
@@ -35906,6 +35973,40 @@ async function writeDurableMemory(config2, params) {
|
|
|
35906
35973
|
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
35907
35974
|
}
|
|
35908
35975
|
}
|
|
35976
|
+
async function writeSharedMemoryReplacement(config2, ov, params, targetUri) {
|
|
35977
|
+
if (params.metadata.kind !== "durable") {
|
|
35978
|
+
return argumentError("Shared memory replacement only supports durable memories.");
|
|
35979
|
+
}
|
|
35980
|
+
const teamName = sharedTeamNameForUri(config2, targetUri);
|
|
35981
|
+
if (!teamName) {
|
|
35982
|
+
return argumentError(`Memory ${targetUri} is not in the shared namespace.`);
|
|
35983
|
+
}
|
|
35984
|
+
const resolved = await resolveTeam(config2, teamName);
|
|
35985
|
+
const inferred = sharedMemoryUriParts(config2, targetUri);
|
|
35986
|
+
const metadata = {
|
|
35987
|
+
...params.metadata,
|
|
35988
|
+
project: params.metadata.project ?? inferred?.project,
|
|
35989
|
+
topic: params.metadata.topic ?? inferred?.topic
|
|
35990
|
+
};
|
|
35991
|
+
const rawMemory = formatMemoryDocument2("MEMORY", metadata, params.bodyText);
|
|
35992
|
+
const scrub = applyScrubber(stripPersonalProvenance(rawMemory), { redact: false });
|
|
35993
|
+
if (scrub.blocker) {
|
|
35994
|
+
return argumentError(
|
|
35995
|
+
`Refusing to update shared memory ${targetUri}: possible ${scrub.blocker}. Strip the sensitive value first.`
|
|
35996
|
+
);
|
|
35997
|
+
}
|
|
35998
|
+
await ensureSharedDirectoryChain(config2, ov, targetUri, false, { quiet: true });
|
|
35999
|
+
await writeMemoryFile(config2, ov, targetUri, scrub.cleaned, "replace", false, { quiet: true });
|
|
36000
|
+
const relativePath = vikingUriToWorktreeRelative(config2, targetUri, resolved.name);
|
|
36001
|
+
const messages = [`Updated shared memory: ${targetUri}`];
|
|
36002
|
+
for (const redaction of scrub.redactions) {
|
|
36003
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
|
|
36004
|
+
}
|
|
36005
|
+
messages.push(
|
|
36006
|
+
...await gitPublishWorkflow(resolved.config.worktree, relativePath, `share: update ${relativePath}`, true)
|
|
36007
|
+
);
|
|
36008
|
+
return { content: [{ type: "text", text: messages.join("\n") }] };
|
|
36009
|
+
}
|
|
35909
36010
|
async function runOpenVikingWriteWithRetry(ov, config2, memoryUri, args) {
|
|
35910
36011
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
35911
36012
|
const result = await runCommand(ov, args, { allowFailure: true });
|
|
@@ -35944,9 +36045,9 @@ async function removeVikingResourceWithRetry(ov, config2, uri) {
|
|
|
35944
36045
|
}
|
|
35945
36046
|
return false;
|
|
35946
36047
|
}
|
|
35947
|
-
function isResourceBusy(stderr,
|
|
36048
|
+
function isResourceBusy(stderr, stdout2) {
|
|
35948
36049
|
const output = `${stderr}
|
|
35949
|
-
${
|
|
36050
|
+
${stdout2}`.toLowerCase();
|
|
35950
36051
|
return output.includes("resource is busy") || output.includes("resource is being processed");
|
|
35951
36052
|
}
|
|
35952
36053
|
async function ensureMemoryDirectory(ov, config2, directoryUri) {
|
|
@@ -36224,11 +36325,11 @@ function withIdentity2(config2, args) {
|
|
|
36224
36325
|
return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
|
|
36225
36326
|
}
|
|
36226
36327
|
async function requiredOpenVikingCli2() {
|
|
36227
|
-
const
|
|
36228
|
-
if (!
|
|
36328
|
+
const command2 = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "ov"), (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "openviking")]);
|
|
36329
|
+
if (!command2) {
|
|
36229
36330
|
throw new Error("Neither ov nor openviking was found. Run threadnote install first.");
|
|
36230
36331
|
}
|
|
36231
|
-
return
|
|
36332
|
+
return command2;
|
|
36232
36333
|
}
|
|
36233
36334
|
async function firstExistingPath(paths) {
|
|
36234
36335
|
for (const path of paths) {
|
package/dist/threadnote.cjs
CHANGED
|
@@ -347,11 +347,11 @@ var require_help = __commonJS({
|
|
|
347
347
|
* @returns {number}
|
|
348
348
|
*/
|
|
349
349
|
longestSubcommandTermLength(cmd, helper) {
|
|
350
|
-
return helper.visibleCommands(cmd).reduce((max,
|
|
350
|
+
return helper.visibleCommands(cmd).reduce((max, command2) => {
|
|
351
351
|
return Math.max(
|
|
352
352
|
max,
|
|
353
353
|
this.displayWidth(
|
|
354
|
-
helper.styleSubcommandTerm(helper.subcommandTerm(
|
|
354
|
+
helper.styleSubcommandTerm(helper.subcommandTerm(command2))
|
|
355
355
|
)
|
|
356
356
|
);
|
|
357
357
|
}, 0);
|
|
@@ -512,9 +512,9 @@ var require_help = __commonJS({
|
|
|
512
512
|
* @param {Help} helper
|
|
513
513
|
* @returns string[]
|
|
514
514
|
*/
|
|
515
|
-
formatItemList(
|
|
515
|
+
formatItemList(heading2, items, helper) {
|
|
516
516
|
if (items.length === 0) return [];
|
|
517
|
-
return [helper.styleTitle(
|
|
517
|
+
return [helper.styleTitle(heading2), ...items, ""];
|
|
518
518
|
}
|
|
519
519
|
/**
|
|
520
520
|
* Group items by their help group heading.
|
|
@@ -993,8 +993,8 @@ var require_option = __commonJS({
|
|
|
993
993
|
* @param {string} heading
|
|
994
994
|
* @return {Option}
|
|
995
995
|
*/
|
|
996
|
-
helpGroup(
|
|
997
|
-
this.helpGroupHeading =
|
|
996
|
+
helpGroup(heading2) {
|
|
997
|
+
this.helpGroupHeading = heading2;
|
|
998
998
|
return this;
|
|
999
999
|
}
|
|
1000
1000
|
/**
|
|
@@ -1284,8 +1284,8 @@ var require_command = __commonJS({
|
|
|
1284
1284
|
*/
|
|
1285
1285
|
_getCommandAndAncestors() {
|
|
1286
1286
|
const result = [];
|
|
1287
|
-
for (let
|
|
1288
|
-
result.push(
|
|
1287
|
+
for (let command2 = this; command2; command2 = command2.parent) {
|
|
1288
|
+
result.push(command2);
|
|
1289
1289
|
}
|
|
1290
1290
|
return result;
|
|
1291
1291
|
}
|
|
@@ -1726,22 +1726,22 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1726
1726
|
* @param {Command} command
|
|
1727
1727
|
* @private
|
|
1728
1728
|
*/
|
|
1729
|
-
_registerCommand(
|
|
1729
|
+
_registerCommand(command2) {
|
|
1730
1730
|
const knownBy = (cmd) => {
|
|
1731
1731
|
return [cmd.name()].concat(cmd.aliases());
|
|
1732
1732
|
};
|
|
1733
|
-
const alreadyUsed = knownBy(
|
|
1733
|
+
const alreadyUsed = knownBy(command2).find(
|
|
1734
1734
|
(name) => this._findCommand(name)
|
|
1735
1735
|
);
|
|
1736
1736
|
if (alreadyUsed) {
|
|
1737
1737
|
const existingCmd = knownBy(this._findCommand(alreadyUsed)).join("|");
|
|
1738
|
-
const newCmd = knownBy(
|
|
1738
|
+
const newCmd = knownBy(command2).join("|");
|
|
1739
1739
|
throw new Error(
|
|
1740
1740
|
`cannot add command '${newCmd}' as already have command '${existingCmd}'`
|
|
1741
1741
|
);
|
|
1742
1742
|
}
|
|
1743
|
-
this._initCommandGroup(
|
|
1744
|
-
this.commands.push(
|
|
1743
|
+
this._initCommandGroup(command2);
|
|
1744
|
+
this.commands.push(command2);
|
|
1745
1745
|
}
|
|
1746
1746
|
/**
|
|
1747
1747
|
* Add an option.
|
|
@@ -2913,12 +2913,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2913
2913
|
let suggestion = "";
|
|
2914
2914
|
if (flag.startsWith("--") && this._showSuggestionAfterError) {
|
|
2915
2915
|
let candidateFlags = [];
|
|
2916
|
-
let
|
|
2916
|
+
let command2 = this;
|
|
2917
2917
|
do {
|
|
2918
|
-
const moreFlags =
|
|
2918
|
+
const moreFlags = command2.createHelp().visibleOptions(command2).filter((option) => option.long).map((option) => option.long);
|
|
2919
2919
|
candidateFlags = candidateFlags.concat(moreFlags);
|
|
2920
|
-
|
|
2921
|
-
} while (
|
|
2920
|
+
command2 = command2.parent;
|
|
2921
|
+
} while (command2 && !command2._enablePositionalOptions);
|
|
2922
2922
|
suggestion = suggestSimilar(flag, candidateFlags);
|
|
2923
2923
|
}
|
|
2924
2924
|
const message = `error: unknown option '${flag}'${suggestion}`;
|
|
@@ -2948,9 +2948,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2948
2948
|
let suggestion = "";
|
|
2949
2949
|
if (this._showSuggestionAfterError) {
|
|
2950
2950
|
const candidateNames = [];
|
|
2951
|
-
this.createHelp().visibleCommands(this).forEach((
|
|
2952
|
-
candidateNames.push(
|
|
2953
|
-
if (
|
|
2951
|
+
this.createHelp().visibleCommands(this).forEach((command2) => {
|
|
2952
|
+
candidateNames.push(command2.name());
|
|
2953
|
+
if (command2.alias()) candidateNames.push(command2.alias());
|
|
2954
2954
|
});
|
|
2955
2955
|
suggestion = suggestSimilar(unknownName, candidateNames);
|
|
2956
2956
|
}
|
|
@@ -3021,11 +3021,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3021
3021
|
*/
|
|
3022
3022
|
alias(alias) {
|
|
3023
3023
|
if (alias === void 0) return this._aliases[0];
|
|
3024
|
-
let
|
|
3024
|
+
let command2 = this;
|
|
3025
3025
|
if (this.commands.length !== 0 && this.commands[this.commands.length - 1]._executableHandler) {
|
|
3026
|
-
|
|
3026
|
+
command2 = this.commands[this.commands.length - 1];
|
|
3027
3027
|
}
|
|
3028
|
-
if (alias ===
|
|
3028
|
+
if (alias === command2._name)
|
|
3029
3029
|
throw new Error("Command alias can't be the same as its name");
|
|
3030
3030
|
const matchingCommand = this.parent?._findCommand(alias);
|
|
3031
3031
|
if (matchingCommand) {
|
|
@@ -3034,7 +3034,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3034
3034
|
`cannot add alias '${alias}' to command '${this.name()}' as already have command '${existingCmd}'`
|
|
3035
3035
|
);
|
|
3036
3036
|
}
|
|
3037
|
-
|
|
3037
|
+
command2._aliases.push(alias);
|
|
3038
3038
|
return this;
|
|
3039
3039
|
}
|
|
3040
3040
|
/**
|
|
@@ -3088,9 +3088,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3088
3088
|
* @param {string} [heading]
|
|
3089
3089
|
* @return {Command | string}
|
|
3090
3090
|
*/
|
|
3091
|
-
helpGroup(
|
|
3092
|
-
if (
|
|
3093
|
-
this._helpGroupHeading =
|
|
3091
|
+
helpGroup(heading2) {
|
|
3092
|
+
if (heading2 === void 0) return this._helpGroupHeading ?? "";
|
|
3093
|
+
this._helpGroupHeading = heading2;
|
|
3094
3094
|
return this;
|
|
3095
3095
|
}
|
|
3096
3096
|
/**
|
|
@@ -3106,9 +3106,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3106
3106
|
* @param {string} [heading]
|
|
3107
3107
|
* @returns {Command | string}
|
|
3108
3108
|
*/
|
|
3109
|
-
commandsGroup(
|
|
3110
|
-
if (
|
|
3111
|
-
this._defaultCommandGroup =
|
|
3109
|
+
commandsGroup(heading2) {
|
|
3110
|
+
if (heading2 === void 0) return this._defaultCommandGroup ?? "";
|
|
3111
|
+
this._defaultCommandGroup = heading2;
|
|
3112
3112
|
return this;
|
|
3113
3113
|
}
|
|
3114
3114
|
/**
|
|
@@ -3124,9 +3124,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3124
3124
|
* @param {string} [heading]
|
|
3125
3125
|
* @returns {Command | string}
|
|
3126
3126
|
*/
|
|
3127
|
-
optionsGroup(
|
|
3128
|
-
if (
|
|
3129
|
-
this._defaultOptionGroup =
|
|
3127
|
+
optionsGroup(heading2) {
|
|
3128
|
+
if (heading2 === void 0) return this._defaultOptionGroup ?? "";
|
|
3129
|
+
this._defaultOptionGroup = heading2;
|
|
3130
3130
|
return this;
|
|
3131
3131
|
}
|
|
3132
3132
|
/**
|
|
@@ -3246,7 +3246,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3246
3246
|
write: outputContext.write,
|
|
3247
3247
|
command: this
|
|
3248
3248
|
};
|
|
3249
|
-
this._getCommandAndAncestors().reverse().forEach((
|
|
3249
|
+
this._getCommandAndAncestors().reverse().forEach((command2) => command2.emit("beforeAllHelp", eventContext));
|
|
3250
3250
|
this.emit("beforeHelp", eventContext);
|
|
3251
3251
|
let helpInformation = this.helpInformation({ error: outputContext.error });
|
|
3252
3252
|
if (deprecatedCallback) {
|
|
@@ -3261,7 +3261,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3261
3261
|
}
|
|
3262
3262
|
this.emit("afterHelp", eventContext);
|
|
3263
3263
|
this._getCommandAndAncestors().forEach(
|
|
3264
|
-
(
|
|
3264
|
+
(command2) => command2.emit("afterAllHelp", eventContext)
|
|
3265
3265
|
);
|
|
3266
3266
|
}
|
|
3267
3267
|
/**
|
|
@@ -3541,6 +3541,83 @@ var import_node_http = require("node:http");
|
|
|
3541
3541
|
var import_node_net = require("node:net");
|
|
3542
3542
|
var import_node_os = require("node:os");
|
|
3543
3543
|
var import_node_path = require("node:path");
|
|
3544
|
+
|
|
3545
|
+
// src/cli_ui.ts
|
|
3546
|
+
var import_node_readline = require("node:readline");
|
|
3547
|
+
var import_node_process = require("node:process");
|
|
3548
|
+
var ANSI = {
|
|
3549
|
+
blue: "\x1B[34m",
|
|
3550
|
+
bold: "\x1B[1m",
|
|
3551
|
+
cyan: "\x1B[36m",
|
|
3552
|
+
dim: "\x1B[2m",
|
|
3553
|
+
green: "\x1B[32m",
|
|
3554
|
+
red: "\x1B[31m",
|
|
3555
|
+
reset: "\x1B[0m",
|
|
3556
|
+
yellow: "\x1B[33m"
|
|
3557
|
+
};
|
|
3558
|
+
function color(name, text) {
|
|
3559
|
+
if (!shouldUseColor()) {
|
|
3560
|
+
return text;
|
|
3561
|
+
}
|
|
3562
|
+
return `${ANSI[name]}${text}${ANSI.reset}`;
|
|
3563
|
+
}
|
|
3564
|
+
function bold(text) {
|
|
3565
|
+
if (!shouldUseColor()) {
|
|
3566
|
+
return text;
|
|
3567
|
+
}
|
|
3568
|
+
return `${ANSI.bold}${text}${ANSI.reset}`;
|
|
3569
|
+
}
|
|
3570
|
+
function command(text) {
|
|
3571
|
+
return color("cyan", text);
|
|
3572
|
+
}
|
|
3573
|
+
function heading(text) {
|
|
3574
|
+
return bold(text);
|
|
3575
|
+
}
|
|
3576
|
+
function info(text) {
|
|
3577
|
+
return color("cyan", text);
|
|
3578
|
+
}
|
|
3579
|
+
function muted(text) {
|
|
3580
|
+
return color("dim", text);
|
|
3581
|
+
}
|
|
3582
|
+
function success(text) {
|
|
3583
|
+
return color("green", text);
|
|
3584
|
+
}
|
|
3585
|
+
function warning(text) {
|
|
3586
|
+
return color("yellow", text);
|
|
3587
|
+
}
|
|
3588
|
+
function failure(text) {
|
|
3589
|
+
return color("red", text);
|
|
3590
|
+
}
|
|
3591
|
+
function keyValue(label, value) {
|
|
3592
|
+
return `${label}: ${value}`;
|
|
3593
|
+
}
|
|
3594
|
+
function shouldUseColor() {
|
|
3595
|
+
return import_node_process.stdout.isTTY === true && process.env.NO_COLOR === void 0 && process.env.CI === void 0 && process.env.TERM !== "dumb";
|
|
3596
|
+
}
|
|
3597
|
+
async function withSpinner(message, action) {
|
|
3598
|
+
if (import_node_process.stdout.isTTY !== true || process.env.CI !== void 0 || process.env.THREADNOTE_NO_SPINNER !== void 0) {
|
|
3599
|
+
return action();
|
|
3600
|
+
}
|
|
3601
|
+
const frames = ["-", "\\", "|", "/"];
|
|
3602
|
+
let frameIndex = 0;
|
|
3603
|
+
const render = () => {
|
|
3604
|
+
(0, import_node_readline.clearLine)(import_node_process.stdout, 0);
|
|
3605
|
+
(0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
|
|
3606
|
+
import_node_process.stdout.write(`${muted(frames[frameIndex])} ${message}`);
|
|
3607
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
3608
|
+
};
|
|
3609
|
+
render();
|
|
3610
|
+
const timer = setInterval(render, 100);
|
|
3611
|
+
try {
|
|
3612
|
+
return await action();
|
|
3613
|
+
} finally {
|
|
3614
|
+
clearInterval(timer);
|
|
3615
|
+
(0, import_node_readline.clearLine)(import_node_process.stdout, 0);
|
|
3616
|
+
(0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// src/utils.ts
|
|
3544
3621
|
function isJsonObject(value) {
|
|
3545
3622
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3546
3623
|
}
|
|
@@ -3630,11 +3707,11 @@ function escapeRegExp(value) {
|
|
|
3630
3707
|
return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
|
|
3631
3708
|
}
|
|
3632
3709
|
async function requiredOpenVikingCli() {
|
|
3633
|
-
const
|
|
3634
|
-
if (!
|
|
3710
|
+
const command2 = await findExecutable(["ov", "openviking"]);
|
|
3711
|
+
if (!command2) {
|
|
3635
3712
|
throw new Error("Neither ov nor openviking was found in PATH. Run threadnote install first.");
|
|
3636
3713
|
}
|
|
3637
|
-
return
|
|
3714
|
+
return command2;
|
|
3638
3715
|
}
|
|
3639
3716
|
async function openVikingCliForMode(dryRun) {
|
|
3640
3717
|
if (dryRun) {
|
|
@@ -3642,16 +3719,16 @@ async function openVikingCliForMode(dryRun) {
|
|
|
3642
3719
|
}
|
|
3643
3720
|
return requiredOpenVikingCli();
|
|
3644
3721
|
}
|
|
3645
|
-
async function requiredExecutable(
|
|
3646
|
-
const executable = await findExecutable([
|
|
3722
|
+
async function requiredExecutable(command2) {
|
|
3723
|
+
const executable = await findExecutable([command2]);
|
|
3647
3724
|
if (!executable) {
|
|
3648
|
-
throw new Error(`${
|
|
3725
|
+
throw new Error(`${command2} was not found in PATH.`);
|
|
3649
3726
|
}
|
|
3650
3727
|
return executable;
|
|
3651
3728
|
}
|
|
3652
3729
|
async function findExecutable(commands) {
|
|
3653
|
-
for (const
|
|
3654
|
-
const result = await runCommand("which", [
|
|
3730
|
+
for (const command2 of commands) {
|
|
3731
|
+
const result = await runCommand("which", [command2], { allowFailure: true });
|
|
3655
3732
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
3656
3733
|
return result.stdout.trim();
|
|
3657
3734
|
}
|
|
@@ -3660,7 +3737,8 @@ async function findExecutable(commands) {
|
|
|
3660
3737
|
}
|
|
3661
3738
|
async function maybeRun(dryRun, executable, args, options = {}) {
|
|
3662
3739
|
const cwdSuffix = options.cwd ? ` (cwd: ${options.cwd})` : "";
|
|
3663
|
-
|
|
3740
|
+
const label = dryRun ? warning("Would run") : info("Running");
|
|
3741
|
+
console.log(`${label}: ${command(formatShellCommand(executable, args))}${cwdSuffix}`);
|
|
3664
3742
|
if (dryRun) {
|
|
3665
3743
|
return void 0;
|
|
3666
3744
|
}
|
|
@@ -4090,12 +4168,12 @@ function firstLine(value) {
|
|
|
4090
4168
|
}
|
|
4091
4169
|
function formatStatus(status) {
|
|
4092
4170
|
if (status === "ok") {
|
|
4093
|
-
return "OK ";
|
|
4171
|
+
return success("OK ");
|
|
4094
4172
|
}
|
|
4095
4173
|
if (status === "warn") {
|
|
4096
|
-
return "WARN";
|
|
4174
|
+
return warning("WARN");
|
|
4097
4175
|
}
|
|
4098
|
-
return "FAIL";
|
|
4176
|
+
return failure("FAIL");
|
|
4099
4177
|
}
|
|
4100
4178
|
function formatShellCommand(executable, args) {
|
|
4101
4179
|
return redactText([executable, ...args].map(shellQuote).join(" "));
|
|
@@ -4152,7 +4230,7 @@ async function runMcpInstall(config, agent, options) {
|
|
|
4152
4230
|
});
|
|
4153
4231
|
return;
|
|
4154
4232
|
}
|
|
4155
|
-
const
|
|
4233
|
+
const command2 = buildMcpInstallCommand(config, agent, name, {
|
|
4156
4234
|
bearerTokenEnvVar: options.bearerTokenEnvVar,
|
|
4157
4235
|
nativeHttp,
|
|
4158
4236
|
scope: options.scope,
|
|
@@ -4161,11 +4239,11 @@ async function runMcpInstall(config, agent, options) {
|
|
|
4161
4239
|
const removeCommand = buildMcpRemoveCommand(agent, name);
|
|
4162
4240
|
if (!apply) {
|
|
4163
4241
|
console.log("Dry run. Re-run with --apply to modify the selected agent config.");
|
|
4164
|
-
if (removeCommand.cwd ||
|
|
4165
|
-
console.log(`Command working directory: ${removeCommand.cwd ??
|
|
4242
|
+
if (removeCommand.cwd || command2.cwd) {
|
|
4243
|
+
console.log(`Command working directory: ${removeCommand.cwd ?? command2.cwd}`);
|
|
4166
4244
|
}
|
|
4167
4245
|
console.log(formatShellCommand(removeCommand.executable, removeCommand.args));
|
|
4168
|
-
console.log(formatShellCommand(
|
|
4246
|
+
console.log(formatShellCommand(command2.executable, command2.args));
|
|
4169
4247
|
printMcpSnippet(config, agent, name, { nativeHttp, scope: options.scope, url });
|
|
4170
4248
|
return;
|
|
4171
4249
|
}
|
|
@@ -4173,7 +4251,7 @@ async function runMcpInstall(config, agent, options) {
|
|
|
4173
4251
|
allowFailure: true,
|
|
4174
4252
|
cwd: removeCommand.cwd
|
|
4175
4253
|
});
|
|
4176
|
-
await maybeRun(false,
|
|
4254
|
+
await maybeRun(false, command2.executable, command2.args, { cwd: command2.cwd });
|
|
4177
4255
|
}
|
|
4178
4256
|
async function runCursorMcpInstall(config, name, options) {
|
|
4179
4257
|
const path = cursorMcpConfigPath();
|
|
@@ -4244,8 +4322,8 @@ async function removeMcpConfigs(value, dryRun) {
|
|
|
4244
4322
|
await removeCopilotMcpConfig(OPENVIKING_MCP_NAME, dryRun);
|
|
4245
4323
|
continue;
|
|
4246
4324
|
}
|
|
4247
|
-
const
|
|
4248
|
-
await maybeRun(dryRun,
|
|
4325
|
+
const command2 = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
|
|
4326
|
+
await maybeRun(dryRun, command2.executable, command2.args, { allowFailure: true, cwd: command2.cwd });
|
|
4249
4327
|
}
|
|
4250
4328
|
}
|
|
4251
4329
|
async function removeMcpSnippets(config, dryRun) {
|
|
@@ -4280,17 +4358,17 @@ function buildMcpInstallCommand(config, agent, name, options) {
|
|
|
4280
4358
|
const claudeCwd = getInvocationCwd();
|
|
4281
4359
|
const claudeScope = options.scope ?? "user";
|
|
4282
4360
|
if (!options.nativeHttp) {
|
|
4283
|
-
const
|
|
4361
|
+
const command2 = mcpAdapterCommand();
|
|
4284
4362
|
const env = mcpEnvironment(config);
|
|
4285
4363
|
if (agent === "codex") {
|
|
4286
4364
|
return {
|
|
4287
4365
|
executable: "codex",
|
|
4288
|
-
args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", "/usr/bin/env", ...
|
|
4366
|
+
args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", "/usr/bin/env", ...command2]
|
|
4289
4367
|
};
|
|
4290
4368
|
}
|
|
4291
4369
|
return {
|
|
4292
4370
|
executable: "claude",
|
|
4293
|
-
args: ["mcp", "add", "--scope", claudeScope, name, "--", "/usr/bin/env", ...env, ...
|
|
4371
|
+
args: ["mcp", "add", "--scope", claudeScope, name, "--", "/usr/bin/env", ...env, ...command2],
|
|
4294
4372
|
cwd: claudeCwd
|
|
4295
4373
|
};
|
|
4296
4374
|
}
|
|
@@ -4472,12 +4550,12 @@ function printMcpSnippet(config, agent, name, options) {
|
|
|
4472
4550
|
return;
|
|
4473
4551
|
}
|
|
4474
4552
|
const snippetPath = (0, import_node_path2.join)(config.agentContextHome, "mcp", `${name}.${agent}.${agent === "codex" ? "toml" : "txt"}`);
|
|
4475
|
-
const
|
|
4553
|
+
const command2 = buildMcpInstallCommand(config, agent, name, {
|
|
4476
4554
|
nativeHttp: options.nativeHttp,
|
|
4477
4555
|
scope: options.scope,
|
|
4478
4556
|
url: options.url
|
|
4479
4557
|
});
|
|
4480
|
-
const snippet2 = `${formatShellCommand(
|
|
4558
|
+
const snippet2 = `${formatShellCommand(command2.executable, command2.args)}
|
|
4481
4559
|
`;
|
|
4482
4560
|
console.log(`
|
|
4483
4561
|
Snippet (${snippetPath}):
|
|
@@ -7960,9 +8038,9 @@ async function syncSharedReposBeforeAgentRead(config) {
|
|
|
7960
8038
|
const remainingBehind = new Set(state.behindTeams);
|
|
7961
8039
|
for (const team of syncTeams) {
|
|
7962
8040
|
try {
|
|
7963
|
-
const
|
|
7964
|
-
if (
|
|
7965
|
-
warnings.push(
|
|
8041
|
+
const warning2 = await runShareSyncQuiet(config, state, { team });
|
|
8042
|
+
if (warning2) {
|
|
8043
|
+
warnings.push(warning2);
|
|
7966
8044
|
} else {
|
|
7967
8045
|
remainingBehind.delete(team);
|
|
7968
8046
|
syncedTeams.push(team);
|
|
@@ -8510,7 +8588,35 @@ function workfileToVikingUri(config, team, filePath) {
|
|
|
8510
8588
|
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
8511
8589
|
}
|
|
8512
8590
|
function isInSharedNamespace(config, uri) {
|
|
8513
|
-
return
|
|
8591
|
+
return sharedTeamNameForUri(config, uri) !== void 0;
|
|
8592
|
+
}
|
|
8593
|
+
function sharedTeamNameForUri(config, uri) {
|
|
8594
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
|
|
8595
|
+
if (!uri.startsWith(prefix)) {
|
|
8596
|
+
return void 0;
|
|
8597
|
+
}
|
|
8598
|
+
const [team] = uri.slice(prefix.length).split("/");
|
|
8599
|
+
return team || void 0;
|
|
8600
|
+
}
|
|
8601
|
+
function sharedMemoryUriParts(config, uri) {
|
|
8602
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
|
|
8603
|
+
if (!uri.startsWith(prefix)) {
|
|
8604
|
+
return void 0;
|
|
8605
|
+
}
|
|
8606
|
+
const [team, kind, scope, project, ...topicParts] = uri.slice(prefix.length).split("/");
|
|
8607
|
+
if (!team) {
|
|
8608
|
+
return void 0;
|
|
8609
|
+
}
|
|
8610
|
+
if (kind !== "durable" || scope !== "projects" || !project || topicParts.length === 0) {
|
|
8611
|
+
return { team };
|
|
8612
|
+
}
|
|
8613
|
+
const topicPath = topicParts.join("/");
|
|
8614
|
+
return {
|
|
8615
|
+
kind,
|
|
8616
|
+
project,
|
|
8617
|
+
team,
|
|
8618
|
+
topic: topicPath.endsWith(".md") ? topicPath.slice(0, -".md".length) : topicPath
|
|
8619
|
+
};
|
|
8514
8620
|
}
|
|
8515
8621
|
function isInTeamNamespace(config, uri, team) {
|
|
8516
8622
|
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`);
|
|
@@ -8696,14 +8802,14 @@ async function waitForOvQueue(ov, config, options = {}) {
|
|
|
8696
8802
|
console.error(result.stderr.trim());
|
|
8697
8803
|
}
|
|
8698
8804
|
}
|
|
8699
|
-
function isTransientOvFailure(stderr,
|
|
8805
|
+
function isTransientOvFailure(stderr, stdout2) {
|
|
8700
8806
|
const output2 = `${stderr}
|
|
8701
|
-
${
|
|
8807
|
+
${stdout2}`.toLowerCase();
|
|
8702
8808
|
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");
|
|
8703
8809
|
}
|
|
8704
|
-
function isResourceBusyFailure(stderr,
|
|
8810
|
+
function isResourceBusyFailure(stderr, stdout2) {
|
|
8705
8811
|
const output2 = `${stderr}
|
|
8706
|
-
${
|
|
8812
|
+
${stdout2}`.toLowerCase();
|
|
8707
8813
|
return output2.includes("resource is busy") || output2.includes("resource is being processed");
|
|
8708
8814
|
}
|
|
8709
8815
|
async function ingestSingleFile(ov, config, uri, filePath, initialMode, options = {}) {
|
|
@@ -9138,8 +9244,8 @@ async function syncSharedReposAndLog(config) {
|
|
|
9138
9244
|
if (syncResult.syncedTeams.length > 0) {
|
|
9139
9245
|
console.error(`Auto-synced shared memories: ${syncResult.syncedTeams.join(", ")}`);
|
|
9140
9246
|
}
|
|
9141
|
-
for (const
|
|
9142
|
-
console.error(`Auto-sync warning: ${
|
|
9247
|
+
for (const warning2 of syncResult.warnings) {
|
|
9248
|
+
console.error(`Auto-sync warning: ${warning2}`);
|
|
9143
9249
|
}
|
|
9144
9250
|
} catch (err) {
|
|
9145
9251
|
console.error(`Auto-sync warning: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -9434,6 +9540,10 @@ async function storeMemory(config, options) {
|
|
|
9434
9540
|
assertVikingUri(options.replaceUri);
|
|
9435
9541
|
}
|
|
9436
9542
|
const ov = await openVikingCliForMode(options.dryRun);
|
|
9543
|
+
if (options.replaceUri && isInSharedNamespace(config, options.replaceUri)) {
|
|
9544
|
+
await storeSharedMemoryReplacement(config, ov, options, options.replaceUri);
|
|
9545
|
+
return;
|
|
9546
|
+
}
|
|
9437
9547
|
const memoryPath = (0, import_node_path6.join)(config.agentContextHome, "last-memory.txt");
|
|
9438
9548
|
const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
|
|
9439
9549
|
const candidateMemory = formatMemoryDocument2(options.title, candidateMetadata, options.bodyText);
|
|
@@ -9484,6 +9594,49 @@ async function storeMemory(config, options) {
|
|
|
9484
9594
|
console.log(`Updated existing memory in place: ${memoryUri}`);
|
|
9485
9595
|
}
|
|
9486
9596
|
}
|
|
9597
|
+
async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
|
|
9598
|
+
if (options.metadata.kind !== "durable") {
|
|
9599
|
+
throw new Error("Shared memory replacement only supports durable memories.");
|
|
9600
|
+
}
|
|
9601
|
+
const teamName = sharedTeamNameForUri(config, targetUri);
|
|
9602
|
+
if (!teamName) {
|
|
9603
|
+
throw new Error(`Memory ${targetUri} is not in the shared namespace.`);
|
|
9604
|
+
}
|
|
9605
|
+
const team = await resolveTeam(config, teamName);
|
|
9606
|
+
const inferred = sharedMemoryUriParts(config, targetUri);
|
|
9607
|
+
const metadata = {
|
|
9608
|
+
...options.metadata,
|
|
9609
|
+
project: options.metadata.project ?? inferred?.project,
|
|
9610
|
+
topic: options.metadata.topic ?? inferred?.topic
|
|
9611
|
+
};
|
|
9612
|
+
const rawMemory = formatMemoryDocument2(options.title, metadata, options.bodyText);
|
|
9613
|
+
const scrub = applyScrubber(stripPersonalProvenance(rawMemory), { redact: false });
|
|
9614
|
+
if (scrub.blocker) {
|
|
9615
|
+
throw new Error(
|
|
9616
|
+
`Refusing to update shared memory ${targetUri}: possible ${scrub.blocker}. Strip the sensitive value first.`
|
|
9617
|
+
);
|
|
9618
|
+
}
|
|
9619
|
+
const memory = scrub.cleaned;
|
|
9620
|
+
const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
|
|
9621
|
+
if (options.dryRun) {
|
|
9622
|
+
console.log(memory);
|
|
9623
|
+
console.log("\nWould run:");
|
|
9624
|
+
}
|
|
9625
|
+
await ensureSharedDirectoryChain(config, ov, targetUri, options.dryRun);
|
|
9626
|
+
await writeMemoryFile(config, ov, targetUri, memory, "replace", options.dryRun);
|
|
9627
|
+
const git = await requiredExecutable("git");
|
|
9628
|
+
await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "add", "--", relativePath]);
|
|
9629
|
+
await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "commit", "-m", `share: update ${relativePath}`], {
|
|
9630
|
+
allowFailure: true
|
|
9631
|
+
});
|
|
9632
|
+
await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "push", DEFAULT_GIT_REMOTE_NAME], {
|
|
9633
|
+
allowFailure: true
|
|
9634
|
+
});
|
|
9635
|
+
for (const redaction of scrub.redactions) {
|
|
9636
|
+
console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
|
|
9637
|
+
}
|
|
9638
|
+
console.log(`Updated shared memory: ${targetUri}`);
|
|
9639
|
+
}
|
|
9487
9640
|
async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
|
|
9488
9641
|
const args = withIdentity(config, [
|
|
9489
9642
|
"write",
|
|
@@ -9920,9 +10073,9 @@ function localUserMemoriesRoot(config) {
|
|
|
9920
10073
|
function uniqueStrings(values) {
|
|
9921
10074
|
return [...new Set(values)].sort();
|
|
9922
10075
|
}
|
|
9923
|
-
function isResourceBusy(stderr,
|
|
10076
|
+
function isResourceBusy(stderr, stdout2) {
|
|
9924
10077
|
const output2 = `${stderr}
|
|
9925
|
-
${
|
|
10078
|
+
${stdout2}`.toLowerCase();
|
|
9926
10079
|
return output2.includes("resource is busy") || output2.includes("resource is being processed");
|
|
9927
10080
|
}
|
|
9928
10081
|
async function buildHandoff(options) {
|
|
@@ -10712,7 +10865,7 @@ var import_node_fs6 = require("node:fs");
|
|
|
10712
10865
|
var import_promises11 = require("node:fs/promises");
|
|
10713
10866
|
var import_node_os6 = require("node:os");
|
|
10714
10867
|
var import_node_path12 = require("node:path");
|
|
10715
|
-
var
|
|
10868
|
+
var import_node_process3 = require("node:process");
|
|
10716
10869
|
var import_promises12 = require("node:readline/promises");
|
|
10717
10870
|
|
|
10718
10871
|
// src/update.ts
|
|
@@ -10721,7 +10874,106 @@ var import_promises9 = require("node:fs/promises");
|
|
|
10721
10874
|
var import_node_os5 = require("node:os");
|
|
10722
10875
|
var import_node_path11 = require("node:path");
|
|
10723
10876
|
var import_promises10 = require("node:readline/promises");
|
|
10724
|
-
var
|
|
10877
|
+
var import_node_process2 = require("node:process");
|
|
10878
|
+
|
|
10879
|
+
// src/release_notes.ts
|
|
10880
|
+
var GITHUB_RELEASES_URL = "https://api.github.com/repos/Kashkovsky/threadnote/releases?per_page=100";
|
|
10881
|
+
var RELEASE_FETCH_TIMEOUT_MS = 3500;
|
|
10882
|
+
async function fetchThreadnoteReleaseNotes() {
|
|
10883
|
+
const response = await fetch(GITHUB_RELEASES_URL, {
|
|
10884
|
+
headers: {
|
|
10885
|
+
accept: "application/vnd.github+json",
|
|
10886
|
+
"user-agent": "threadnote-cli"
|
|
10887
|
+
},
|
|
10888
|
+
signal: AbortSignal.timeout(RELEASE_FETCH_TIMEOUT_MS)
|
|
10889
|
+
});
|
|
10890
|
+
if (!response.ok) {
|
|
10891
|
+
throw new Error(`GitHub releases returned HTTP ${response.status}`);
|
|
10892
|
+
}
|
|
10893
|
+
const parsed = await response.json();
|
|
10894
|
+
if (!Array.isArray(parsed)) {
|
|
10895
|
+
throw new Error("GitHub releases response was not an array.");
|
|
10896
|
+
}
|
|
10897
|
+
return parsed.flatMap(parseGithubRelease);
|
|
10898
|
+
}
|
|
10899
|
+
function releasesBetween(releases, currentVersion, latestVersion) {
|
|
10900
|
+
return releases.filter(
|
|
10901
|
+
(release) => compareVersions(release.version, currentVersion) > 0 && compareVersions(release.version, latestVersion) <= 0
|
|
10902
|
+
).sort((left, right) => compareVersions(left.version, right.version));
|
|
10903
|
+
}
|
|
10904
|
+
function releaseForVersion(releases, version) {
|
|
10905
|
+
return releases.filter((release) => compareVersions(release.version, version) === 0).slice(0, 1);
|
|
10906
|
+
}
|
|
10907
|
+
function formatWhatsNew(releases) {
|
|
10908
|
+
if (releases.length === 0) {
|
|
10909
|
+
return [];
|
|
10910
|
+
}
|
|
10911
|
+
const lines = ["What's new:"];
|
|
10912
|
+
for (const release of releases) {
|
|
10913
|
+
lines.push(`${release.version}: ${release.title || "Release notes"}`);
|
|
10914
|
+
for (const line of bodyLines(release.body)) {
|
|
10915
|
+
lines.push(` ${line}`);
|
|
10916
|
+
}
|
|
10917
|
+
}
|
|
10918
|
+
return lines;
|
|
10919
|
+
}
|
|
10920
|
+
async function whatsNewLinesForVersion(version) {
|
|
10921
|
+
try {
|
|
10922
|
+
const releases = await fetchThreadnoteReleaseNotes();
|
|
10923
|
+
const lines = formatWhatsNew(releaseForVersion(releases, version));
|
|
10924
|
+
return lines.length > 0 ? lines : ["What's new:", `No GitHub release notes found for ${version}.`];
|
|
10925
|
+
} catch (err) {
|
|
10926
|
+
return [`What's new: unavailable (${errorMessage(err)})`];
|
|
10927
|
+
}
|
|
10928
|
+
}
|
|
10929
|
+
async function whatsNewLinesForVersionRange(currentVersion, latestVersion) {
|
|
10930
|
+
try {
|
|
10931
|
+
const releases = await fetchThreadnoteReleaseNotes();
|
|
10932
|
+
const lines = formatWhatsNew(releasesBetween(releases, currentVersion, latestVersion));
|
|
10933
|
+
return lines.length > 0 ? lines : ["What's new:", "No GitHub release notes found for this version range."];
|
|
10934
|
+
} catch (err) {
|
|
10935
|
+
return [`What's new: unavailable (${errorMessage(err)})`];
|
|
10936
|
+
}
|
|
10937
|
+
}
|
|
10938
|
+
function parseGithubRelease(value) {
|
|
10939
|
+
if (!isJsonObject(value) || value.draft === true || value.prerelease === true) {
|
|
10940
|
+
return [];
|
|
10941
|
+
}
|
|
10942
|
+
const tagName = typeof value.tag_name === "string" ? value.tag_name : "";
|
|
10943
|
+
const version = tagName.trim().replace(/^v/, "");
|
|
10944
|
+
if (!/^\d+\.\d+\.\d+(?:[-.A-Za-z0-9]+)?$/.test(version)) {
|
|
10945
|
+
return [];
|
|
10946
|
+
}
|
|
10947
|
+
const name = typeof value.name === "string" ? value.name : tagName;
|
|
10948
|
+
const body = typeof value.body === "string" ? value.body : "";
|
|
10949
|
+
const url = typeof value.html_url === "string" ? value.html_url : void 0;
|
|
10950
|
+
return [{ body, title: titleFromReleaseName(name, version), url, version }];
|
|
10951
|
+
}
|
|
10952
|
+
function titleFromReleaseName(name, version) {
|
|
10953
|
+
const trimmed = name.trim();
|
|
10954
|
+
const withoutPrefix = trimmed.replace(new RegExp(`^v?${escapeRegExp2(version)}:\\s*`, "i"), "");
|
|
10955
|
+
return withoutPrefix || trimmed || "Release notes";
|
|
10956
|
+
}
|
|
10957
|
+
function bodyLines(body) {
|
|
10958
|
+
const out = [];
|
|
10959
|
+
for (const rawLine of body.split("\n")) {
|
|
10960
|
+
const line = rawLine.trim();
|
|
10961
|
+
if (!line || /^#{1,6}\s+/.test(line)) {
|
|
10962
|
+
continue;
|
|
10963
|
+
}
|
|
10964
|
+
if (/^[-*]\s+/.test(line)) {
|
|
10965
|
+
out.push(`- ${line.replace(/^[-*]\s+/, "")}`);
|
|
10966
|
+
} else {
|
|
10967
|
+
out.push(line);
|
|
10968
|
+
}
|
|
10969
|
+
}
|
|
10970
|
+
return out;
|
|
10971
|
+
}
|
|
10972
|
+
function escapeRegExp2(value) {
|
|
10973
|
+
return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
|
|
10974
|
+
}
|
|
10975
|
+
|
|
10976
|
+
// src/update.ts
|
|
10725
10977
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
10726
10978
|
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
10727
10979
|
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -10738,43 +10990,47 @@ async function maybeNotifyUpdate(config, options = {}) {
|
|
|
10738
10990
|
return;
|
|
10739
10991
|
}
|
|
10740
10992
|
try {
|
|
10741
|
-
const
|
|
10993
|
+
const info2 = await getUpdateInfo(config, {
|
|
10742
10994
|
allowCacheWrite: options.dryRun !== true,
|
|
10743
10995
|
preferFresh: false,
|
|
10744
10996
|
registry: updateRegistry()
|
|
10745
10997
|
});
|
|
10746
|
-
if (!
|
|
10998
|
+
if (!info2.isUpdateAvailable) {
|
|
10747
10999
|
return;
|
|
10748
11000
|
}
|
|
10749
11001
|
console.log("");
|
|
10750
|
-
console.log(`Update available: threadnote ${
|
|
10751
|
-
console.log(
|
|
11002
|
+
console.log(warning(`Update available: threadnote ${info2.currentVersion} -> ${info2.latestVersion}`));
|
|
11003
|
+
console.log(`Run: ${info("threadnote update")}`);
|
|
10752
11004
|
} catch (_err) {
|
|
10753
11005
|
return;
|
|
10754
11006
|
}
|
|
10755
11007
|
}
|
|
10756
11008
|
async function runUpdate(config, options) {
|
|
10757
11009
|
const registry = normalizeRegistry(options.registry ?? updateRegistry());
|
|
10758
|
-
const
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
11010
|
+
const info2 = await withSpinner(
|
|
11011
|
+
"Checking npm for latest threadnote version",
|
|
11012
|
+
() => getUpdateInfo(config, {
|
|
11013
|
+
allowCacheWrite: options.dryRun !== true,
|
|
11014
|
+
preferFresh: true,
|
|
11015
|
+
registry
|
|
11016
|
+
})
|
|
11017
|
+
);
|
|
11018
|
+
console.log(keyValue("Current version", info(info2.currentVersion)));
|
|
11019
|
+
console.log(keyValue("Latest version", info(info2.latestVersion)));
|
|
11020
|
+
console.log(keyValue("Registry", info2.registry));
|
|
10766
11021
|
if (options.check === true) {
|
|
10767
|
-
if (
|
|
10768
|
-
console.log(
|
|
11022
|
+
if (info2.isUpdateAvailable) {
|
|
11023
|
+
console.log(warning("Update available. Run: threadnote update"));
|
|
11024
|
+
await printWhatsNewIfAvailable(info2);
|
|
10769
11025
|
} else {
|
|
10770
11026
|
console.log(
|
|
10771
|
-
compareVersions(
|
|
11027
|
+
compareVersions(info2.currentVersion, info2.latestVersion) > 0 ? warning("Current version is newer than npm latest.") : success("Threadnote is up to date.")
|
|
10772
11028
|
);
|
|
10773
11029
|
}
|
|
10774
11030
|
return;
|
|
10775
11031
|
}
|
|
10776
|
-
if (!
|
|
10777
|
-
console.log("Threadnote is up to date.");
|
|
11032
|
+
if (!info2.isUpdateAvailable && options.force !== true) {
|
|
11033
|
+
console.log(success("Threadnote is up to date."));
|
|
10778
11034
|
return;
|
|
10779
11035
|
}
|
|
10780
11036
|
const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
|
|
@@ -10782,6 +11038,7 @@ async function runUpdate(config, options) {
|
|
|
10782
11038
|
await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
|
|
10783
11039
|
if (options.repair === false) {
|
|
10784
11040
|
console.log("Skipping repair because --no-repair was provided.");
|
|
11041
|
+
await printWhatsNewIfAvailable(info2);
|
|
10785
11042
|
return;
|
|
10786
11043
|
}
|
|
10787
11044
|
const threadnoteCommand = await installedThreadnoteCommand(runtime);
|
|
@@ -10790,9 +11047,9 @@ async function runUpdate(config, options) {
|
|
|
10790
11047
|
const postUpdateArgs = [
|
|
10791
11048
|
"post-update",
|
|
10792
11049
|
"--from-version",
|
|
10793
|
-
|
|
11050
|
+
info2.currentVersion,
|
|
10794
11051
|
"--to-version",
|
|
10795
|
-
|
|
11052
|
+
info2.latestVersion,
|
|
10796
11053
|
...options.yes === true ? ["--yes"] : []
|
|
10797
11054
|
];
|
|
10798
11055
|
if (options.dryRun === true) {
|
|
@@ -10810,6 +11067,20 @@ async function runUpdate(config, options) {
|
|
|
10810
11067
|
console.log(
|
|
10811
11068
|
"Update complete. Restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload."
|
|
10812
11069
|
);
|
|
11070
|
+
await printWhatsNewIfAvailable(info2);
|
|
11071
|
+
}
|
|
11072
|
+
async function printWhatsNewIfAvailable(info2) {
|
|
11073
|
+
if (!info2.isUpdateAvailable) {
|
|
11074
|
+
return;
|
|
11075
|
+
}
|
|
11076
|
+
console.log("");
|
|
11077
|
+
const whatsNew = await withSpinner(
|
|
11078
|
+
"Fetching GitHub release notes",
|
|
11079
|
+
() => whatsNewLinesForVersionRange(info2.currentVersion, info2.latestVersion)
|
|
11080
|
+
);
|
|
11081
|
+
for (const line of whatsNew) {
|
|
11082
|
+
console.log(line === "What's new:" ? heading(line) : line);
|
|
11083
|
+
}
|
|
10813
11084
|
}
|
|
10814
11085
|
async function runPostUpdate(config, options) {
|
|
10815
11086
|
if (!options.fromVersion || !options.toVersion) {
|
|
@@ -11127,7 +11398,7 @@ function printPostUpdateMigration(migration) {
|
|
|
11127
11398
|
}
|
|
11128
11399
|
}
|
|
11129
11400
|
async function confirmPostUpdateMigration(prompt, defaultYes = false) {
|
|
11130
|
-
const readline = (0, import_promises10.createInterface)({ input:
|
|
11401
|
+
const readline = (0, import_promises10.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
11131
11402
|
try {
|
|
11132
11403
|
const answer = (await readline.question(prompt)).trim().toLowerCase();
|
|
11133
11404
|
if (answer === "") {
|
|
@@ -11649,8 +11920,8 @@ async function commandPresenceCheck(name, args) {
|
|
|
11649
11920
|
};
|
|
11650
11921
|
}
|
|
11651
11922
|
async function firstCommandCheck(name, commands, args) {
|
|
11652
|
-
for (const
|
|
11653
|
-
const executable = await findExecutable([
|
|
11923
|
+
for (const command2 of commands) {
|
|
11924
|
+
const executable = await findExecutable([command2]);
|
|
11654
11925
|
if (!executable) {
|
|
11655
11926
|
continue;
|
|
11656
11927
|
}
|
|
@@ -11658,7 +11929,7 @@ async function firstCommandCheck(name, commands, args) {
|
|
|
11658
11929
|
return {
|
|
11659
11930
|
name,
|
|
11660
11931
|
status: result.exitCode === 0 ? "ok" : "warn",
|
|
11661
|
-
detail: `${
|
|
11932
|
+
detail: `${command2}: ${firstLine(result.stdout || result.stderr) || executable}`
|
|
11662
11933
|
};
|
|
11663
11934
|
}
|
|
11664
11935
|
return { name, status: "fail", detail: `none found: ${commands.join(", ")}` };
|
|
@@ -11833,13 +12104,13 @@ async function runInstallCommands(config, preferred, force, dryRun) {
|
|
|
11833
12104
|
}
|
|
11834
12105
|
}
|
|
11835
12106
|
async function offerToInstallUv() {
|
|
11836
|
-
if (
|
|
12107
|
+
if (import_node_process3.stdin.isTTY !== true || import_node_process3.stdout.isTTY !== true) {
|
|
11837
12108
|
console.warn(
|
|
11838
12109
|
"Neither uv nor pipx was found on PATH. Falling back to `python3 -m pip install --user`, which fails on PEP 668 (Homebrew/system) Python.\nRe-run with --package-manager uv after installing uv (brew install uv), or pass --package-manager pipx."
|
|
11839
12110
|
);
|
|
11840
12111
|
return false;
|
|
11841
12112
|
}
|
|
11842
|
-
const readline = (0, import_promises12.createInterface)({ input:
|
|
12113
|
+
const readline = (0, import_promises12.createInterface)({ input: import_node_process3.stdin, output: import_node_process3.stdout });
|
|
11843
12114
|
let answer;
|
|
11844
12115
|
try {
|
|
11845
12116
|
answer = (await readline.question(
|
|
@@ -12290,6 +12561,46 @@ function parsePackageManager(value) {
|
|
|
12290
12561
|
throw new Error(`Invalid package manager: ${value}`);
|
|
12291
12562
|
}
|
|
12292
12563
|
|
|
12564
|
+
// src/version_command.ts
|
|
12565
|
+
async function runVersion(config, options) {
|
|
12566
|
+
const currentVersion = await currentPackageVersion();
|
|
12567
|
+
const registry = normalizeRegistry(options.registry ?? updateRegistry());
|
|
12568
|
+
let latestVersion;
|
|
12569
|
+
let latestWarning;
|
|
12570
|
+
try {
|
|
12571
|
+
latestVersion = await withSpinner("Checking npm for latest threadnote version", () => fetchLatestVersion2(registry));
|
|
12572
|
+
} catch (err) {
|
|
12573
|
+
latestWarning = errorMessage(err);
|
|
12574
|
+
}
|
|
12575
|
+
console.log(keyValue("Current version", info(currentVersion)));
|
|
12576
|
+
console.log(keyValue("Latest version", latestVersion ? info(latestVersion) : warning("unavailable")));
|
|
12577
|
+
if (latestWarning) {
|
|
12578
|
+
console.log(warning(`Warning: ${latestWarning}`));
|
|
12579
|
+
}
|
|
12580
|
+
if (!latestVersion) {
|
|
12581
|
+
return;
|
|
12582
|
+
}
|
|
12583
|
+
const comparison = compareVersions(currentVersion, latestVersion);
|
|
12584
|
+
if (comparison >= 0) {
|
|
12585
|
+
console.log(success(comparison > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."));
|
|
12586
|
+
console.log("");
|
|
12587
|
+
const whatsNew2 = await withSpinner("Fetching GitHub release notes", () => whatsNewLinesForVersion(currentVersion));
|
|
12588
|
+
printWhatsNew(whatsNew2);
|
|
12589
|
+
return;
|
|
12590
|
+
}
|
|
12591
|
+
console.log("");
|
|
12592
|
+
const whatsNew = await withSpinner(
|
|
12593
|
+
"Fetching GitHub release notes",
|
|
12594
|
+
() => whatsNewLinesForVersionRange(currentVersion, latestVersion)
|
|
12595
|
+
);
|
|
12596
|
+
printWhatsNew(whatsNew);
|
|
12597
|
+
}
|
|
12598
|
+
function printWhatsNew(whatsNew) {
|
|
12599
|
+
for (const line of whatsNew) {
|
|
12600
|
+
console.log(line === "What's new:" ? heading(line) : line);
|
|
12601
|
+
}
|
|
12602
|
+
}
|
|
12603
|
+
|
|
12293
12604
|
// src/threadnote.ts
|
|
12294
12605
|
async function main() {
|
|
12295
12606
|
const program2 = new Command();
|
|
@@ -12315,6 +12626,9 @@ async function main() {
|
|
|
12315
12626
|
}
|
|
12316
12627
|
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
12317
12628
|
});
|
|
12629
|
+
program2.command("version").description("Print the installed Threadnote version, latest npm version, and release notes").option("--registry <url>", "npm registry URL", process.env.THREADNOTE_NPM_REGISTRY).action(async (options) => {
|
|
12630
|
+
await runVersion(getRuntimeConfig(program2), options);
|
|
12631
|
+
});
|
|
12318
12632
|
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) => {
|
|
12319
12633
|
await runUpdate(getRuntimeConfig(program2), options);
|
|
12320
12634
|
});
|
|
@@ -73,14 +73,17 @@ For branch feature work, prefer two stable memories with the same project and to
|
|
|
73
73
|
- `kind: handoff` for current status, files touched, tests, blockers, and next step.
|
|
74
74
|
|
|
75
75
|
If a durable feature memory already exists, update it in place with the same project/topic or with `--replace <uri>` /
|
|
76
|
-
`replaceUri`.
|
|
76
|
+
`replaceUri`. When `replaceUri` points at a shared `durable/` URI, Threadnote updates that shared memory in place and
|
|
77
|
+
pushes the shared repo; do not create a personal replacement and then run `share_publish` again. Avoid creating a new
|
|
78
|
+
timestamped durable memory for every small progress note.
|
|
77
79
|
|
|
78
80
|
## Memory Compaction
|
|
79
81
|
|
|
80
82
|
When working on the same active issue, prefer keeping one current-state memory updated instead of creating many small
|
|
81
83
|
progress memories. If an existing memory is clearly the current state for the issue, store the updated version with
|
|
82
84
|
`remember_context({"text":"...","replaceUri":"<uri>"})`, `threadnote remember --replace <uri>`, or
|
|
83
|
-
`threadnote handoff --replace <uri>` so the old memory is removed only after the replacement is stored.
|
|
85
|
+
`threadnote handoff --replace <uri>` so the old personal memory is removed only after the replacement is stored. Shared
|
|
86
|
+
`durable/` replacements are updated in place and pushed instead.
|
|
84
87
|
|
|
85
88
|
When the issue has a stable name, prefer project/topic storage over timestamped memories:
|
|
86
89
|
|
package/docs/index.html
CHANGED
|
@@ -1341,7 +1341,7 @@ threadnote remember \
|
|
|
1341
1341
|
<p class="muted" style="font-size: 0.9rem; margin-top: 0.6rem">
|
|
1342
1342
|
Refuses to publish if the memory matches secret patterns (PEM, <code>sk-</code>, <code>gh*_</code>,
|
|
1343
1343
|
<code>github_pat_</code>, <code>glpat-</code>, JWTs, AWS keys, Slack tokens). Refuses to silently
|
|
1344
|
-
overwrite an existing shared URI
|
|
1344
|
+
overwrite an existing shared URI; update shared memories with <code>remember --replace</code>.
|
|
1345
1345
|
</p>
|
|
1346
1346
|
</div>
|
|
1347
1347
|
|
package/docs/share.md
CHANGED
|
@@ -71,6 +71,21 @@ lines from the memory's header block. Those pointers only resolve on the
|
|
|
71
71
|
publisher's machine — teammates pull via git and cannot dereference them — so
|
|
72
72
|
keeping them would just leak local-only provenance into team git history.
|
|
73
73
|
|
|
74
|
+
To update an existing shared durable memory, replace the shared URI directly:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
threadnote remember \
|
|
78
|
+
--replace viking://user/you/memories/shared/default/durable/projects/foo/bar.md \
|
|
79
|
+
--project foo \
|
|
80
|
+
--topic bar \
|
|
81
|
+
--text "Updated shared knowledge."
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For MCP, pass the same shared URI as `replaceUri` to `remember_context`.
|
|
85
|
+
Threadnote rewrites the shared memory in place, strips local-only provenance
|
|
86
|
+
headers, commits, and pushes the shared repo. You do not need to store a
|
|
87
|
+
personal replacement and run `share publish` again.
|
|
88
|
+
|
|
74
89
|
### Keep teammates' updates current
|
|
75
90
|
|
|
76
91
|
Threadnote does a periodic background `git fetch` for configured share teams.
|
|
@@ -155,7 +170,9 @@ otherwise unpublished work is lost.
|
|
|
155
170
|
keep both, copy the memory to a new URI first (`ov read` then
|
|
156
171
|
`threadnote remember`).
|
|
157
172
|
- `share publish` refuses to overwrite an existing shared memory at the same
|
|
158
|
-
URI;
|
|
173
|
+
URI; use `threadnote remember --replace <shared-uri>` or
|
|
174
|
+
`remember_context({replaceUri:"<shared-uri>"})` for updates, or pick a
|
|
175
|
+
different topic name.
|
|
159
176
|
|
|
160
177
|
## Conflict resolution
|
|
161
178
|
|