@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.
@@ -0,0 +1,298 @@
1
+ import {
2
+ outputText
3
+ } from "./chunk-7U4W4J3G.js";
4
+
5
+ // src/commands/init-remote.ts
6
+ import { existsSync, readFileSync, writeFileSync, appendFileSync } from "fs";
7
+ import { dirname, join, resolve } from "path";
8
+ import { homedir } from "os";
9
+ var HELP = `plur init-remote \u2014 opt this project into recall from PLUR Enterprise
10
+
11
+ USAGE
12
+ plur init-remote --url <enterprise-url> --token <api-key> [--scopes <list>]
13
+ plur init-remote --verify Check connectivity against existing .plur.yaml
14
+
15
+ OPTIONS
16
+ --url URL Enterprise base URL, e.g. https://plur.datafund.io
17
+ --token KEY API key for authentication
18
+ --scopes SCOPES Optional comma-separated scope whitelist
19
+ e.g. "org:plur,group:plur/engineering"
20
+ --no-gitignore Skip adding .plur.yaml to .gitignore (NOT RECOMMENDED \u2014
21
+ the token is sensitive)
22
+ --verify Read existing .plur.yaml and test the /api/v1/me
23
+ endpoint against the configured remote
24
+
25
+ WHAT THIS DOES
26
+ Writes .plur.yaml in the current directory with remote_url, remote_token,
27
+ and optional remote_scopes fields. The UserPromptSubmit hook will then
28
+ call \${remote_url}/api/v1/inject for each prompt (before falling back to
29
+ local PLUR). The hook walks upward from the current working directory to
30
+ find .plur.yaml, so you can work from any subdirectory.
31
+
32
+ WITHOUT this command, projects stay 100% local-only and Enterprise
33
+ never sees their prompts.
34
+ `;
35
+ function parseArgs(args) {
36
+ const out = {};
37
+ const consumeValue = (i, flag) => {
38
+ const next = args[i + 1];
39
+ if (next === void 0 || next.startsWith("--")) {
40
+ return { error: `${flag} requires a value (got ${next === void 0 ? "nothing" : `another flag: ${next}`})` };
41
+ }
42
+ return { value: next };
43
+ };
44
+ for (let i = 0; i < args.length; i++) {
45
+ const a = args[i];
46
+ if (a === "--help" || a === "-h") {
47
+ out.help = true;
48
+ continue;
49
+ }
50
+ if (a === "--verify") {
51
+ out.verify = true;
52
+ continue;
53
+ }
54
+ if (a === "--no-gitignore") {
55
+ out.noGitignore = true;
56
+ continue;
57
+ }
58
+ if (a === "--url" || a === "--token" || a === "--scopes") {
59
+ const r = consumeValue(i, a);
60
+ if ("error" in r) return r;
61
+ i++;
62
+ if (a === "--url") out.url = r.value;
63
+ if (a === "--token") out.token = r.value;
64
+ if (a === "--scopes") out.scopes = r.value.split(",").map((s) => s.trim()).filter(Boolean);
65
+ continue;
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ function stripRemoteKeys(content) {
71
+ const lines = content.split("\n");
72
+ const out = [];
73
+ let skippingList = false;
74
+ const REMOTE_KEY = /^remote_(url|token|scopes)\s*:(.*)$/;
75
+ for (const line of lines) {
76
+ const trimmed = line.trim();
77
+ if (skippingList) {
78
+ if (trimmed === "" || trimmed.startsWith("-")) continue;
79
+ skippingList = false;
80
+ }
81
+ const m = trimmed.match(REMOTE_KEY);
82
+ if (m) {
83
+ const key = m[1];
84
+ const rest = m[2].trim();
85
+ if (key === "scopes" && (rest === "" || rest === "|" || rest === ">")) {
86
+ skippingList = true;
87
+ }
88
+ continue;
89
+ }
90
+ out.push(line);
91
+ }
92
+ while (out.length > 0 && out[out.length - 1].trim() === "") out.pop();
93
+ return out.join("\n");
94
+ }
95
+ function buildConfigBody(existing, url, token, scopes) {
96
+ const stripped = stripRemoteKeys(existing);
97
+ const sep = stripped.length > 0 && !stripped.endsWith("\n") ? "\n\n" : stripped.length > 0 ? "\n" : "";
98
+ const block = [];
99
+ block.push("# --- PLUR Enterprise remote (opt-in for this project) ---");
100
+ block.push("# remote_token is sensitive \u2014 keep .plur.yaml in .gitignore.");
101
+ block.push(`remote_url: ${url}`);
102
+ block.push(`remote_token: ${token}`);
103
+ if (scopes && scopes.length > 0) {
104
+ block.push("remote_scopes:");
105
+ for (const s of scopes) block.push(` - ${s}`);
106
+ }
107
+ return stripped + sep + block.join("\n") + "\n";
108
+ }
109
+ function ensureGitignore() {
110
+ const home = resolve(homedir());
111
+ let dir = resolve(process.cwd());
112
+ let gitignorePath = null;
113
+ const MAX_DEPTH = 12;
114
+ for (let depth = 0; depth < MAX_DEPTH; depth++) {
115
+ const candidate = join(dir, ".gitignore");
116
+ if (existsSync(candidate)) {
117
+ gitignorePath = candidate;
118
+ break;
119
+ }
120
+ if (existsSync(join(dir, ".git"))) break;
121
+ if (dir === home || dir === "/" || dir === ".") break;
122
+ const parent = dirname(dir);
123
+ if (parent === dir) break;
124
+ dir = parent;
125
+ }
126
+ const PATTERN = ".plur.yaml";
127
+ if (!gitignorePath) {
128
+ const newPath = join(process.cwd(), ".gitignore");
129
+ writeFileSync(newPath, `# Added by 'plur init-remote' \u2014 .plur.yaml may hold an API token
130
+ ${PATTERN}
131
+ `);
132
+ return { path: newPath, action: "created" };
133
+ }
134
+ const content = readFileSync(gitignorePath, "utf8");
135
+ const already = content.split("\n").some((l) => l.trim() === PATTERN);
136
+ if (already) return { path: gitignorePath, action: "already" };
137
+ const sep = content.endsWith("\n") ? "" : "\n";
138
+ appendFileSync(gitignorePath, `${sep}# Added by 'plur init-remote' \u2014 .plur.yaml may hold an API token
139
+ ${PATTERN}
140
+ `);
141
+ return { path: gitignorePath, action: "added" };
142
+ }
143
+ function findExistingConfigPath() {
144
+ const home = resolve(homedir());
145
+ let dir = resolve(process.cwd());
146
+ const MAX_DEPTH = 12;
147
+ for (let depth = 0; depth < MAX_DEPTH; depth++) {
148
+ if (dir !== home) {
149
+ const candidate = join(dir, ".plur.yaml");
150
+ if (existsSync(candidate)) return candidate;
151
+ }
152
+ if (existsSync(join(dir, ".git"))) return null;
153
+ if (dir === home || dir === "/" || dir === ".") return null;
154
+ const parent = dirname(dir);
155
+ if (parent === dir) return null;
156
+ dir = parent;
157
+ }
158
+ return null;
159
+ }
160
+ function readRemoteFromConfig(path) {
161
+ if (!existsSync(path)) return {};
162
+ const content = readFileSync(path, "utf8");
163
+ const out = {};
164
+ for (const line of content.split("\n")) {
165
+ const trimmed = line.trim();
166
+ if (trimmed.startsWith("#") || !trimmed) continue;
167
+ const m = trimmed.match(/^(remote_url|remote_token)\s*:\s*(.+)$/);
168
+ if (m) {
169
+ if (m[1] === "remote_url") out.url = m[2].trim();
170
+ if (m[1] === "remote_token") out.token = m[2].trim();
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+ async function verifyConnectivity(url, token) {
176
+ let base;
177
+ try {
178
+ base = new URL(url).origin;
179
+ } catch {
180
+ throw new Error(`Invalid URL: ${url}`);
181
+ }
182
+ const probeUrl = `${base}/api/v1/me`;
183
+ const ctrl = new AbortController();
184
+ const timer = setTimeout(() => ctrl.abort(), 5e3);
185
+ try {
186
+ const r = await fetch(probeUrl, {
187
+ signal: ctrl.signal,
188
+ headers: { "authorization": `Bearer ${token}`, "accept": "application/json" }
189
+ });
190
+ if (r.status === 401) throw new Error(`401 Unauthorized \u2014 check your API token`);
191
+ if (r.status === 403) throw new Error(`403 Forbidden \u2014 token lacks /me access`);
192
+ if (!r.ok) throw new Error(`HTTP ${r.status} from ${probeUrl}`);
193
+ const data = await r.json();
194
+ if (!data.username) throw new Error(`Unexpected response shape from ${probeUrl}`);
195
+ return {
196
+ username: data.username,
197
+ org_id: data.org_id ?? "(unknown)",
198
+ scopes: Array.isArray(data.scopes) ? data.scopes : []
199
+ };
200
+ } finally {
201
+ clearTimeout(timer);
202
+ }
203
+ }
204
+ async function run(args, flags) {
205
+ const parsed = parseArgs(args);
206
+ if ("error" in parsed) {
207
+ outputText(`Error: ${parsed.error}
208
+
209
+ ${HELP}`, flags);
210
+ process.exit(1);
211
+ }
212
+ const opts = parsed;
213
+ if (opts.help) {
214
+ outputText(HELP, flags);
215
+ return;
216
+ }
217
+ const configPath = join(process.cwd(), ".plur.yaml");
218
+ if (opts.verify) {
219
+ const verifyPath = findExistingConfigPath() ?? configPath;
220
+ const cfg = readRemoteFromConfig(verifyPath);
221
+ if (!cfg.url || !cfg.token) {
222
+ outputText(`No remote config found (walked upward from ${process.cwd()}). Run \`plur init-remote --url <url> --token <key>\` first.`, flags);
223
+ process.exit(1);
224
+ }
225
+ outputText(`Using config at ${verifyPath}`, flags);
226
+ try {
227
+ const me = await verifyConnectivity(cfg.url, cfg.token);
228
+ outputText(`\u2713 Connected to ${cfg.url} as ${me.username} (org: ${me.org_id})`, flags);
229
+ outputText(` readable scopes: ${me.scopes.length === 0 ? "(none)" : me.scopes.join(", ")}`, flags);
230
+ } catch (err) {
231
+ outputText(`\u2717 Connection failed: ${err.message}`, flags);
232
+ process.exit(2);
233
+ }
234
+ return;
235
+ }
236
+ if (!opts.url || !opts.token) {
237
+ outputText(`Missing required flags.
238
+ ${HELP}`, flags);
239
+ process.exit(1);
240
+ }
241
+ if (/[\n\r\t]/.test(opts.token)) {
242
+ outputText(`Error: token contains newline/tab characters. Refusing to write a corrupt config.`, flags);
243
+ process.exit(1);
244
+ }
245
+ try {
246
+ const u = new URL(opts.url);
247
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
248
+ outputText(`Error: remote_url must be http:// or https:// (got ${u.protocol})`, flags);
249
+ process.exit(1);
250
+ }
251
+ } catch {
252
+ outputText(`Error: remote_url is not a valid URL: ${opts.url}`, flags);
253
+ process.exit(1);
254
+ }
255
+ outputText(`Testing connectivity to ${opts.url}...`, flags);
256
+ try {
257
+ const me = await verifyConnectivity(opts.url, opts.token);
258
+ outputText(`\u2713 Authenticated as ${me.username} (org: ${me.org_id})`, flags);
259
+ outputText(` readable scopes: ${me.scopes.length === 0 ? "(none)" : me.scopes.join(", ")}`, flags);
260
+ } catch (err) {
261
+ outputText(`\u2717 Connection failed: ${err.message}`, flags);
262
+ outputText(` Refusing to write a broken config. Fix the URL/token and re-run.`, flags);
263
+ process.exit(2);
264
+ }
265
+ const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
266
+ const next = buildConfigBody(existing, opts.url, opts.token, opts.scopes);
267
+ writeFileSync(configPath, next);
268
+ outputText(`\u2713 Wrote ${configPath}`, flags);
269
+ if (opts.scopes && opts.scopes.length > 0) {
270
+ outputText(` scope whitelist: ${opts.scopes.join(", ")}`, flags);
271
+ } else {
272
+ outputText(` scope whitelist: (none \u2014 hook will query all readable scopes)`, flags);
273
+ }
274
+ if (!opts.noGitignore) {
275
+ const gi = ensureGitignore();
276
+ if (gi.action === "added") outputText(`\u2713 Added .plur.yaml to ${gi.path}`, flags);
277
+ else if (gi.action === "created") outputText(`\u2713 Created ${gi.path} with .plur.yaml entry`, flags);
278
+ else outputText(`\u2713 ${gi.path} already excludes .plur.yaml`, flags);
279
+ } else {
280
+ outputText(`\u26A0 Skipped .gitignore (--no-gitignore). The token in .plur.yaml is sensitive.`, flags);
281
+ }
282
+ outputText(`
283
+ Done. The UserPromptSubmit hook will now query ${opts.url} on every prompt`, flags);
284
+ outputText(`from this directory tree (bounded by the nearest .git). Personal/non-project`, flags);
285
+ outputText(`sessions (without a .plur.yaml in the path) stay local-only.`, flags);
286
+ outputText(``, flags);
287
+ outputText(`\u26A0 Token sensitivity:`, flags);
288
+ outputText(` .plur.yaml now contains an API token in plaintext.`, flags);
289
+ outputText(` - .gitignore protects against git commits but NOT against cloud sync`, flags);
290
+ outputText(` (iCloud Drive, Dropbox, Google Drive). If this project lives in a`, flags);
291
+ outputText(` synced folder, the token will leave your machine.`, flags);
292
+ outputText(` - Also not protected: \`cp -r\`, \`zip\`, \`rsync\`, archived backups.`, flags);
293
+ outputText(` - Consider moving the token to an env var if your project ships with`, flags);
294
+ outputText(` others (future: env-var substitution in .plur.yaml).`, flags);
295
+ }
296
+ export {
297
+ run
298
+ };
@@ -11,115 +11,153 @@ import {
11
11
  } from "./chunk-7U4W4J3G.js";
12
12
 
13
13
  // src/commands/init.ts
14
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
15
- import { join } from "path";
16
- import { homedir } from "os";
17
- var CLI = "npx @plur-ai/cli";
18
- var PLUR_HOOKS_ENFORCEMENT = {
19
- SessionStart: [
20
- {
21
- hooks: [
22
- { type: "command", command: `${CLI} hook-session-remind`, timeout: 3 }
23
- ]
24
- }
25
- ],
26
- PreToolUse: [
27
- // Session guard blocks all tools until plur_session_start is called.
28
- // Must be first so it runs before any other PreToolUse hook.
29
- {
30
- matcher: "*",
31
- hooks: [
32
- { type: "command", command: `${CLI} hook-session-guard`, timeout: 3 }
33
- ]
34
- }
35
- ],
36
- PostToolUse: [
37
- // Session sentinel — creates marker file after plur_session_start succeeds
38
- {
39
- matcher: "mcp__plur__plur_session_start",
40
- hooks: [
41
- { type: "command", command: `${CLI} hook-session-mark`, timeout: 3 }
42
- ]
43
- }
44
- ]
45
- };
46
- var PLUR_HOOKS_INJECTION = {
47
- // First message: inject engrams based on the prompt.
48
- // Subsequent messages: periodic reminder to call plur_learn (~1ms skip).
49
- UserPromptSubmit: [
50
- {
51
- hooks: [
52
- { type: "command", command: `${CLI} hook-inject`, timeout: 15 }
53
- ]
54
- }
55
- ],
56
- // Re-inject after context compaction so engrams survive long conversations.
57
- PostCompact: [
58
- {
59
- matcher: "auto|manual",
60
- hooks: [
61
- { type: "command", command: `${CLI} hook-inject --rehydrate`, timeout: 15 }
62
- ]
63
- }
64
- ],
65
- PreToolUse: [
66
- // Full injection when entering plan mode — planning needs broad context
67
- {
68
- matcher: "EnterPlanMode",
69
- hooks: [
70
- { type: "command", command: `${CLI} hook-inject --event plan_mode`, timeout: 10 }
71
- ]
72
- },
73
- // Domain-specific engrams when a skill is invoked
74
- {
75
- matcher: "Skill",
76
- hooks: [
77
- { type: "command", command: `${CLI} hook-inject --event skill`, timeout: 10 }
78
- ]
79
- },
80
- // Agent-scoped engrams when spawning an agent
81
- {
82
- matcher: "Agent",
83
- hooks: [
84
- { type: "command", command: `${CLI} hook-inject --event agent`, timeout: 10 }
85
- ]
86
- },
87
- // Observation capture — log tool calls for offline pattern extraction
88
- {
89
- matcher: "Bash|Edit|Write|Agent",
90
- hooks: [
91
- { type: "command", command: `${CLI} hook-observe`, timeout: 3 }
92
- ]
93
- }
94
- ],
95
- PostToolUse: [
96
- {
97
- matcher: "Bash|Edit|Write|Agent",
98
- hooks: [
99
- { type: "command", command: `${CLI} hook-observe --post`, timeout: 3 }
100
- ]
101
- }
102
- ],
103
- // Inject agent-scoped engrams into subagent context
104
- SubagentStart: [
105
- {
106
- matcher: ".*",
107
- hooks: [
108
- { type: "command", command: `${CLI} hook-inject --event subagent`, timeout: 10 }
109
- ]
110
- }
111
- ],
112
- // Learning reflection — nudge the LLM to call plur_learn after responses
113
- // where it discovered or learned something. Fires every 3rd Stop to avoid fatigue.
114
- Stop: [
115
- {
116
- matcher: "*",
117
- hooks: [
118
- { type: "command", command: `${CLI} hook-learn-check`, timeout: 2 }
119
- ]
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
15
+ import { join, dirname } from "path";
16
+ import { fileURLToPath } from "url";
17
+ import { homedir, platform } from "os";
18
+ function resolveCliEntrypoint() {
19
+ const thisFile = fileURLToPath(import.meta.url);
20
+ return join(dirname(thisFile), "..", "index.js");
21
+ }
22
+ function shimPath() {
23
+ const name = platform() === "win32" ? "plur-hook.cmd" : "plur-hook";
24
+ return join(homedir(), ".plur", "bin", name);
25
+ }
26
+ function installHookBinary() {
27
+ const binDir = join(homedir(), ".plur", "bin");
28
+ mkdirSync(binDir, { recursive: true });
29
+ const entrypoint = resolveCliEntrypoint();
30
+ const nodeBin = process.execPath;
31
+ if (!existsSync(entrypoint)) {
32
+ return { shimPath: "", status: `error: CLI entrypoint not found at ${entrypoint}` };
33
+ }
34
+ const target = shimPath();
35
+ if (platform() === "win32") {
36
+ writeFileSync(target, `@echo off\r
37
+ "${nodeBin}" "${entrypoint}" %*\r
38
+ `);
39
+ } else {
40
+ writeFileSync(target, `#!/bin/sh
41
+ exec "${nodeBin}" "${entrypoint}" "$@"
42
+ `, { mode: 493 });
43
+ try {
44
+ chmodSync(target, 493);
45
+ } catch {
120
46
  }
121
- ]
122
- };
47
+ }
48
+ const meta = { entrypoint, node: nodeBin, installed: (/* @__PURE__ */ new Date()).toISOString() };
49
+ writeFileSync(join(binDir, "plur-hook.meta.json"), JSON.stringify(meta, null, 2) + "\n");
50
+ return { shimPath: target, status: "installed" };
51
+ }
52
+ function buildEnforcementHooks(cmd) {
53
+ return {
54
+ SessionStart: [
55
+ {
56
+ hooks: [
57
+ { type: "command", command: `${cmd} hook-session-remind`, timeout: 3 }
58
+ ]
59
+ }
60
+ ],
61
+ PreToolUse: [
62
+ // Session guard — blocks all tools until plur_session_start is called.
63
+ // Must be first so it runs before any other PreToolUse hook.
64
+ {
65
+ matcher: "*",
66
+ hooks: [
67
+ { type: "command", command: `${cmd} hook-session-guard`, timeout: 3 }
68
+ ]
69
+ }
70
+ ],
71
+ PostToolUse: [
72
+ // Session sentinel — creates marker file after plur_session_start succeeds
73
+ {
74
+ matcher: "mcp__plur__plur_session_start",
75
+ hooks: [
76
+ { type: "command", command: `${cmd} hook-session-mark`, timeout: 3 }
77
+ ]
78
+ }
79
+ ]
80
+ };
81
+ }
82
+ function buildInjectionHooks(cmd) {
83
+ return {
84
+ // First message: inject engrams based on the prompt.
85
+ // Subsequent messages: periodic reminder to call plur_learn (~1ms skip).
86
+ UserPromptSubmit: [
87
+ {
88
+ hooks: [
89
+ { type: "command", command: `${cmd} hook-inject`, timeout: 15 }
90
+ ]
91
+ }
92
+ ],
93
+ // Re-inject after context compaction so engrams survive long conversations.
94
+ PostCompact: [
95
+ {
96
+ matcher: "auto|manual",
97
+ hooks: [
98
+ { type: "command", command: `${cmd} hook-inject --rehydrate`, timeout: 15 }
99
+ ]
100
+ }
101
+ ],
102
+ PreToolUse: [
103
+ // Full injection when entering plan mode — planning needs broad context
104
+ {
105
+ matcher: "EnterPlanMode",
106
+ hooks: [
107
+ { type: "command", command: `${cmd} hook-inject --event plan_mode`, timeout: 10 }
108
+ ]
109
+ },
110
+ // Domain-specific engrams when a skill is invoked
111
+ {
112
+ matcher: "Skill",
113
+ hooks: [
114
+ { type: "command", command: `${cmd} hook-inject --event skill`, timeout: 10 }
115
+ ]
116
+ },
117
+ // Agent-scoped engrams when spawning an agent
118
+ {
119
+ matcher: "Agent",
120
+ hooks: [
121
+ { type: "command", command: `${cmd} hook-inject --event agent`, timeout: 10 }
122
+ ]
123
+ },
124
+ // Observation capture — log tool calls for offline pattern extraction
125
+ {
126
+ matcher: "Bash|Edit|Write|Agent",
127
+ hooks: [
128
+ { type: "command", command: `${cmd} hook-observe`, timeout: 3 }
129
+ ]
130
+ }
131
+ ],
132
+ PostToolUse: [
133
+ {
134
+ matcher: "Bash|Edit|Write|Agent",
135
+ hooks: [
136
+ { type: "command", command: `${cmd} hook-observe --post`, timeout: 3 }
137
+ ]
138
+ }
139
+ ],
140
+ // Inject agent-scoped engrams into subagent context
141
+ SubagentStart: [
142
+ {
143
+ matcher: ".*",
144
+ hooks: [
145
+ { type: "command", command: `${cmd} hook-inject --event subagent`, timeout: 10 }
146
+ ]
147
+ }
148
+ ],
149
+ // Learning reflection — nudge the LLM to call plur_learn after responses
150
+ // where it discovered or learned something. Fires every 3rd Stop to avoid fatigue.
151
+ Stop: [
152
+ {
153
+ matcher: "*",
154
+ hooks: [
155
+ { type: "command", command: `${cmd} hook-learn-check`, timeout: 2 }
156
+ ]
157
+ }
158
+ ]
159
+ };
160
+ }
123
161
  function mergeHookMaps(a, b) {
124
162
  const out = {};
125
163
  for (const [event, entries] of Object.entries(a)) out[event] = [...entries];
@@ -224,7 +262,9 @@ function loadSettings(path) {
224
262
  }
225
263
  }
226
264
  function isPlurHook(entry) {
227
- return (entry.hooks ?? []).some((h) => h.command.includes("@plur-ai/cli"));
265
+ return (entry.hooks ?? []).some(
266
+ (h) => h.command.includes("@plur-ai/cli") || h.command.includes(".plur/bin/plur-hook")
267
+ );
228
268
  }
229
269
  function hasPlurHooks(settings) {
230
270
  const hooks = settings.hooks ?? {};
@@ -278,6 +318,10 @@ function hooksStatusFor(before, after, hadHooks) {
278
318
  return before === after ? "already up to date" : "upgraded";
279
319
  }
280
320
  async function run(args, flags) {
321
+ const shim = installHookBinary();
322
+ const cmd = shim.shimPath || "npx @plur-ai/cli";
323
+ const PLUR_HOOKS_ENFORCEMENT = buildEnforcementHooks(cmd);
324
+ const PLUR_HOOKS_INJECTION = buildInjectionHooks(cmd);
281
325
  const injectionPath = findSettingsPath(flags, args);
282
326
  const enforcementPath = join(homedir(), ".claude", "settings.json");
283
327
  const samePath = injectionPath === enforcementPath;
@@ -330,6 +374,8 @@ async function run(args, flags) {
330
374
  const entry = buildMcpServerEntry();
331
375
  outputText("PLUR installed for Claude Code.");
332
376
  outputText("");
377
+ outputText(`Hook binary: ${shim.status}${shim.shimPath ? ` (${shim.shimPath})` : ""}`);
378
+ outputText("");
333
379
  outputText("Architecture: One global engram store (~/.plur/), enforcement hooks global, injection hooks project-scoped.");
334
380
  outputText("Multi-project scoping via domain/scope fields on engrams, not separate installs.");
335
381
  outputText("");
@@ -29,7 +29,7 @@ async function run(args, flags) {
29
29
  return;
30
30
  }
31
31
  if (!subcommand || subcommand === "list") {
32
- const storeList = plur.listStores();
32
+ const storeList = await plur.listStoresAsync();
33
33
  if (shouldOutputJson(flags)) {
34
34
  outputJson({ stores: storeList, count: storeList.length });
35
35
  } else {