@plur-ai/cli 0.9.9 → 0.9.11
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 +3 -2
- package/dist/commands/doctor.js +131 -28
- package/dist/commands/embedder-probe.js +47 -0
- package/dist/commands/hook-inject.js +178 -31
- package/dist/commands/hook-session-guard.js +26 -1
- package/dist/commands/init-remote.js +298 -0
- package/dist/commands/init.js +155 -109
- package/dist/commands/stores.js +1 -1
- package/dist/commands/tensions.js +168 -0
- package/dist/index.js +10 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npx @plur-ai/cli@0.9.4 status
|
|
|
21
21
|
## Quick Start
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
# Install Claude Code hooks (automatic memory injection)
|
|
24
|
+
# Install Claude Code hooks + local hook binary (automatic memory injection)
|
|
25
25
|
plur init
|
|
26
26
|
|
|
27
27
|
# Store a learning
|
|
@@ -59,7 +59,8 @@ plur forget ENG-2026-0329-001
|
|
|
59
59
|
| `plur sync` | Cross-device sync via git |
|
|
60
60
|
| `plur packs list` | List installed engram packs |
|
|
61
61
|
| `plur packs install <source>` | Install an engram pack |
|
|
62
|
-
| `plur init` | Install Claude Code hooks for automatic injection |
|
|
62
|
+
| `plur init` | Install Claude Code hooks + local hook binary for automatic injection |
|
|
63
|
+
| `plur doctor` | Diagnose installation health (hooks, MCP, shim, embedder) |
|
|
63
64
|
|
|
64
65
|
## Global Flags
|
|
65
66
|
|
package/dist/commands/doctor.js
CHANGED
|
@@ -10,23 +10,50 @@ import {
|
|
|
10
10
|
outputText,
|
|
11
11
|
shouldOutputJson
|
|
12
12
|
} from "./chunk-7U4W4J3G.js";
|
|
13
|
-
import {
|
|
14
|
-
createPlur
|
|
15
|
-
} from "./chunk-O6WTH7H7.js";
|
|
16
13
|
|
|
17
14
|
// src/commands/doctor.ts
|
|
18
15
|
import { spawn } from "child_process";
|
|
16
|
+
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
17
|
+
import { join, extname } from "path";
|
|
18
|
+
import { homedir, platform } from "os";
|
|
19
19
|
function hasAnyPlurHook(config) {
|
|
20
20
|
const hooks = config.hooks ?? {};
|
|
21
21
|
for (const entries of Object.values(hooks)) {
|
|
22
22
|
for (const entry of entries) {
|
|
23
23
|
for (const h of entry.hooks ?? []) {
|
|
24
|
-
if (h.command && h.command.includes("@plur-ai/cli")) return true;
|
|
24
|
+
if (h.command && (h.command.includes("@plur-ai/cli") || h.command.includes(".plur/bin/plur-hook"))) return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function hasStaleNpxHooks(config) {
|
|
31
|
+
const hooks = config.hooks ?? {};
|
|
32
|
+
for (const entries of Object.values(hooks)) {
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
for (const h of entry.hooks ?? []) {
|
|
35
|
+
if (h.command && h.command.includes("npx") && h.command.includes("@plur-ai/cli")) return true;
|
|
25
36
|
}
|
|
26
37
|
}
|
|
27
38
|
}
|
|
28
39
|
return false;
|
|
29
40
|
}
|
|
41
|
+
function validateHookShim() {
|
|
42
|
+
const name = platform() === "win32" ? "plur-hook.cmd" : "plur-hook";
|
|
43
|
+
const path = join(homedir(), ".plur", "bin", name);
|
|
44
|
+
if (!existsSync(path)) {
|
|
45
|
+
return { valid: false, shimPath: path, error: "shim not found \u2014 run `plur init` to create it" };
|
|
46
|
+
}
|
|
47
|
+
const content = readFileSync(path, "utf-8");
|
|
48
|
+
const match = content.match(/"([^"]+index\.js)"/);
|
|
49
|
+
if (!match) {
|
|
50
|
+
return { valid: false, shimPath: path, error: "shim has unexpected format" };
|
|
51
|
+
}
|
|
52
|
+
if (!existsSync(match[1])) {
|
|
53
|
+
return { valid: false, shimPath: path, error: `entrypoint missing: ${match[1]} \u2014 run \`plur init\` to fix` };
|
|
54
|
+
}
|
|
55
|
+
return { valid: true, shimPath: path };
|
|
56
|
+
}
|
|
30
57
|
function inspectConfigs() {
|
|
31
58
|
return knownConfigFiles().map((cf) => {
|
|
32
59
|
if (!cf.exists) {
|
|
@@ -121,46 +148,111 @@ async function mcpHandshake(timeoutMs = 2e4) {
|
|
|
121
148
|
proc.stdin?.write(JSON.stringify(initRequest) + "\n");
|
|
122
149
|
});
|
|
123
150
|
}
|
|
124
|
-
|
|
151
|
+
function resolveCliJsEntry() {
|
|
152
|
+
const argv1 = process.argv[1];
|
|
153
|
+
if (!argv1) return null;
|
|
154
|
+
let resolved;
|
|
125
155
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
available: status.available,
|
|
138
|
-
loaded: status.loaded,
|
|
139
|
-
lastError: status.lastError,
|
|
140
|
-
modelLoaded: status.available && status.loaded,
|
|
141
|
-
disabled: status.disabled,
|
|
142
|
-
disabledReason: status.disabledReason
|
|
143
|
-
};
|
|
144
|
-
} catch (err) {
|
|
145
|
-
return {
|
|
156
|
+
resolved = realpathSync(argv1);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const ext = extname(resolved).toLowerCase();
|
|
161
|
+
if (ext !== ".js" && ext !== ".mjs" && ext !== ".cjs") return null;
|
|
162
|
+
return resolved;
|
|
163
|
+
}
|
|
164
|
+
async function checkEmbedder(_flags, timeoutMs = 1e4) {
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const fallback = (lastError) => ({
|
|
146
167
|
available: false,
|
|
147
168
|
loaded: false,
|
|
148
|
-
lastError
|
|
169
|
+
lastError,
|
|
149
170
|
modelLoaded: false,
|
|
150
171
|
disabled: false,
|
|
151
172
|
disabledReason: null
|
|
173
|
+
});
|
|
174
|
+
const cliEntry = resolveCliJsEntry();
|
|
175
|
+
if (!cliEntry) {
|
|
176
|
+
resolve(fallback("embedder probe skipped: CLI entry is not a JS file (compiled binary?)"));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let resolved = false;
|
|
180
|
+
const finish = (result) => {
|
|
181
|
+
if (resolved) return;
|
|
182
|
+
resolved = true;
|
|
183
|
+
try {
|
|
184
|
+
proc.kill();
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
resolve(result);
|
|
152
188
|
};
|
|
153
|
-
|
|
189
|
+
let proc;
|
|
190
|
+
try {
|
|
191
|
+
proc = spawn(process.execPath, [cliEntry, "_embedder-probe"], {
|
|
192
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
193
|
+
// Mark the subprocess as the parent-spawned probe — the probe checks
|
|
194
|
+
// this and refuses to run if invoked directly by a curious user.
|
|
195
|
+
env: { ...process.env, PLUR_INTERNAL_PROBE: "1" }
|
|
196
|
+
});
|
|
197
|
+
} catch (err) {
|
|
198
|
+
finish(fallback(`spawn failed: ${err.message}`));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const timeout = setTimeout(() => {
|
|
202
|
+
finish(fallback(`probe timeout after ${timeoutMs}ms`));
|
|
203
|
+
}, timeoutMs);
|
|
204
|
+
let stdoutBuf = "";
|
|
205
|
+
let stderrBuf = "";
|
|
206
|
+
proc.stdout?.on("data", (chunk) => {
|
|
207
|
+
stdoutBuf += chunk.toString("utf8");
|
|
208
|
+
});
|
|
209
|
+
proc.stderr?.on("data", (chunk) => {
|
|
210
|
+
stderrBuf += chunk.toString("utf8");
|
|
211
|
+
});
|
|
212
|
+
proc.on("error", (err) => {
|
|
213
|
+
clearTimeout(timeout);
|
|
214
|
+
finish(fallback(`probe spawn error: ${err.message}`));
|
|
215
|
+
});
|
|
216
|
+
proc.on("exit", (code, signal) => {
|
|
217
|
+
clearTimeout(timeout);
|
|
218
|
+
const lines = stdoutBuf.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("{"));
|
|
219
|
+
const resultLine = lines[lines.length - 1];
|
|
220
|
+
if (resultLine) {
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(resultLine);
|
|
223
|
+
finish({
|
|
224
|
+
available: !!parsed.available,
|
|
225
|
+
loaded: !!parsed.loaded,
|
|
226
|
+
lastError: parsed.lastError ?? null,
|
|
227
|
+
modelLoaded: !!parsed.modelLoaded,
|
|
228
|
+
disabled: !!parsed.disabled,
|
|
229
|
+
disabledReason: parsed.disabledReason ?? null
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const detail = signal ? `signal ${signal}` : `exit ${code}`;
|
|
236
|
+
const stderrTrim = stderrBuf.trim().slice(0, 200);
|
|
237
|
+
finish(fallback(`embedder probe failed (${detail})${stderrTrim ? `: ${stderrTrim}` : ""}`));
|
|
238
|
+
});
|
|
239
|
+
});
|
|
154
240
|
}
|
|
155
241
|
function buildReport(skipHandshake, flags) {
|
|
156
242
|
const configs = inspectConfigs();
|
|
157
243
|
const hooksInstalled = configs.some((c) => c.hasPlurHooks);
|
|
158
244
|
const mcpRegistered = configs.some((c) => c.hasPlurMcp);
|
|
159
245
|
const datacoreCollision = configs.some((c) => c.hasDatacoreMcp);
|
|
246
|
+
const staleNpxHooks = configs.some((c) => {
|
|
247
|
+
if (!c.exists) return false;
|
|
248
|
+
const config = readConfig(c.path);
|
|
249
|
+
return hasStaleNpxHooks(config);
|
|
250
|
+
});
|
|
251
|
+
const hookShim = validateHookShim();
|
|
160
252
|
const handshakePromise = skipHandshake ? Promise.resolve({ ok: false, error: "skipped (--no-handshake)" }) : mcpHandshake();
|
|
161
253
|
return Promise.all([handshakePromise, checkEmbedder(flags)]).then(([handshake, embedder]) => {
|
|
162
254
|
const overall = hooksInstalled && mcpRegistered && (skipHandshake || handshake.ok) ? "ok" : "fail";
|
|
163
|
-
return { configs, hooksInstalled, mcpRegistered, datacoreCollision, handshake, embedder, overall };
|
|
255
|
+
return { configs, hooksInstalled, mcpRegistered, datacoreCollision, staleNpxHooks, hookShim, handshake, embedder, overall };
|
|
164
256
|
});
|
|
165
257
|
}
|
|
166
258
|
function printText(report) {
|
|
@@ -192,6 +284,17 @@ function printText(report) {
|
|
|
192
284
|
outputText(" If your agent confuses them, this is the cause.");
|
|
193
285
|
}
|
|
194
286
|
outputText("");
|
|
287
|
+
if (report.hookShim.valid) {
|
|
288
|
+
outputText(`\u2713 Hook shim: ${report.hookShim.shimPath}`);
|
|
289
|
+
} else {
|
|
290
|
+
outputText(`\u2717 Hook shim: ${report.hookShim.error}`);
|
|
291
|
+
}
|
|
292
|
+
if (report.staleNpxHooks) {
|
|
293
|
+
outputText("");
|
|
294
|
+
outputText("\u26A0 Hooks still use npx \u2014 slow (200-2000ms per hook) and vulnerable to cache corruption.");
|
|
295
|
+
outputText(" Fix: run `plur init` to migrate to the local hook binary (<5ms per hook).");
|
|
296
|
+
}
|
|
297
|
+
outputText("");
|
|
195
298
|
if (report.handshake.ok) {
|
|
196
299
|
outputText(`\u2713 MCP handshake: ${report.handshake.serverName} v${report.handshake.serverVersion}`);
|
|
197
300
|
} else {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPlur
|
|
3
|
+
} from "./chunk-O6WTH7H7.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/embedder-probe.ts
|
|
6
|
+
async function run(_args, flags) {
|
|
7
|
+
if (process.env.PLUR_INTERNAL_PROBE !== "1") {
|
|
8
|
+
process.stderr.write(
|
|
9
|
+
"_embedder-probe is an internal subcommand spawned by `plur doctor`. Run `plur doctor` instead.\n"
|
|
10
|
+
);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const plur = createPlur(flags);
|
|
15
|
+
const preStatus = plur.embedderStatus();
|
|
16
|
+
if (!preStatus.disabled) {
|
|
17
|
+
plur.resetEmbedder();
|
|
18
|
+
try {
|
|
19
|
+
await plur.recallSemantic("plur doctor probe", { limit: 1 });
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const status = plur.embedderStatus();
|
|
24
|
+
process.stdout.write(JSON.stringify({
|
|
25
|
+
available: status.available,
|
|
26
|
+
loaded: status.loaded,
|
|
27
|
+
lastError: status.lastError,
|
|
28
|
+
modelLoaded: status.available && status.loaded,
|
|
29
|
+
disabled: status.disabled,
|
|
30
|
+
disabledReason: status.disabledReason
|
|
31
|
+
}) + "\n");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
process.stdout.write(JSON.stringify({
|
|
35
|
+
available: false,
|
|
36
|
+
loaded: false,
|
|
37
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
38
|
+
modelLoaded: false,
|
|
39
|
+
disabled: false,
|
|
40
|
+
disabledReason: null
|
|
41
|
+
}) + "\n");
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
run
|
|
47
|
+
};
|
|
@@ -3,30 +3,165 @@ import {
|
|
|
3
3
|
} from "./chunk-O6WTH7H7.js";
|
|
4
4
|
|
|
5
5
|
// src/commands/hook-inject.ts
|
|
6
|
-
import { existsSync, writeFileSync, readFileSync, mkdirSync, readSync, statSync } from "fs";
|
|
7
|
-
import { join } from "path";
|
|
8
|
-
import { tmpdir } from "os";
|
|
6
|
+
import { existsSync, writeFileSync, readFileSync, appendFileSync, mkdirSync, readSync, statSync } from "fs";
|
|
7
|
+
import { dirname, join, resolve } from "path";
|
|
8
|
+
import { tmpdir, homedir } from "os";
|
|
9
9
|
import { randomUUID } from "crypto";
|
|
10
|
+
var MAX_REMOTE_TASK_CHARS = 1e3;
|
|
11
|
+
var MAX_REMOTE_RESPONSE_BYTES = 128 * 1024;
|
|
12
|
+
var REMOTE_TIMEOUT_MS = 1500;
|
|
13
|
+
var REMOTE_INJECT_LOG_DIR = join(homedir(), ".plur", "logs");
|
|
14
|
+
function remoteInjectLogPath() {
|
|
15
|
+
return join(REMOTE_INJECT_LOG_DIR, `remote-inject-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.jsonl`);
|
|
16
|
+
}
|
|
17
|
+
function logRemoteAttempt(entry) {
|
|
18
|
+
try {
|
|
19
|
+
mkdirSync(REMOTE_INJECT_LOG_DIR, { recursive: true });
|
|
20
|
+
appendFileSync(remoteInjectLogPath(), JSON.stringify(entry) + "\n");
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
}
|
|
10
24
|
var REMINDER_INTERVAL_MS = 10 * 60 * 1e3;
|
|
25
|
+
function findProjectConfigPath(startDir = process.cwd()) {
|
|
26
|
+
const home = resolve(homedir());
|
|
27
|
+
let dir = resolve(startDir);
|
|
28
|
+
const MAX_DEPTH = 12;
|
|
29
|
+
for (let depth = 0; depth < MAX_DEPTH; depth++) {
|
|
30
|
+
if (dir !== home) {
|
|
31
|
+
const candidate = join(dir, ".plur.yaml");
|
|
32
|
+
if (existsSync(candidate)) return candidate;
|
|
33
|
+
}
|
|
34
|
+
if (existsSync(join(dir, ".git"))) return null;
|
|
35
|
+
if (dir === home || dir === "/" || dir === ".") return null;
|
|
36
|
+
const parent = dirname(dir);
|
|
37
|
+
if (parent === dir) return null;
|
|
38
|
+
dir = parent;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function unquoteYamlValue(v) {
|
|
43
|
+
return v.replace(/^(['"])(.*)\1$/, "$2");
|
|
44
|
+
}
|
|
11
45
|
function readProjectConfig() {
|
|
12
|
-
const configPath =
|
|
13
|
-
if (!
|
|
46
|
+
const configPath = findProjectConfigPath();
|
|
47
|
+
if (!configPath) return {};
|
|
14
48
|
try {
|
|
15
|
-
const content = readFileSync(configPath, "utf8");
|
|
49
|
+
const content = readFileSync(configPath, "utf8").replace(/^/, "");
|
|
16
50
|
const config = {};
|
|
17
|
-
|
|
51
|
+
let inListKey = null;
|
|
52
|
+
let listAcc = [];
|
|
53
|
+
const finishList = () => {
|
|
54
|
+
if (inListKey === "remote_scopes") {
|
|
55
|
+
config.remote_scopes = listAcc;
|
|
56
|
+
}
|
|
57
|
+
inListKey = null;
|
|
58
|
+
listAcc = [];
|
|
59
|
+
};
|
|
60
|
+
for (const rawLine of content.split("\n")) {
|
|
61
|
+
const line = rawLine.replace(/\r$/, "");
|
|
18
62
|
const trimmed = line.trim();
|
|
19
63
|
if (trimmed.startsWith("#") || !trimmed) continue;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
64
|
+
if (inListKey === "remote_scopes" && trimmed.startsWith("-")) {
|
|
65
|
+
listAcc.push(unquoteYamlValue(trimmed.slice(1).trim()));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (inListKey) finishList();
|
|
69
|
+
const colonIdx = trimmed.indexOf(":");
|
|
70
|
+
if (colonIdx < 0) continue;
|
|
71
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
72
|
+
const value = unquoteYamlValue(trimmed.slice(colonIdx + 1).trim());
|
|
73
|
+
switch (key) {
|
|
74
|
+
case "domain":
|
|
75
|
+
config.domain = value;
|
|
76
|
+
break;
|
|
77
|
+
case "scope":
|
|
78
|
+
config.scope = value;
|
|
79
|
+
break;
|
|
80
|
+
case "remote_url":
|
|
81
|
+
config.remote_url = value;
|
|
82
|
+
break;
|
|
83
|
+
case "remote_token":
|
|
84
|
+
config.remote_token = value;
|
|
85
|
+
break;
|
|
86
|
+
case "remote_scopes":
|
|
87
|
+
if (value === "" || value === "|" || value === ">") {
|
|
88
|
+
inListKey = "remote_scopes";
|
|
89
|
+
listAcc = [];
|
|
90
|
+
} else {
|
|
91
|
+
config.remote_scopes = value.split(",").map((s) => unquoteYamlValue(s.trim())).filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
24
95
|
}
|
|
96
|
+
finishList();
|
|
25
97
|
return config;
|
|
26
98
|
} catch {
|
|
27
99
|
return {};
|
|
28
100
|
}
|
|
29
101
|
}
|
|
102
|
+
async function tryRemoteInject(config, task) {
|
|
103
|
+
if (!config.remote_url || !config.remote_token) return null;
|
|
104
|
+
const startTs = Date.now();
|
|
105
|
+
let base;
|
|
106
|
+
try {
|
|
107
|
+
base = new URL(config.remote_url).origin;
|
|
108
|
+
} catch {
|
|
109
|
+
logRemoteAttempt({ ts: (/* @__PURE__ */ new Date()).toISOString(), url: config.remote_url ?? "?", outcome: "bad_response", ms: 0, detail: "invalid URL" });
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const url = `${base}/api/v1/inject`;
|
|
113
|
+
const truncatedTask = task.length > MAX_REMOTE_TASK_CHARS ? task.slice(0, MAX_REMOTE_TASK_CHARS) : task;
|
|
114
|
+
const body = { task: truncatedTask };
|
|
115
|
+
if (config.remote_scopes && config.remote_scopes.length > 0) {
|
|
116
|
+
body.scopes = config.remote_scopes;
|
|
117
|
+
}
|
|
118
|
+
const ctrl = new AbortController();
|
|
119
|
+
const t = setTimeout(() => ctrl.abort(), REMOTE_TIMEOUT_MS);
|
|
120
|
+
try {
|
|
121
|
+
const r = await fetch(url, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
signal: ctrl.signal,
|
|
124
|
+
headers: {
|
|
125
|
+
"authorization": `Bearer ${config.remote_token}`,
|
|
126
|
+
"content-type": "application/json"
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify(body)
|
|
129
|
+
});
|
|
130
|
+
if (!r.ok) {
|
|
131
|
+
logRemoteAttempt({ ts: (/* @__PURE__ */ new Date()).toISOString(), url, outcome: "http_error", ms: Date.now() - startTs, http: r.status });
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const contentLength = r.headers.get("content-length");
|
|
135
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_REMOTE_RESPONSE_BYTES) {
|
|
136
|
+
logRemoteAttempt({ ts: (/* @__PURE__ */ new Date()).toISOString(), url, outcome: "oversize", ms: Date.now() - startTs, http: r.status, detail: `content-length=${contentLength}` });
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const data = await r.json();
|
|
140
|
+
if (typeof data.text !== "string" || !data.text.trim()) {
|
|
141
|
+
logRemoteAttempt({ ts: (/* @__PURE__ */ new Date()).toISOString(), url, outcome: "bad_response", ms: Date.now() - startTs, http: r.status, detail: "empty or non-string text field" });
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const count = typeof data.count === "number" ? data.count : 0;
|
|
145
|
+
logRemoteAttempt({ ts: (/* @__PURE__ */ new Date()).toISOString(), url, outcome: "ok", ms: Date.now() - startTs, http: r.status, engrams: count });
|
|
146
|
+
return {
|
|
147
|
+
text: data.text,
|
|
148
|
+
count,
|
|
149
|
+
injectedIds: Array.isArray(data.injected_ids) ? data.injected_ids : []
|
|
150
|
+
};
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
153
|
+
logRemoteAttempt({
|
|
154
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
155
|
+
url,
|
|
156
|
+
outcome: isAbort ? "timeout" : "network_error",
|
|
157
|
+
ms: Date.now() - startTs,
|
|
158
|
+
detail: err instanceof Error ? err.message.slice(0, 120) : void 0
|
|
159
|
+
});
|
|
160
|
+
return null;
|
|
161
|
+
} finally {
|
|
162
|
+
clearTimeout(t);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
30
165
|
function sessionDir() {
|
|
31
166
|
const dir = join(tmpdir(), "plur-sessions");
|
|
32
167
|
mkdirSync(dir, { recursive: true });
|
|
@@ -177,25 +312,36 @@ ${parts2.join("\n")}` };
|
|
|
177
312
|
const injectOpts = projectConfig.scope ? { scope: projectConfig.scope } : void 0;
|
|
178
313
|
let context = null;
|
|
179
314
|
let count = 0;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
context = parts2.join("\n");
|
|
188
|
-
count = result.count;
|
|
315
|
+
let remoteUsed = false;
|
|
316
|
+
if (projectConfig.remote_url && projectConfig.remote_token) {
|
|
317
|
+
const remote = await tryRemoteInject(projectConfig, task);
|
|
318
|
+
if (remote && remote.count > 0) {
|
|
319
|
+
context = remote.text;
|
|
320
|
+
count = remote.count;
|
|
321
|
+
remoteUsed = true;
|
|
189
322
|
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
if (result.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
323
|
+
}
|
|
324
|
+
if (!remoteUsed) {
|
|
325
|
+
try {
|
|
326
|
+
const result = await plur.injectHybrid(task, injectOpts);
|
|
327
|
+
if (result.count > 0) {
|
|
328
|
+
const parts2 = [];
|
|
329
|
+
if (result.directives) parts2.push(result.directives);
|
|
330
|
+
if (result.constraints) parts2.push(result.constraints);
|
|
331
|
+
if (result.consider) parts2.push(result.consider);
|
|
332
|
+
context = parts2.join("\n");
|
|
333
|
+
count = result.count;
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
const result = plur.inject(task, injectOpts);
|
|
337
|
+
if (result.count > 0) {
|
|
338
|
+
const parts2 = [];
|
|
339
|
+
if (result.directives) parts2.push(result.directives);
|
|
340
|
+
if (result.constraints) parts2.push(result.constraints);
|
|
341
|
+
if (result.consider) parts2.push(result.consider);
|
|
342
|
+
context = parts2.join("\n");
|
|
343
|
+
count = result.count;
|
|
344
|
+
}
|
|
199
345
|
}
|
|
200
346
|
}
|
|
201
347
|
const parts = [];
|
|
@@ -205,10 +351,11 @@ ${parts2.join("\n")}` };
|
|
|
205
351
|
sessionId = markerData.sessionId;
|
|
206
352
|
} catch {
|
|
207
353
|
}
|
|
354
|
+
const sourceLabel = remoteUsed ? " (Enterprise)" : "";
|
|
208
355
|
if (isRehydrate) {
|
|
209
|
-
parts.push(`[PLUR Memory \u2014 rehydrated after compaction, ${count} engrams]`);
|
|
356
|
+
parts.push(`[PLUR Memory${sourceLabel} \u2014 rehydrated after compaction, ${count} engrams]`);
|
|
210
357
|
} else {
|
|
211
|
-
parts.push(`[PLUR Memory \u2014 session started, ${count} engrams injected]`);
|
|
358
|
+
parts.push(`[PLUR Memory${sourceLabel} \u2014 session started, ${count} engrams injected]`);
|
|
212
359
|
if (sessionId) parts.push(`Session ID: ${sessionId}`);
|
|
213
360
|
if (projectConfig.domain) parts.push(`Project domain: ${projectConfig.domain}`);
|
|
214
361
|
if (projectConfig.scope) parts.push(`Project scope: ${projectConfig.scope} \u2014 use this scope for plur_learn calls`);
|
|
@@ -3,13 +3,14 @@ import {
|
|
|
3
3
|
} from "./chunk-6RANUJMM.js";
|
|
4
4
|
|
|
5
5
|
// src/commands/hook-session-guard.ts
|
|
6
|
-
import { readSync, existsSync } from "fs";
|
|
6
|
+
import { readSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
import { tmpdir } from "os";
|
|
9
9
|
var EXEMPT_TOOLS = /* @__PURE__ */ new Set([
|
|
10
10
|
"mcp__plur__plur_session_start",
|
|
11
11
|
"ToolSearch"
|
|
12
12
|
]);
|
|
13
|
+
var MAX_BLOCKS_BEFORE_FALLBACK = 5;
|
|
13
14
|
function readStdinRaw() {
|
|
14
15
|
try {
|
|
15
16
|
const chunks = [];
|
|
@@ -31,6 +32,22 @@ function readStdinRaw() {
|
|
|
31
32
|
function sentinelPath(sessionId) {
|
|
32
33
|
return join(tmpdir(), `plur-session-${sessionId}`);
|
|
33
34
|
}
|
|
35
|
+
function blockCountPath(sessionId) {
|
|
36
|
+
const dir = join(tmpdir(), "plur-sessions");
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
return join(dir, `${sessionId}.guard-count`);
|
|
39
|
+
}
|
|
40
|
+
function incrementBlockCount(sessionId) {
|
|
41
|
+
const path = blockCountPath(sessionId);
|
|
42
|
+
let count = 0;
|
|
43
|
+
try {
|
|
44
|
+
count = parseInt(readFileSync(path, "utf8"), 10) || 0;
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
count++;
|
|
48
|
+
writeFileSync(path, String(count));
|
|
49
|
+
return count;
|
|
50
|
+
}
|
|
34
51
|
async function run(_args, _flags) {
|
|
35
52
|
if (!isPlurConfigured()) return;
|
|
36
53
|
const raw = readStdinRaw();
|
|
@@ -45,6 +62,14 @@ async function run(_args, _flags) {
|
|
|
45
62
|
if (EXEMPT_TOOLS.has(toolName)) return;
|
|
46
63
|
if (!sessionId) return;
|
|
47
64
|
if (existsSync(sentinelPath(sessionId))) return;
|
|
65
|
+
const blockCount = incrementBlockCount(sessionId);
|
|
66
|
+
if (blockCount > MAX_BLOCKS_BEFORE_FALLBACK) {
|
|
67
|
+
process.stderr.write(
|
|
68
|
+
`[plur] WARNING: session guard gave up after ${MAX_BLOCKS_BEFORE_FALLBACK} blocked calls. The plur MCP server may not be running. Run \`plur doctor\` to diagnose.
|
|
69
|
+
`
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
48
73
|
const output = {
|
|
49
74
|
hookSpecificOutput: {
|
|
50
75
|
hookEventName: "PreToolUse",
|