@plur-ai/mcp 0.21.0 → 0.21.1
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 +31 -0
- package/dist/{chunk-WA3JKTZ7.js → chunk-O2NJFMNP.js} +1 -1
- package/dist/{chunk-UINIIUBY.js → chunk-OETJSRYE.js} +205 -41
- package/dist/index.d.ts +8 -1
- package/dist/index.js +92 -54
- package/dist/server-EHKHU4JH.js +1006 -0
- package/dist/tools-export.js +2 -2
- package/package.json +2 -2
- package/dist/server-EUMEOVXB.js +0 -483
package/README.md
CHANGED
|
@@ -69,6 +69,37 @@ Less commonly needed tools (`plur_recall_hybrid`, `plur_inject_hybrid`, `plur_le
|
|
|
69
69
|
|
|
70
70
|
A `plur_*` name missing from `tools/list` means it moved behind the gateway, not that the server is down. `plur_admin { action: "help" }` returns every action with a one-line description and its argument schema; `plur_doctor` reports the same inventory as `tool_surface`.
|
|
71
71
|
|
|
72
|
+
## Folders where PLUR is off
|
|
73
|
+
|
|
74
|
+
Your folder map (`~/.plur/folders.yaml`, changed by `plur folders set` from a terminal) can turn PLUR off for a folder. The editor hooks already go silent there; the MCP server's memory tools do too.
|
|
75
|
+
|
|
76
|
+
**What is gated.** 33 tools — every tool that reads or writes engrams or episodes or returns their text: `plur_learn`, `plur_learn_batch`, `plur_recall`, `plur_recall_hybrid`, `plur_inject`, `plur_inject_hybrid`, `plur_session_start`, `plur_session_end`, `plur_capture`, `plur_timeline`, `plur_feedback`, `plur_pin`, `plur_forget`, `plur_ingest`, `plur_promote`, `plur_rescope`, `plur_episode_to_engram`, `plur_report_failure`, `plur_extract_meta`, `plur_meta_engrams`, `plur_validate_meta`, `plur_tensions`, `plur_tensions_purge`, `plur_similarity_search`, `plur_history`, `plur_provenance`, `plur_profile`, `plur_receipt`, `plur_packs_install`, `plur_packs_uninstall`, `plur_packs_export`, `plur_sync`, `plur_outbox` — called directly or through `plur_admin`. In an `off` folder they read and write no store, local or remote (no outbox row either), and return a normal, non-error answer:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{ "success": true, "plur": "off", "folder": "/work/secret", "message": "PLUR is off for this folder … plur folders set /work/secret --on" }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The message names every folder-map entry that turns the folder off (a parent folder or a glob can), with the command for each.
|
|
83
|
+
|
|
84
|
+
**What is not gated.** The admin and diagnostic tools keep working: `plur_status`, `plur_doctor`, `plur_stores_list`, `plur_stores_add`, `plur_sync_status`, `plur_packs_list`, `plur_packs_discover`, `plur_packs_preview`, `plur_scopes_discover`, `plur_suggest_scope`, `plur_session_scope`. Some of them still read stores — status, doctor and stores_list load them to count or probe them, and doctor runs a recall probe. What they can return: counts, health and configuration; A store that cannot be parsed is reported by the error's first line only (what went wrong, line and column) — in `plur_status` and in the error any tool returns — never by the file's lines. One exception: `plur_packs_preview` previews any pack directory the agent names, including an installed pack under your PLUR home, and then returns that pack's statements. Server startup is not gated yet: starting the server in an `off` folder can still register a `.plur/` store found there ([#1523](https://github.com/plur-ai/plur/issues/1523)).
|
|
85
|
+
|
|
86
|
+
**Which folder.** The editor's workspace: each root the client lists over MCP `roots/list`, plus the folder the server was started in. If any of them is `off`, the memory tools are off. The folder map is read on every call, so a change takes effect on the next one. Calls made while the roots are being fetched wait for them. If fetching them fails or times out, or a root does not resolve to a folder on this machine (`file://otherhost/…`, an encoded slash), that call does nothing (it answers `"reason": "workspace-unknown"`, not an error) and the next call asks again; it never falls back to the start folder alone. If the client's roots keep failing, memory stays off until they work. The roots answer is cached, and shared between calls made at the same time, only when the client declares `roots.listChanged`; otherwise every call sends its own roots request. A client that declares no roots capability at all, and was started outside the workspace, cannot be checked against it.
|
|
87
|
+
|
|
88
|
+
**A broken folder map fails safe.** If `folders.yaml` exists but cannot be read or parsed — including a dangling symlink, a symlink loop, an empty or comments-only file, and an unknown top-level key such as a misspelled `folder:` — the gated tools do nothing and answer with `"reason": "folder-map-unreadable"`, naming the file, the line and column, and the problem in plain words — quoting at most the key on that line, never a value (#1526). A misspelled `plur:` or `path:` key (`plru:`, `pth:`) counts as broken too. When `plur folders repair` can fix it, the answer carries `repair_summary` (what the repair changes, to show the user first) and `repair_command` (`plur folders repair --yes`, with `--path` for a non-default store) and tells the agent to run it only after the user agrees; otherwise it says the line must be fixed by hand. A repair adds no `on` to the map; folders with their own trusted `.plur.yaml` or project MCP config are on again, as before the map broke. `plur_status` and `plur_doctor` report the same problem as `folder_map` (and `plur_doctor` fails its `folder map` check). No `folders.yaml` at all (nothing at that path) means no decisions yet: every folder is undecided and gets the question below. A broken map offers no folder-question command and issues no nonce (only the repair command, when the repair can fix it). On first read, an old `trust.yaml` is imported into a new `folders.yaml` (a one-time core migration); otherwise the server never writes the map.
|
|
89
|
+
|
|
90
|
+
**An undecided folder gets the folder question** ([#1525](https://github.com/plur-ai/plur/issues/1525)). In a folder you have not decided about (folder map `ask`: no entry and no project marker, or a repo `.plur.yaml` asking for settings you have not trusted), the gated tools read and write no memory and answer, without an error, with the same question the editor hooks ask:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{ "success": true, "plur": "ask", "folder": "/work/app", "question": "[PLUR Memory — no decision for this folder yet …] …",
|
|
94
|
+
"answers": [
|
|
95
|
+
{ "label": "Yes, with the team scope group:acme/eng", "command": "plur folders set /work/app --scope group:acme/eng --nonce … --session mcp-…" },
|
|
96
|
+
{ "label": "Yes, without a team scope", "command": "plur folders set /work/app --on --nonce … --session mcp-…" },
|
|
97
|
+
{ "label": "Not now", "command": "plur folders set /work/app --not-now --nonce … --session mcp-…" },
|
|
98
|
+
{ "label": "Never here", "command": "plur folders set /work/app --off --nonce … --session mcp-…" } ] }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The agent asks you and runs the command for your answer (or you run it in a terminal). Each command carries its own single-use nonce, bound to that folder, that answer and this MCP session: a command from another session, or one that drops `--session`, is refused. Every memory call returns the same question, with the same nonces, until you answer; the next call after an answer follows it. **Yes** turns memory on, and the answer's team scope becomes this session's default write scope: an unscoped `plur_learn` or `plur_learn_batch` goes there, with or without `plur_session_start` (which also uses the folder map's scope for its default when you pass none). An unscoped `plur_recall` (either mode), `plur_recall_hybrid` or `plur_inject_hybrid` searches that team's store too, again with or without `plur_session_start`. An explicit scope still wins. With several workspace folders, a folder scope is used only when every folder (none left out, each through its real path) has the same one; the home folder, a folder above it or `/` as a workspace folder means no folder scope. A `plur_session_start` default holds only while the workspace folders, and the scope they resolve to, are the ones the session started with. **Never here** turns it off. **Not now** writes nothing to the folder map: memory stays off for the rest of this MCP session, without the question; the folder stays undecided, so the next session asks again. The server never writes the map itself. The session's unanswered nonces are deleted as soon as the session closes (stdin ends, or SIGTERM / SIGINT); the server then exits once the tool calls already running have answered (after stdin ends, however long they take; on a signal, within 2 s); a server killed outright leaves its nonce file, which the next server removes once the nonces have expired (24 h). Precedence across the workspace folders is off, then ask, then on. `off` is checked on every workspace folder, the server's start folder included. The question is never asked about your home folder, a filesystem root or a folder above home, whether it is a root the client sends or the start folder: a yes or a never-here there would cover every folder under it, so memory there runs as before. Otherwise it is asked about the client's roots when it sends any — the start folder is wherever the client happened to launch the server — and, when it sends none (or only such folders), about the start folder. "Not now" stops the question, not the other answers: the yes and never-here commands of the same question keep working for that session, as with the editor hooks.
|
|
102
|
+
|
|
72
103
|
## Sync across machines
|
|
73
104
|
|
|
74
105
|
Your agent can sync memory to any git remote:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-O2NJFMNP.js";
|
|
4
4
|
|
|
5
5
|
// src/telemetry.ts
|
|
6
6
|
import { recordEvent, flushIfNeeded, registerFlushOnExit } from "@plur-ai/core";
|
|
@@ -17,7 +17,27 @@ function recordTelemetry(event) {
|
|
|
17
17
|
import { existsSync, unlinkSync } from "fs";
|
|
18
18
|
import { join, dirname, resolve } from "path";
|
|
19
19
|
import { homedir } from "os";
|
|
20
|
-
import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, NO_SESSION, findProjectConfigPath, readProjectConfigFromPath, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName, SUGGEST_DISPLAY_MIN_CONFIDENCE, mcpRemoteWarningLine, doctorRemoteRemediation, normalizeEndpointUrl, REMOTE_STATUS_TTL_MS, PROBE_CLEARABLE_STATES,
|
|
20
|
+
import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, NO_SESSION, findProjectConfigPath, readProjectConfigFromPath, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName, SUGGEST_DISPLAY_MIN_CONFIDENCE, mcpRemoteWarningLine, doctorRemoteRemediation, normalizeEndpointUrl, REMOTE_STATUS_TTL_MS, PROBE_CLEARABLE_STATES, summariseProvenance, formatLayer3, renderProvenanceSummary, describeNeedsAction, summarizeOutbox, folderMapProblem, tokenEnvUnsetDetail, tokenEnvUnsetFix } from "@plur-ai/core";
|
|
21
|
+
|
|
22
|
+
// src/folder-map-advice.ts
|
|
23
|
+
import { folderRepairCommand } from "@plur-ai/core";
|
|
24
|
+
function folderMapAdvice(problem, root) {
|
|
25
|
+
const command = problem.fixable ? folderRepairCommand(root) : null;
|
|
26
|
+
if (command) {
|
|
27
|
+
const summary = problem.repair_summary;
|
|
28
|
+
return {
|
|
29
|
+
command,
|
|
30
|
+
...summary ? { summary } : {},
|
|
31
|
+
text: `PLUR can repair this${summary ? `; the repair changes ${summary}` : ""}. Show the user what is wrong and what the repair changes, and ask whether to repair the file (a backup is saved first; they can see the full change by running plur folders repair in a terminal). Only after the user agrees, run: ${command}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (problem.fixable) return { text: "The user can repair it by running plur folders repair in a terminal (it shows the change and asks first)." };
|
|
35
|
+
return {
|
|
36
|
+
text: problem.line !== void 0 ? `plur folders repair cannot fix this automatically: the user has to fix line ${problem.line} of that file by hand (plur folders repair then checks it).` : "plur folders repair cannot fix this automatically: the user has to fix or remove that file by hand."
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/tools.ts
|
|
21
41
|
import { z } from "zod";
|
|
22
42
|
function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
|
|
23
43
|
return async (prompt) => {
|
|
@@ -81,8 +101,9 @@ var recallHandler = async (args, plur) => {
|
|
|
81
101
|
// changes) establishes the remote dialing org context when no explicit
|
|
82
102
|
// scope filter is passed. Same rule as writes (E7, formal R2): not
|
|
83
103
|
// exactly one open session and no id → NO_SESSION, never the process
|
|
84
|
-
// slot the last-started session owns.
|
|
85
|
-
|
|
104
|
+
// slot the last-started session owns. #1566: with no session default
|
|
105
|
+
// of its own, the workspace's scope — the one resolver writes use.
|
|
106
|
+
session: await _readSession(args, plur)
|
|
86
107
|
});
|
|
87
108
|
const response2 = {
|
|
88
109
|
results: results.map((e) => {
|
|
@@ -92,7 +113,11 @@ var recallHandler = async (args, plur) => {
|
|
|
92
113
|
const measuredUnder = raw.measured_under;
|
|
93
114
|
const measuredAnnotation = measuredUnder ? " [measured under: " + Object.entries(measuredUnder).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join(", ") + "]" : "";
|
|
94
115
|
return {
|
|
95
|
-
id
|
|
116
|
+
// The id plur_learn returned for this engram (F3): a team row keeps
|
|
117
|
+
// its store prefix (`ENG-<PREFIX>-…`), so it never shares an id with
|
|
118
|
+
// a local engram minted the same day, and forget/feedback/pin route
|
|
119
|
+
// it to its store. A local engram's id has no prefix.
|
|
120
|
+
id: e.id,
|
|
96
121
|
statement: e.statement + annotation + measuredAnnotation,
|
|
97
122
|
type: e.type,
|
|
98
123
|
scope: e.scope,
|
|
@@ -126,8 +151,9 @@ var recallHandler = async (args, plur) => {
|
|
|
126
151
|
// MCP recall remote budget (#776)
|
|
127
152
|
// #243: session default scope (incl. mid-session plur_session_scope
|
|
128
153
|
// changes) establishes the remote dialing org context when no explicit
|
|
129
|
-
// scope filter is passed. Same rule as writes (E7, formal R2)
|
|
130
|
-
session
|
|
154
|
+
// scope filter is passed. Same rule as writes (E7, formal R2), and the
|
|
155
|
+
// same workspace scope when the session has no default (#1566).
|
|
156
|
+
session: await _readSession(args, plur)
|
|
131
157
|
});
|
|
132
158
|
recordTelemetry("recall");
|
|
133
159
|
const truncatedByCount = budget?.max_results != null && meta.engrams.length > cap;
|
|
@@ -156,7 +182,8 @@ var recallHandler = async (args, plur) => {
|
|
|
156
182
|
const measuredUnder = raw.measured_under;
|
|
157
183
|
const measuredAnnotation = measuredUnder ? " [measured under: " + Object.entries(measuredUnder).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join(", ") + "]" : "";
|
|
158
184
|
const base = {
|
|
159
|
-
id:
|
|
185
|
+
id: e.id,
|
|
186
|
+
// same id plur_learn returned — see the keyword branch (F3)
|
|
160
187
|
statement: e.statement + annotation + measuredAnnotation,
|
|
161
188
|
type: e.type,
|
|
162
189
|
scope: e.scope,
|
|
@@ -422,12 +449,51 @@ function trustCommand(dir, storageRoot, platform = process.platform) {
|
|
|
422
449
|
const store = _shellWord(resolve(storageRoot), platform);
|
|
423
450
|
return store === null ? null : `plur --path ${store} trust ${target}`;
|
|
424
451
|
}
|
|
452
|
+
function folderMapStatus(root) {
|
|
453
|
+
let p;
|
|
454
|
+
try {
|
|
455
|
+
p = folderMapProblem(root);
|
|
456
|
+
} catch {
|
|
457
|
+
return {};
|
|
458
|
+
}
|
|
459
|
+
if (!p) return {};
|
|
460
|
+
const advice = folderMapAdvice(p, root);
|
|
461
|
+
return {
|
|
462
|
+
folder_map: {
|
|
463
|
+
ok: false,
|
|
464
|
+
file: p.file,
|
|
465
|
+
problem: p.problem,
|
|
466
|
+
...p.line !== void 0 ? { line: p.line } : {},
|
|
467
|
+
...p.column !== void 0 ? { column: p.column } : {},
|
|
468
|
+
fixable: p.fixable,
|
|
469
|
+
...advice.command ? { repair_command: advice.command } : {},
|
|
470
|
+
...advice.summary ? { repair_summary: advice.summary } : {},
|
|
471
|
+
advice: `Memory tools are paused until it is fixed. ${advice.text}`
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function folderOnCommand(entry, storageRoot, platform = process.platform) {
|
|
476
|
+
if (_UNSAFE_PATH_CHARS.test(entry)) return null;
|
|
477
|
+
const target = _shellWord(entry, platform);
|
|
478
|
+
if (target === null) return null;
|
|
479
|
+
if (!storageRoot || resolve(storageRoot) === resolve(join(homedir(), ".plur"))) return `plur folders set ${target} --on`;
|
|
480
|
+
const store = _shellWord(resolve(storageRoot), platform);
|
|
481
|
+
return store === null ? null : `plur --path ${store} folders set ${target} --on`;
|
|
482
|
+
}
|
|
483
|
+
function redactStoreErrors(errors) {
|
|
484
|
+
return Object.fromEntries(Object.entries(errors).map(([k, v]) => [k, String(v).split("\n", 1)[0].slice(0, 300)]));
|
|
485
|
+
}
|
|
425
486
|
var UNTRUSTED_SCOPE_GRAMMAR = /^(?:global|[a-z][a-z0-9-]*:[A-Za-z0-9][A-Za-z0-9._@/:-]{0,199})$/;
|
|
426
487
|
var UNTRUSTED_DOMAIN_GRAMMAR = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/;
|
|
427
488
|
var _UNSAFE_PATH_CHARS = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/;
|
|
428
489
|
function _escapedText(s) {
|
|
429
490
|
return JSON.stringify(s.length > 1024 ? s.slice(0, 1024) + "\u2026" : s).replace(/[\u007f-\u009f\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
430
491
|
}
|
|
492
|
+
var FOLDER_SCOPE = /* @__PURE__ */ Symbol("plur.folderScope");
|
|
493
|
+
function _folderContext(args) {
|
|
494
|
+
const c = args[FOLDER_SCOPE];
|
|
495
|
+
return c && typeof c === "object" && typeof c.resolve === "function" ? c : null;
|
|
496
|
+
}
|
|
431
497
|
function readTrustedProjectConfig(trust) {
|
|
432
498
|
const configPath = findProjectConfigPath();
|
|
433
499
|
const raw = readProjectConfigFromPath(configPath);
|
|
@@ -472,6 +538,44 @@ var NO_SESSION_SLOT_WARNING = "No session is open, so this is the process-defaul
|
|
|
472
538
|
function _resolveWriteSession(args) {
|
|
473
539
|
return _resolveInjectionSession(args) ?? NO_SESSION;
|
|
474
540
|
}
|
|
541
|
+
var FOLDER_SCOPE_SESSION_PREFIX = "\0plur:folder-scope:";
|
|
542
|
+
function _knownSession(id, plur, viaServer) {
|
|
543
|
+
if (id.startsWith("\0")) return false;
|
|
544
|
+
if (_sessionTelemetry.has(id)) return true;
|
|
545
|
+
if (viaServer) return false;
|
|
546
|
+
try {
|
|
547
|
+
return plur.trackedSessionScopes().includes(id);
|
|
548
|
+
} catch {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async function _writeSession(args, plur, base, read = false) {
|
|
553
|
+
const explicit = typeof args.session_id === "string" && args.session_id.length > 0 ? args.session_id : void 0;
|
|
554
|
+
const chosen = base !== void 0 ? base : explicit ?? _implicitSessionId();
|
|
555
|
+
const ctx = _folderContext(args);
|
|
556
|
+
if (!ctx) return chosen ?? NO_SESSION;
|
|
557
|
+
const session = chosen !== void 0 && _knownSession(chosen, plur, true) ? chosen : NO_SESSION;
|
|
558
|
+
let ws = null;
|
|
559
|
+
try {
|
|
560
|
+
ws = read && ctx.admitted ? ctx.admitted() : await ctx.resolve();
|
|
561
|
+
} catch {
|
|
562
|
+
ws = null;
|
|
563
|
+
}
|
|
564
|
+
if (session !== NO_SESSION) {
|
|
565
|
+
const record = _sessionTelemetry.get(session);
|
|
566
|
+
const own = plur.getSessionScope({ session });
|
|
567
|
+
if (record?.scope_adjusted) return session;
|
|
568
|
+
if (record && own != null && ws !== null && record.workspace_key === ws.key && record.workspace_scope === ws.scope) return session;
|
|
569
|
+
}
|
|
570
|
+
const scope = ws?.scope ?? null;
|
|
571
|
+
if (scope === null) return NO_SESSION;
|
|
572
|
+
const key = FOLDER_SCOPE_SESSION_PREFIX + scope;
|
|
573
|
+
plur.setSessionScope(scope, { session: key });
|
|
574
|
+
return key;
|
|
575
|
+
}
|
|
576
|
+
function _readSession(args, plur) {
|
|
577
|
+
return _writeSession(args, plur, void 0, true);
|
|
578
|
+
}
|
|
475
579
|
function _resolveScopeSession(args) {
|
|
476
580
|
const explicit = args.session_id;
|
|
477
581
|
if (typeof explicit === "string" && explicit.length > 0) {
|
|
@@ -587,7 +691,9 @@ function buildAdminDispatchTool(all) {
|
|
|
587
691
|
return { ...validated.errorPayload, error: inner.startsWith(`${action}:`) ? inner : `${action}: ${inner}` };
|
|
588
692
|
}
|
|
589
693
|
try {
|
|
590
|
-
|
|
694
|
+
const carried = args[FOLDER_SCOPE];
|
|
695
|
+
const data = carried !== void 0 ? { ...validated.data, [FOLDER_SCOPE]: carried } : validated.data;
|
|
696
|
+
return await target.handler(data, plur);
|
|
591
697
|
} catch (err) {
|
|
592
698
|
const message = err?.message ?? String(err);
|
|
593
699
|
throw new Error(message.startsWith(`${action}:`) ? message : `${action}: ${message}`);
|
|
@@ -762,13 +868,15 @@ function getAllToolDefinitions() {
|
|
|
762
868
|
// #243: resolve which session's default scope governs this write —
|
|
763
869
|
// explicit session_id first, else the lone open session. Never
|
|
764
870
|
// persisted on the engram (LearnContext.session selects a scope, it
|
|
765
|
-
// is not part of one).
|
|
766
|
-
|
|
871
|
+
// is not part of one). The workspace's answer where the session
|
|
872
|
+
// gives none, or its roots changed (#1562, #1563 review round 2).
|
|
873
|
+
session: await _writeSession(args, plur),
|
|
767
874
|
llm
|
|
768
875
|
};
|
|
769
876
|
const explicitScope = typeof args.scope === "string" && args.scope.length > 0;
|
|
770
|
-
const scopeHint = (engramScope, wasRouted) => {
|
|
877
|
+
const scopeHint = (engramScope, wasRouted, delivery) => {
|
|
771
878
|
if (explicitScope || wasRouted || isSharedScope(engramScope)) return {};
|
|
879
|
+
if (delivery !== "local") return {};
|
|
772
880
|
let remote = [];
|
|
773
881
|
try {
|
|
774
882
|
remote = plur.getWritableRemoteScopes();
|
|
@@ -776,6 +884,7 @@ function getAllToolDefinitions() {
|
|
|
776
884
|
return {};
|
|
777
885
|
}
|
|
778
886
|
if (remote.length === 0) return {};
|
|
887
|
+
if (remote.some((s) => s.scope === engramScope)) return {};
|
|
779
888
|
const scopes = remote.map((s) => `"${s.scope}"`).join(", ");
|
|
780
889
|
return { scope_hint: `Stored at "${engramScope}" because no scope was passed, but a team store is configured (${scopes}). If this is team/engineering knowledge, re-learn it with an explicit scope so it reaches the shared store; keep genuinely personal notes at the default scope.` };
|
|
781
890
|
};
|
|
@@ -823,7 +932,7 @@ function getAllToolDefinitions() {
|
|
|
823
932
|
const routeRefused = engram.structured_data?._routeRefused;
|
|
824
933
|
mcpCanary.signal("learn_activity");
|
|
825
934
|
recordTelemetry("learn");
|
|
826
|
-
const dedup = isOutbox ? void 0 : await plur.nearDuplicates(statement, context, engram
|
|
935
|
+
const dedup = isOutbox ? void 0 : await plur.nearDuplicates(statement, context, plur.readIdFor(engram));
|
|
827
936
|
const redraft = (() => {
|
|
828
937
|
const ids = args.supersedes;
|
|
829
938
|
if (!ids?.length) return void 0;
|
|
@@ -861,6 +970,8 @@ function getAllToolDefinitions() {
|
|
|
861
970
|
// more specific `warning` below (outbox, demotion, refusal) still
|
|
862
971
|
// wins that key; `delivery_warning` keeps this one either way.
|
|
863
972
|
delivery: delivered.delivery,
|
|
973
|
+
// 0.21.1: why it was queued (rejected token vs unreachable server).
|
|
974
|
+
...delivered.reason ? { delivery_reason: delivered.reason, delivery_reason_code: delivered.reason_code } : {},
|
|
864
975
|
...delivered.warning ? { delivery_warning: delivered.warning, warning: delivered.warning } : {},
|
|
865
976
|
...dedup?.near_duplicates?.length ? { dedup } : {},
|
|
866
977
|
...redraft ? { redraft } : {},
|
|
@@ -869,9 +980,9 @@ function getAllToolDefinitions() {
|
|
|
869
980
|
return c ? { composition: c } : {};
|
|
870
981
|
})(),
|
|
871
982
|
...temporalEcho(engram),
|
|
872
|
-
...scopeHint(engram.scope, !!routed),
|
|
983
|
+
...scopeHint(engram.scope, !!routed, delivered.delivery),
|
|
873
984
|
...domainHint(!!routed),
|
|
874
|
-
...isOutbox ? { outbox: true, warning: "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {},
|
|
985
|
+
...isOutbox ? { outbox: true, warning: delivered.reason ?? "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {},
|
|
875
986
|
...demoted ? { demoted: true, requested_scope: demoted.from, warning: `Sensitive content (${demoted.patterns}) detected \u2014 stored at "${demoted.to}"/private instead of the requested shared scope "${demoted.from}". If this is a false positive, re-scope deliberately.` } : {},
|
|
876
987
|
...routed ? { routed: { scope: routed.scope, confidence: routed.confidence, reason: routed.reason }, info: `No scope was provided; auto-routed to "${routed.scope}" (confidence ${routed.confidence}) because its content matched that scope's covers. Pass an explicit scope to override.` } : {},
|
|
877
988
|
// #1115: a shared scope matched but was NOT adopted. Said plainly,
|
|
@@ -902,9 +1013,10 @@ function getAllToolDefinitions() {
|
|
|
902
1013
|
type: engram.type,
|
|
903
1014
|
...learnDecision(engram),
|
|
904
1015
|
delivery: delivered.delivery,
|
|
1016
|
+
...delivered.reason ? { delivery_reason: delivered.reason, delivery_reason_code: delivered.reason_code } : {},
|
|
905
1017
|
...delivered.warning ? { delivery_warning: delivered.warning } : {},
|
|
906
1018
|
...temporalEcho(engram),
|
|
907
|
-
...scopeHint(engram.scope, !!routedFallback),
|
|
1019
|
+
...scopeHint(engram.scope, !!routedFallback, delivered.delivery),
|
|
908
1020
|
...domainHint(!!routedFallback),
|
|
909
1021
|
...isOutbox ? { outbox: true } : {},
|
|
910
1022
|
// The routed write can fail for reasons that have nothing to do
|
|
@@ -966,7 +1078,7 @@ function getAllToolDefinitions() {
|
|
|
966
1078
|
if (raw.length === 0) {
|
|
967
1079
|
return { ids: [], results: [], stats: { added: 0, updated: 0, merged: 0, noops: 0, failed: 0 }, failures: [], warning: "No engrams provided \u2014 pass a non-empty `engrams` array." };
|
|
968
1080
|
}
|
|
969
|
-
const batchSession =
|
|
1081
|
+
const batchSession = await _writeSession(args, plur);
|
|
970
1082
|
const projectDomain = readTrustedProjectConfig(plur).domain ?? void 0;
|
|
971
1083
|
const items = raw.map((e) => ({
|
|
972
1084
|
statement: sanitizeStatement(e.statement),
|
|
@@ -1066,9 +1178,14 @@ function getAllToolDefinitions() {
|
|
|
1066
1178
|
const isOutbox = !!r.engram.structured_data?._outbox;
|
|
1067
1179
|
const routed = r.engram.structured_data?._routed;
|
|
1068
1180
|
const routeRefused = r.engram.structured_data?._routeRefused;
|
|
1181
|
+
const requested = r.input_index !== void 0 ? raw[r.input_index]?.scope : void 0;
|
|
1182
|
+
const delivered = plur.deliveryOf(r.engram, typeof requested === "string" ? requested : void 0);
|
|
1069
1183
|
return {
|
|
1070
1184
|
input_index: r.input_index,
|
|
1071
1185
|
id: isOutbox ? r.engram.id : plur.readIdFor(r.engram),
|
|
1186
|
+
delivery: delivered.delivery,
|
|
1187
|
+
...delivered.reason ? { delivery_reason: delivered.reason, delivery_reason_code: delivered.reason_code } : {},
|
|
1188
|
+
...delivered.warning ? { delivery_warning: delivered.warning } : {},
|
|
1072
1189
|
statement: r.engram.statement,
|
|
1073
1190
|
scope: r.engram.scope,
|
|
1074
1191
|
type: r.engram.type,
|
|
@@ -1091,7 +1208,7 @@ function getAllToolDefinitions() {
|
|
|
1091
1208
|
},
|
|
1092
1209
|
{
|
|
1093
1210
|
name: "plur_recall",
|
|
1094
|
-
description: 'Search engrams by topic. Default mode is hybrid (BM25 + local embeddings via RRF) \u2014 set mode:"keyword" for BM25-only. Local search plus, when a configured enterprise store is part of the current project/work, one live timeout-bounded recall per remote host merged in (a `remote_stores` block + warning appears when a host is degraded; no host configured or implicated = fully local). Note: a project-scope filter also returns personal-family engrams (local, global, user:*, agent:*); an explicit scope=global recall returns ALL personal-family engrams \u2014 wider than scope=global INJECT, which is targeted to the global namespace only.',
|
|
1211
|
+
description: 'Search engrams by topic. Default mode is hybrid (BM25 + local embeddings via RRF) \u2014 set mode:"keyword" for BM25-only. Local search plus, when a configured enterprise store is part of the current project/work \u2014 or when the scope (or the default scope this session registered itself) is a personal user: scope and a remote store is configured with that same scope (exact match, case-insensitive; that rule adds only the matching store, though a store set to dial: always or a trusted project remote can still add others) \u2014 one live timeout-bounded recall per remote host merged in (a `remote_stores` block + warning appears when a host is degraded; no host configured or implicated = fully local). Note: a project-scope filter also returns personal-family engrams (local, global, user:*, agent:*); an explicit scope=global recall returns ALL personal-family engrams \u2014 wider than scope=global INJECT, which is targeted to the global namespace only.',
|
|
1095
1212
|
annotations: { title: "Recall", readOnlyHint: true, idempotentHint: true },
|
|
1096
1213
|
inputSchema: {
|
|
1097
1214
|
type: "object",
|
|
@@ -1118,7 +1235,7 @@ function getAllToolDefinitions() {
|
|
|
1118
1235
|
},
|
|
1119
1236
|
{
|
|
1120
1237
|
name: "plur_recall_hybrid",
|
|
1121
|
-
description: "[Deprecated since 0.16 \u2014 use plur_recall (mode defaults to hybrid). Alias kept for backwards compatibility; removal earliest 0.18.] Hybrid search \u2014 BM25 + local embeddings merged via Reciprocal Rank Fusion, plus the live enterprise-store recall leg when one is configured and project-relevant.",
|
|
1238
|
+
description: "[Deprecated since 0.16 \u2014 use plur_recall (mode defaults to hybrid). Alias kept for backwards compatibility; removal earliest 0.18.] Hybrid search \u2014 BM25 + local embeddings merged via Reciprocal Rank Fusion, plus the live enterprise-store recall leg when one is configured and project-relevant, or when the scope is a personal user: scope matching the own scope of a configured remote store (exact, case-insensitive; adds only that store, while dial: always and a trusted project remote still apply).",
|
|
1122
1239
|
annotations: { title: "Recall (hybrid) [deprecated alias]", readOnlyHint: true, idempotentHint: true },
|
|
1123
1240
|
inputSchema: {
|
|
1124
1241
|
type: "object",
|
|
@@ -1200,7 +1317,11 @@ function getAllToolDefinitions() {
|
|
|
1200
1317
|
budget: args.budget,
|
|
1201
1318
|
scope: args.scope,
|
|
1202
1319
|
source: "inject",
|
|
1203
|
-
session_id
|
|
1320
|
+
session_id,
|
|
1321
|
+
// #1566: the dialing context follows the same rule as writes — the
|
|
1322
|
+
// workspace's scope when the session has no default of its own.
|
|
1323
|
+
// session_id above stays the caller's for attribution.
|
|
1324
|
+
dial_session: await _readSession(args, plur)
|
|
1204
1325
|
});
|
|
1205
1326
|
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
1206
1327
|
const response = {
|
|
@@ -1269,8 +1390,8 @@ function getAllToolDefinitions() {
|
|
|
1269
1390
|
return { mode: "batch", results, summary };
|
|
1270
1391
|
}
|
|
1271
1392
|
try {
|
|
1272
|
-
await plur.feedback(args.id, args.signal, args.scope);
|
|
1273
|
-
return { success: true, id: args.id, signal: args.signal };
|
|
1393
|
+
const { warnings } = await plur.feedback(args.id, args.signal, args.scope);
|
|
1394
|
+
return { success: true, id: args.id, signal: args.signal, ...warnings.length > 0 ? { warnings } : {} };
|
|
1274
1395
|
} catch (err) {
|
|
1275
1396
|
if (err.message?.includes("readonly store")) {
|
|
1276
1397
|
return { success: false, id: args.id, signal: args.signal, note: "Engram is in a readonly store. Feedback noted for this session but not persisted." };
|
|
@@ -1288,7 +1409,8 @@ function getAllToolDefinitions() {
|
|
|
1288
1409
|
properties: {
|
|
1289
1410
|
id: { type: "string", description: "Engram ID to pin or unpin" },
|
|
1290
1411
|
pinned: { type: "boolean", description: "Target value (default true)" },
|
|
1291
|
-
list: { type: "boolean", description: "If true, just return the current set of pinned engrams (no mutation)" }
|
|
1412
|
+
list: { type: "boolean", description: "If true, just return the current set of pinned engrams (no mutation)" },
|
|
1413
|
+
scope: { type: "string", description: `Which store holds it. Ids are minted per store, so one bare id can name a local engram and an unrelated remote one; such an id is refused. Pass "primary" for the local engram, or a remote store's scope (or the namespaced ENG-XXX-\u2026 id from recall) for the remote one.` }
|
|
1292
1414
|
}
|
|
1293
1415
|
},
|
|
1294
1416
|
handler: async (args, plur) => {
|
|
@@ -1305,7 +1427,7 @@ function getAllToolDefinitions() {
|
|
|
1305
1427
|
if (!args.id) throw new Error("Provide id (or list:true to list pinned)");
|
|
1306
1428
|
const target = args.pinned ?? true;
|
|
1307
1429
|
if (target === true) {
|
|
1308
|
-
const q = await plur.pinnedQuota(args.id);
|
|
1430
|
+
const q = await plur.pinnedQuota(args.id, args.scope ? { scope: args.scope } : void 0);
|
|
1309
1431
|
if (q.candidate && !q.candidate.fits) {
|
|
1310
1432
|
const deficit = q.candidate.would_be - q.quota;
|
|
1311
1433
|
const covering = [];
|
|
@@ -1330,7 +1452,7 @@ function getAllToolDefinitions() {
|
|
|
1330
1452
|
};
|
|
1331
1453
|
}
|
|
1332
1454
|
}
|
|
1333
|
-
const updated = await plur.setPinnedAsync(args.id, target);
|
|
1455
|
+
const updated = await plur.setPinnedAsync(args.id, target, args.scope ? { scope: args.scope } : void 0);
|
|
1334
1456
|
if (!updated) throw new Error(`Engram not found: ${args.id}`);
|
|
1335
1457
|
return {
|
|
1336
1458
|
id: updated.id,
|
|
@@ -1361,18 +1483,18 @@ function getAllToolDefinitions() {
|
|
|
1361
1483
|
const engram = scope ? void 0 : await plur.getById(args.id);
|
|
1362
1484
|
if (engram) {
|
|
1363
1485
|
if (engram.status === "retired") return { success: false, error: `Already retired: ${args.id}` };
|
|
1364
|
-
await plur.forget(args.id, args.reason, { force: true });
|
|
1365
|
-
return { success: true, retired: { id: engram.id, statement: engram.statement } };
|
|
1486
|
+
const { warnings: warnings2 } = await plur.forget(args.id, args.reason, { force: true });
|
|
1487
|
+
return { success: true, retired: { id: engram.id, statement: engram.statement }, ...warnings2.length > 0 ? { warnings: warnings2 } : {} };
|
|
1366
1488
|
}
|
|
1367
|
-
await plur.forget(args.id, args.reason, { force: true, ...scope ? { scope } : {} });
|
|
1368
|
-
return { success: true, retired: { id: args.id, ...scope ? { scope } : {} } };
|
|
1489
|
+
const { warnings } = await plur.forget(args.id, args.reason, { force: true, ...scope ? { scope } : {} });
|
|
1490
|
+
return { success: true, retired: { id: args.id, ...scope ? { scope } : {} }, ...warnings.length > 0 ? { warnings } : {} };
|
|
1369
1491
|
}
|
|
1370
1492
|
if (args.search) {
|
|
1371
1493
|
const matches = await plur.recall(args.search, { limit: 100, remote: false });
|
|
1372
1494
|
if (matches.length === 0) return { success: false, error: `No active engrams matching "${args.search}"` };
|
|
1373
1495
|
if (matches.length === 1) {
|
|
1374
|
-
await plur.forget(matches[0].id, args.reason, { force: true });
|
|
1375
|
-
return { success: true, retired: { id: matches[0].id, statement: matches[0].statement } };
|
|
1496
|
+
const { warnings } = await plur.forget(matches[0].id, args.reason, { force: true });
|
|
1497
|
+
return { success: true, retired: { id: matches[0].id, statement: matches[0].statement }, ...warnings.length > 0 ? { warnings } : {} };
|
|
1376
1498
|
}
|
|
1377
1499
|
return {
|
|
1378
1500
|
success: false,
|
|
@@ -1918,7 +2040,7 @@ function getAllToolDefinitions() {
|
|
|
1918
2040
|
// Artifacts that could not be read (audit 2026-08-03, finding 14).
|
|
1919
2041
|
// Core reports these; this hand-built response dropped them, so an
|
|
1920
2042
|
// agent asking for status saw a healthy-looking `pack_count: 0`.
|
|
1921
|
-
...status.store_errors ? { store_errors: status.store_errors } : {},
|
|
2043
|
+
...status.store_errors ? { store_errors: redactStoreErrors(status.store_errors) } : {},
|
|
1922
2044
|
// Spreading-activation drop counters — absent when both are zero.
|
|
1923
2045
|
...status.spread_drops ? { spread_drops: status.spread_drops } : {},
|
|
1924
2046
|
// Version check (issue #151)
|
|
@@ -1929,7 +2051,11 @@ function getAllToolDefinitions() {
|
|
|
1929
2051
|
behind: minorVersionsBehind(versionCheck.current, versionCheck.latest)
|
|
1930
2052
|
}
|
|
1931
2053
|
} : {},
|
|
1932
|
-
capabilities: await mcpCanary.status()
|
|
2054
|
+
capabilities: await mcpCanary.status(),
|
|
2055
|
+
// #1526: a broken folders.yaml pauses every memory tool. Status is
|
|
2056
|
+
// where an agent looks, so it names the line, the problem in plain
|
|
2057
|
+
// words, and the repair (run only after the user agrees).
|
|
2058
|
+
...folderMapStatus(plur.storageRoot)
|
|
1933
2059
|
};
|
|
1934
2060
|
}
|
|
1935
2061
|
},
|
|
@@ -2212,6 +2338,9 @@ function getAllToolDefinitions() {
|
|
|
2212
2338
|
detail: soon ? `Reachable, but token expires in ${h.tokenExpiresInDays}d \u2014 reauth soon` : `Reachable, auth valid${expiresNote}`
|
|
2213
2339
|
});
|
|
2214
2340
|
if (soon) remediation.push(`Remote ${h.url}: token expires in ${h.tokenExpiresInDays}d \u2014 mint a new token (<host>/me/api-keys), update ~/.plur/config.yaml, restart.`);
|
|
2341
|
+
} else if (h.tokenEnvUnset) {
|
|
2342
|
+
checks.push({ check: `remote store: ${h.url}`, ok: false, detail: tokenEnvUnsetDetail(h.tokenEnvUnset, h.scopes?.[0] ?? h.url) });
|
|
2343
|
+
remediation.push(tokenEnvUnsetFix(h.url, h.tokenEnvUnset));
|
|
2215
2344
|
} else if (h.status === "auth_expired") {
|
|
2216
2345
|
checks.push({ check: `remote store: ${h.url}`, ok: false, detail: `AUTH FAILED${expiresNote} \u2014 team-scoped writes are queuing to the outbox, not syncing. (${h.reason ?? ""})` });
|
|
2217
2346
|
remediation.push(`Remote ${h.url}: re-authenticate \u2014 open <host>/auth/github (or <host>/me/api-keys) in a browser, paste the token into ~/.plur/config.yaml, then restart Claude/MCP so it reloads. Queued engrams flush on next session_start.`);
|
|
@@ -2257,9 +2386,18 @@ function getAllToolDefinitions() {
|
|
|
2257
2386
|
} catch {
|
|
2258
2387
|
}
|
|
2259
2388
|
const tool_surface = describeToolSurface();
|
|
2389
|
+
const folderMap = folderMapStatus(plur.storageRoot);
|
|
2390
|
+
if (folderMap.folder_map) {
|
|
2391
|
+
const fm = folderMap.folder_map;
|
|
2392
|
+
checks.push({ check: "folder map", ok: false, detail: `${fm.file} ${fm.problem}` });
|
|
2393
|
+
remediation.push(`Folder map: ${fm.file} ${fm.problem}. ${fm.advice}`);
|
|
2394
|
+
} else {
|
|
2395
|
+
checks.push({ check: "folder map", ok: true, detail: "folders.yaml is readable (or absent: no decisions yet)" });
|
|
2396
|
+
}
|
|
2260
2397
|
return {
|
|
2261
2398
|
ok: checks.every((c) => c.ok),
|
|
2262
2399
|
checks,
|
|
2400
|
+
...folderMap,
|
|
2263
2401
|
embedder: {
|
|
2264
2402
|
before_probe: before,
|
|
2265
2403
|
after_probe: after
|
|
@@ -2323,16 +2461,30 @@ function getAllToolDefinitions() {
|
|
|
2323
2461
|
});
|
|
2324
2462
|
const projectConfig = readTrustedProjectConfig(plur);
|
|
2325
2463
|
const explicit_default_scope = args.default_scope ?? null;
|
|
2326
|
-
const
|
|
2327
|
-
|
|
2464
|
+
const folderCtx = _folderContext(args);
|
|
2465
|
+
let workspace = null;
|
|
2466
|
+
if (folderCtx) {
|
|
2467
|
+
try {
|
|
2468
|
+
workspace = await folderCtx.resolve();
|
|
2469
|
+
} catch {
|
|
2470
|
+
workspace = null;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
const folder_scope = folderCtx ? workspace?.scope ?? null : null;
|
|
2474
|
+
const default_scope = explicit_default_scope ?? (folderCtx ? folder_scope : projectConfig.scope ?? null) ?? null;
|
|
2475
|
+
const scope_source = explicit_default_scope ? "caller" : folder_scope && folder_scope !== projectConfig.scope ? "folder-map" : default_scope ? "project-config" : "none";
|
|
2328
2476
|
const default_domain = projectConfig.domain ?? null;
|
|
2329
|
-
plur.setSessionScope(default_scope);
|
|
2477
|
+
if (!folderCtx) plur.setSessionScope(default_scope);
|
|
2330
2478
|
plur.setSessionScope(default_scope, { session: session_id });
|
|
2331
2479
|
{
|
|
2332
2480
|
const t = _sessionTelemetry.get(session_id);
|
|
2333
2481
|
if (t) {
|
|
2334
2482
|
t.default_scope = default_scope;
|
|
2335
2483
|
t.default_scope_source = scope_source;
|
|
2484
|
+
if (workspace) {
|
|
2485
|
+
t.workspace_key = workspace.key;
|
|
2486
|
+
t.workspace_scope = workspace.scope;
|
|
2487
|
+
}
|
|
2336
2488
|
}
|
|
2337
2489
|
}
|
|
2338
2490
|
const status = await plur.status().catch(() => null);
|
|
@@ -2341,7 +2493,7 @@ function getAllToolDefinitions() {
|
|
|
2341
2493
|
episode_count: status?.episode_count ?? 0,
|
|
2342
2494
|
pack_count: status?.pack_count ?? 0
|
|
2343
2495
|
};
|
|
2344
|
-
const store_errors = status?.store_errors;
|
|
2496
|
+
const store_errors = status?.store_errors ? redactStoreErrors(status.store_errors) : void 0;
|
|
2345
2497
|
await plur.warmRemoteCaches().catch(() => {
|
|
2346
2498
|
});
|
|
2347
2499
|
let engrams = null;
|
|
@@ -2418,7 +2570,11 @@ ${guide}`;
|
|
|
2418
2570
|
|
|
2419
2571
|
${guide}`;
|
|
2420
2572
|
}
|
|
2421
|
-
if (scope_source === "
|
|
2573
|
+
if (scope_source === "folder-map") {
|
|
2574
|
+
guide += `
|
|
2575
|
+
|
|
2576
|
+
This folder's scope: "${default_scope}" (from your folder map, plur folders). plur_learn calls without an explicit scope will be tagged with this scope. Pass scope: "global" only for genuinely cross-project knowledge.`;
|
|
2577
|
+
} else if (scope_source === "project-config") {
|
|
2422
2578
|
guide += `
|
|
2423
2579
|
|
|
2424
2580
|
Auto-detected project scope: "${default_scope}" (from .plur.yaml in the current project). plur_learn calls without an explicit scope will be tagged with this scope, keeping this project's knowledge separate from your other projects. Pass scope: "global" only for genuinely cross-project knowledge (general coding conventions, language gotchas, tool quirks).`;
|
|
@@ -2612,7 +2768,8 @@ Tool profile "${session_tool_profile}": most plur_* tools are not exposed by nam
|
|
|
2612
2768
|
...warning ? { warning } : {}
|
|
2613
2769
|
});
|
|
2614
2770
|
}
|
|
2615
|
-
const
|
|
2771
|
+
const clearCtx = _folderContext(args);
|
|
2772
|
+
const restored = record !== void 0 ? record.default_scope ?? null : clearCtx ? null : readTrustedProjectConfig(plur).scope ?? null;
|
|
2616
2773
|
const restored_source = record !== void 0 ? record.default_scope_source === "caller" ? "session-start" : record.default_scope_source ?? "none" : restored != null ? "project-config" : "none";
|
|
2617
2774
|
const { previous, next } = plur.adjustSessionScope(restored, { session, reason, trigger: "clear" });
|
|
2618
2775
|
if (record) record.scope_adjusted = false;
|
|
@@ -2700,8 +2857,9 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2700
2857
|
try {
|
|
2701
2858
|
await plur.learnRouted(sanitizeStatement(statement), {
|
|
2702
2859
|
type,
|
|
2703
|
-
// E7: no resolvable session → no session default
|
|
2704
|
-
|
|
2860
|
+
// E7: no resolvable session → no session default; through the
|
|
2861
|
+
// server, the workspace's answer (#1563 review round 2).
|
|
2862
|
+
session: await _writeSession(args, plur, endSession),
|
|
2705
2863
|
domain: projectDomain,
|
|
2706
2864
|
// Link the engram back to the session that produced it (#960).
|
|
2707
2865
|
session_episode_id: episode.id,
|
|
@@ -3135,6 +3293,9 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
3135
3293
|
handler: async (args, plur) => {
|
|
3136
3294
|
const engram = await plur.episodeToEngram(args.episode_id, {
|
|
3137
3295
|
scope: args.scope,
|
|
3296
|
+
// The same default as every other new write (#1563 review round 3,
|
|
3297
|
+
// N1): never the process-wide slot.
|
|
3298
|
+
session: await _writeSession(args, plur),
|
|
3138
3299
|
domain: args.domain,
|
|
3139
3300
|
tags: args.tags
|
|
3140
3301
|
});
|
|
@@ -3365,9 +3526,12 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
3365
3526
|
}
|
|
3366
3527
|
|
|
3367
3528
|
export {
|
|
3529
|
+
folderMapAdvice,
|
|
3368
3530
|
registerFlushOnExit,
|
|
3369
3531
|
validateToolArgs,
|
|
3370
3532
|
mcpCanary,
|
|
3533
|
+
folderOnCommand,
|
|
3534
|
+
FOLDER_SCOPE,
|
|
3371
3535
|
CURSOR_CORE_TOOL_NAMES,
|
|
3372
3536
|
resolveToolProfile,
|
|
3373
3537
|
setActiveToolProfile,
|
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ interface HookEntry {
|
|
|
48
48
|
async?: boolean;
|
|
49
49
|
}>;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Write the PLUR section into CLAUDE.md with core's `upsertInstructionSection`
|
|
53
|
+
* — the same logic as `plur init`: only a section PLUR shipped is replaced,
|
|
54
|
+
* one the user wrote or edited is kept and the new one added beside it, and a
|
|
55
|
+
* timestamped backup precedes any change to an existing file.
|
|
56
|
+
*/
|
|
57
|
+
declare function installClaudeMd(claudeMdPath?: string): Promise<string>;
|
|
51
58
|
/**
|
|
52
59
|
* Merge PLUR hooks into Claude Code settings (#1279). No I/O apart from
|
|
53
60
|
* reading plur-hook.meta.json for the exec-form check.
|
|
@@ -72,4 +79,4 @@ declare function applyPlurHooks(settings: Settings, hooksMap: Record<string, Hoo
|
|
|
72
79
|
status: 'installed' | 'healed' | 'already';
|
|
73
80
|
};
|
|
74
81
|
|
|
75
|
-
export { type HookEntry, type Settings, applyPlurHooks, buildPlurHooks, compareSemver, extractManifestVersion };
|
|
82
|
+
export { type HookEntry, type Settings, applyPlurHooks, buildPlurHooks, compareSemver, extractManifestVersion, installClaudeMd };
|