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 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 one
351
- after the new memory succeeds. Use `--kind`, `--project`, and `--topic` to store lifecycle-aware current knowledge.
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
@@ -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 uri.startsWith(`viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`);
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("Optional viking:// memory URI to forget after the new memory is safely stored"),
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 });
@@ -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 uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`);
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`. Avoid creating a new timestamped durable memory for every small progress note.
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; forget the existing shared copy first or pick a different topic name.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",