threadnote 0.7.6 → 0.7.7
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 +5 -3
- package/dist/mcp_server.cjs +69 -2
- package/dist/threadnote.cjs +76 -1
- 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
|
|
|
@@ -347,8 +348,9 @@ This is it! Start working with your agents as usual. The agent will automaticall
|
|
|
347
348
|
reusable workflow guidance. Use `seed-skills --native` only after configuring a working VLM provider.
|
|
348
349
|
- `mcp-install codex|claude|cursor|copilot`: installs or prints OpenViking MCP configuration for Codex, Claude, Cursor,
|
|
349
350
|
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
|
|
351
|
+
- `remember`: stores a durable memory. Use `--replace <uri>` to store an updated memory and remove a superseded personal
|
|
352
|
+
memory after the new memory succeeds; if `<uri>` is shared, Threadnote updates that shared memory in place and pushes
|
|
353
|
+
the shared repo. Use `--kind`, `--project`, and `--topic` to store lifecycle-aware current knowledge.
|
|
352
354
|
- `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
|
|
353
355
|
`migrate-memories --dry-run` first; use `--all-accounts` when importing from older local OpenViking accounts.
|
|
354
356
|
- `migrate-lifecycle`: moves clear legacy handoff memories from the old events path into archived lifecycle handoff
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -34726,7 +34726,35 @@ function workfileToVikingUri(config2, team, filePath) {
|
|
|
34726
34726
|
return `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
34727
34727
|
}
|
|
34728
34728
|
function isInSharedNamespace(config2, uri) {
|
|
34729
|
-
return
|
|
34729
|
+
return sharedTeamNameForUri(config2, uri) !== void 0;
|
|
34730
|
+
}
|
|
34731
|
+
function sharedTeamNameForUri(config2, uri) {
|
|
34732
|
+
const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
|
|
34733
|
+
if (!uri.startsWith(prefix)) {
|
|
34734
|
+
return void 0;
|
|
34735
|
+
}
|
|
34736
|
+
const [team] = uri.slice(prefix.length).split("/");
|
|
34737
|
+
return team || void 0;
|
|
34738
|
+
}
|
|
34739
|
+
function sharedMemoryUriParts(config2, uri) {
|
|
34740
|
+
const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
|
|
34741
|
+
if (!uri.startsWith(prefix)) {
|
|
34742
|
+
return void 0;
|
|
34743
|
+
}
|
|
34744
|
+
const [team, kind, scope, project, ...topicParts] = uri.slice(prefix.length).split("/");
|
|
34745
|
+
if (!team) {
|
|
34746
|
+
return void 0;
|
|
34747
|
+
}
|
|
34748
|
+
if (kind !== "durable" || scope !== "projects" || !project || topicParts.length === 0) {
|
|
34749
|
+
return { team };
|
|
34750
|
+
}
|
|
34751
|
+
const topicPath = topicParts.join("/");
|
|
34752
|
+
return {
|
|
34753
|
+
kind,
|
|
34754
|
+
project,
|
|
34755
|
+
team,
|
|
34756
|
+
topic: topicPath.endsWith(".md") ? topicPath.slice(0, -".md".length) : topicPath
|
|
34757
|
+
};
|
|
34730
34758
|
}
|
|
34731
34759
|
function sharedUriFor(config2, personalUri, team) {
|
|
34732
34760
|
const prefix = `viking://user/${uriSegment(config2.user)}/memories/`;
|
|
@@ -35547,7 +35575,9 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
35547
35575
|
inputSchema: {
|
|
35548
35576
|
kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional().describe("Memory lifecycle kind; durable facts and handoffs are most common"),
|
|
35549
35577
|
project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
|
|
35550
|
-
replaceUri: external_exports.string().optional().describe(
|
|
35578
|
+
replaceUri: external_exports.string().optional().describe(
|
|
35579
|
+
"Optional viking:// memory URI to replace. Shared URIs are updated in place and pushed; personal URIs are forgotten after the replacement is safely stored."
|
|
35580
|
+
),
|
|
35551
35581
|
text: external_exports.string().optional().describe("Required memory text to store"),
|
|
35552
35582
|
sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, copilot, codex, or claude"),
|
|
35553
35583
|
status: external_exports.enum(["active", "archived", "superseded"]).optional().describe("Memory lifecycle status"),
|
|
@@ -35866,6 +35896,9 @@ async function readTextIfExists(path) {
|
|
|
35866
35896
|
async function writeDurableMemory(config2, params) {
|
|
35867
35897
|
try {
|
|
35868
35898
|
const ov = await requiredOpenVikingCli2();
|
|
35899
|
+
if (params.replaceUri && isInSharedNamespace(config2, params.replaceUri)) {
|
|
35900
|
+
return await writeSharedMemoryReplacement(config2, ov, params, params.replaceUri);
|
|
35901
|
+
}
|
|
35869
35902
|
const directoryUri = memoryDirectoryUri(config2, params.metadata);
|
|
35870
35903
|
await ensureMemoryDirectory(ov, config2, directoryUri);
|
|
35871
35904
|
const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
|
|
@@ -35906,6 +35939,40 @@ async function writeDurableMemory(config2, params) {
|
|
|
35906
35939
|
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
35907
35940
|
}
|
|
35908
35941
|
}
|
|
35942
|
+
async function writeSharedMemoryReplacement(config2, ov, params, targetUri) {
|
|
35943
|
+
if (params.metadata.kind !== "durable") {
|
|
35944
|
+
return argumentError("Shared memory replacement only supports durable memories.");
|
|
35945
|
+
}
|
|
35946
|
+
const teamName = sharedTeamNameForUri(config2, targetUri);
|
|
35947
|
+
if (!teamName) {
|
|
35948
|
+
return argumentError(`Memory ${targetUri} is not in the shared namespace.`);
|
|
35949
|
+
}
|
|
35950
|
+
const resolved = await resolveTeam(config2, teamName);
|
|
35951
|
+
const inferred = sharedMemoryUriParts(config2, targetUri);
|
|
35952
|
+
const metadata = {
|
|
35953
|
+
...params.metadata,
|
|
35954
|
+
project: params.metadata.project ?? inferred?.project,
|
|
35955
|
+
topic: params.metadata.topic ?? inferred?.topic
|
|
35956
|
+
};
|
|
35957
|
+
const rawMemory = formatMemoryDocument2("MEMORY", metadata, params.bodyText);
|
|
35958
|
+
const scrub = applyScrubber(stripPersonalProvenance(rawMemory), { redact: false });
|
|
35959
|
+
if (scrub.blocker) {
|
|
35960
|
+
return argumentError(
|
|
35961
|
+
`Refusing to update shared memory ${targetUri}: possible ${scrub.blocker}. Strip the sensitive value first.`
|
|
35962
|
+
);
|
|
35963
|
+
}
|
|
35964
|
+
await ensureSharedDirectoryChain(config2, ov, targetUri, false, { quiet: true });
|
|
35965
|
+
await writeMemoryFile(config2, ov, targetUri, scrub.cleaned, "replace", false, { quiet: true });
|
|
35966
|
+
const relativePath = vikingUriToWorktreeRelative(config2, targetUri, resolved.name);
|
|
35967
|
+
const messages = [`Updated shared memory: ${targetUri}`];
|
|
35968
|
+
for (const redaction of scrub.redactions) {
|
|
35969
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
|
|
35970
|
+
}
|
|
35971
|
+
messages.push(
|
|
35972
|
+
...await gitPublishWorkflow(resolved.config.worktree, relativePath, `share: update ${relativePath}`, true)
|
|
35973
|
+
);
|
|
35974
|
+
return { content: [{ type: "text", text: messages.join("\n") }] };
|
|
35975
|
+
}
|
|
35909
35976
|
async function runOpenVikingWriteWithRetry(ov, config2, memoryUri, args) {
|
|
35910
35977
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
35911
35978
|
const result = await runCommand(ov, args, { allowFailure: true });
|
package/dist/threadnote.cjs
CHANGED
|
@@ -8510,7 +8510,35 @@ function workfileToVikingUri(config, team, filePath) {
|
|
|
8510
8510
|
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
8511
8511
|
}
|
|
8512
8512
|
function isInSharedNamespace(config, uri) {
|
|
8513
|
-
return
|
|
8513
|
+
return sharedTeamNameForUri(config, uri) !== void 0;
|
|
8514
|
+
}
|
|
8515
|
+
function sharedTeamNameForUri(config, uri) {
|
|
8516
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
|
|
8517
|
+
if (!uri.startsWith(prefix)) {
|
|
8518
|
+
return void 0;
|
|
8519
|
+
}
|
|
8520
|
+
const [team] = uri.slice(prefix.length).split("/");
|
|
8521
|
+
return team || void 0;
|
|
8522
|
+
}
|
|
8523
|
+
function sharedMemoryUriParts(config, uri) {
|
|
8524
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
|
|
8525
|
+
if (!uri.startsWith(prefix)) {
|
|
8526
|
+
return void 0;
|
|
8527
|
+
}
|
|
8528
|
+
const [team, kind, scope, project, ...topicParts] = uri.slice(prefix.length).split("/");
|
|
8529
|
+
if (!team) {
|
|
8530
|
+
return void 0;
|
|
8531
|
+
}
|
|
8532
|
+
if (kind !== "durable" || scope !== "projects" || !project || topicParts.length === 0) {
|
|
8533
|
+
return { team };
|
|
8534
|
+
}
|
|
8535
|
+
const topicPath = topicParts.join("/");
|
|
8536
|
+
return {
|
|
8537
|
+
kind,
|
|
8538
|
+
project,
|
|
8539
|
+
team,
|
|
8540
|
+
topic: topicPath.endsWith(".md") ? topicPath.slice(0, -".md".length) : topicPath
|
|
8541
|
+
};
|
|
8514
8542
|
}
|
|
8515
8543
|
function isInTeamNamespace(config, uri, team) {
|
|
8516
8544
|
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`);
|
|
@@ -9434,6 +9462,10 @@ async function storeMemory(config, options) {
|
|
|
9434
9462
|
assertVikingUri(options.replaceUri);
|
|
9435
9463
|
}
|
|
9436
9464
|
const ov = await openVikingCliForMode(options.dryRun);
|
|
9465
|
+
if (options.replaceUri && isInSharedNamespace(config, options.replaceUri)) {
|
|
9466
|
+
await storeSharedMemoryReplacement(config, ov, options, options.replaceUri);
|
|
9467
|
+
return;
|
|
9468
|
+
}
|
|
9437
9469
|
const memoryPath = (0, import_node_path6.join)(config.agentContextHome, "last-memory.txt");
|
|
9438
9470
|
const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
|
|
9439
9471
|
const candidateMemory = formatMemoryDocument2(options.title, candidateMetadata, options.bodyText);
|
|
@@ -9484,6 +9516,49 @@ async function storeMemory(config, options) {
|
|
|
9484
9516
|
console.log(`Updated existing memory in place: ${memoryUri}`);
|
|
9485
9517
|
}
|
|
9486
9518
|
}
|
|
9519
|
+
async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
|
|
9520
|
+
if (options.metadata.kind !== "durable") {
|
|
9521
|
+
throw new Error("Shared memory replacement only supports durable memories.");
|
|
9522
|
+
}
|
|
9523
|
+
const teamName = sharedTeamNameForUri(config, targetUri);
|
|
9524
|
+
if (!teamName) {
|
|
9525
|
+
throw new Error(`Memory ${targetUri} is not in the shared namespace.`);
|
|
9526
|
+
}
|
|
9527
|
+
const team = await resolveTeam(config, teamName);
|
|
9528
|
+
const inferred = sharedMemoryUriParts(config, targetUri);
|
|
9529
|
+
const metadata = {
|
|
9530
|
+
...options.metadata,
|
|
9531
|
+
project: options.metadata.project ?? inferred?.project,
|
|
9532
|
+
topic: options.metadata.topic ?? inferred?.topic
|
|
9533
|
+
};
|
|
9534
|
+
const rawMemory = formatMemoryDocument2(options.title, metadata, options.bodyText);
|
|
9535
|
+
const scrub = applyScrubber(stripPersonalProvenance(rawMemory), { redact: false });
|
|
9536
|
+
if (scrub.blocker) {
|
|
9537
|
+
throw new Error(
|
|
9538
|
+
`Refusing to update shared memory ${targetUri}: possible ${scrub.blocker}. Strip the sensitive value first.`
|
|
9539
|
+
);
|
|
9540
|
+
}
|
|
9541
|
+
const memory = scrub.cleaned;
|
|
9542
|
+
const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
|
|
9543
|
+
if (options.dryRun) {
|
|
9544
|
+
console.log(memory);
|
|
9545
|
+
console.log("\nWould run:");
|
|
9546
|
+
}
|
|
9547
|
+
await ensureSharedDirectoryChain(config, ov, targetUri, options.dryRun);
|
|
9548
|
+
await writeMemoryFile(config, ov, targetUri, memory, "replace", options.dryRun);
|
|
9549
|
+
const git = await requiredExecutable("git");
|
|
9550
|
+
await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "add", "--", relativePath]);
|
|
9551
|
+
await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "commit", "-m", `share: update ${relativePath}`], {
|
|
9552
|
+
allowFailure: true
|
|
9553
|
+
});
|
|
9554
|
+
await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "push", DEFAULT_GIT_REMOTE_NAME], {
|
|
9555
|
+
allowFailure: true
|
|
9556
|
+
});
|
|
9557
|
+
for (const redaction of scrub.redactions) {
|
|
9558
|
+
console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
|
|
9559
|
+
}
|
|
9560
|
+
console.log(`Updated shared memory: ${targetUri}`);
|
|
9561
|
+
}
|
|
9487
9562
|
async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
|
|
9488
9563
|
const args = withIdentity(config, [
|
|
9489
9564
|
"write",
|
|
@@ -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
|
|