@plur-ai/cli 0.9.9 → 0.9.10

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.
@@ -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 = join(process.cwd(), ".plur.yaml");
13
- if (!existsSync(configPath)) return {};
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
- for (const line of content.split("\n")) {
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
- const [key, ...rest] = trimmed.split(":");
21
- const value = rest.join(":").trim();
22
- if (key === "domain") config.domain = value;
23
- if (key === "scope") config.scope = value;
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
- try {
181
- const result = await plur.injectHybrid(task, injectOpts);
182
- if (result.count > 0) {
183
- const parts2 = [];
184
- if (result.directives) parts2.push(result.directives);
185
- if (result.constraints) parts2.push(result.constraints);
186
- if (result.consider) parts2.push(result.consider);
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
- } catch {
191
- const result = plur.inject(task, injectOpts);
192
- if (result.count > 0) {
193
- const parts2 = [];
194
- if (result.directives) parts2.push(result.directives);
195
- if (result.constraints) parts2.push(result.constraints);
196
- if (result.consider) parts2.push(result.consider);
197
- context = parts2.join("\n");
198
- count = result.count;
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`);
@@ -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
+ };
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ function createPlur(flags2) {
49
49
  }
50
50
 
51
51
  // src/index.ts
52
- var VERSION = "0.9.9";
52
+ var VERSION = "0.9.10";
53
53
  var argv = process.argv.slice(2);
54
54
  if (argv.includes("--version") || argv.includes("-v")) {
55
55
  console.log(VERSION);
@@ -82,6 +82,7 @@ Commands:
82
82
  stores list List configured stores
83
83
  stores add <path> Add a knowledge store
84
84
  init Install Claude Code hooks + register plur MCP server
85
+ init-remote Opt this project into recall from a PLUR Enterprise server
85
86
  doctor Diagnose Claude Code / Claude Desktop integration
86
87
  audit [--source X] Audit working memory (claude-code|claw|hermes) for conflicts vs engrams
87
88
  hook-inject (internal) Hook handler for engram injection
@@ -124,6 +125,7 @@ var COMMANDS = {
124
125
  stores: "./commands/stores.js",
125
126
  migrate: "./commands/migrate.js",
126
127
  init: "./commands/init.js",
128
+ "init-remote": "./commands/init-remote.js",
127
129
  doctor: "./commands/doctor.js",
128
130
  audit: "./commands/audit.js",
129
131
  "hook-inject": "./commands/hook-inject.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plur-ai/cli",
3
- "version": "0.9.9",
3
+ "version": "0.9.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "plur": "dist/index.js"