@persistmemory/cli 0.3.3 → 0.5.0

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/dist/bin.js CHANGED
@@ -344,7 +344,7 @@ var SignalFired = class extends Error {
344
344
  };
345
345
  function untilAborted(work, signal) {
346
346
  work.catch(() => void 0);
347
- return new Promise((resolve8, reject) => {
347
+ return new Promise((resolve9, reject) => {
348
348
  if (signal.aborted) {
349
349
  reject(new SignalFired());
350
350
  return;
@@ -354,7 +354,7 @@ function untilAborted(work, signal) {
354
354
  work.then(
355
355
  (value) => {
356
356
  signal.removeEventListener("abort", onAbort);
357
- resolve8(value);
357
+ resolve9(value);
358
358
  },
359
359
  (error) => {
360
360
  signal.removeEventListener("abort", onAbort);
@@ -364,14 +364,14 @@ function untilAborted(work, signal) {
364
364
  });
365
365
  }
366
366
  function defaultSleep(ms, signal) {
367
- return new Promise((resolve8, reject) => {
367
+ return new Promise((resolve9, reject) => {
368
368
  if (signal?.aborted) {
369
369
  reject(new AbortError());
370
370
  return;
371
371
  }
372
372
  const timer = setTimeout(() => {
373
373
  signal?.removeEventListener("abort", onAbort);
374
- resolve8();
374
+ resolve9();
375
375
  }, ms);
376
376
  function onAbort() {
377
377
  clearTimeout(timer);
@@ -1404,7 +1404,7 @@ function shortDate(iso) {
1404
1404
  }
1405
1405
 
1406
1406
  // src/help.ts
1407
- var VERSION = true ? "0.3.3" : versionFromManifest();
1407
+ var VERSION = true ? "0.5.0" : versionFromManifest();
1408
1408
  var PACKAGE = "@persistmemory/cli";
1409
1409
  var HELP = `
1410
1410
  pm \u2014 PersistMemory from your terminal
@@ -1446,12 +1446,24 @@ var HELP = `
1446
1446
  remember - capture whatever is piped in
1447
1447
  remember --file <path> capture a file's contents
1448
1448
 
1449
- agent --root <dir> [--root ...] answer file requests from this machine \u2014
1450
- nothing outside those folders is read.
1451
- It stays in the foreground. Background it,
1452
- and stop it later, with:
1453
- nohup pm agent --root ~/Desktop &
1449
+ agent answer file and command requests from this
1450
+ machine. Reads the folders in your home
1451
+ directory \u2014 never anything hidden \u2014 and
1452
+ nothing outside them. It says which ones
1453
+ when it starts. Stays in the foreground;
1454
+ background it and stop it later with:
1455
+ nohup pm agent > ~/agent.log 2>&1 &
1454
1456
  pkill -f "pm agent"
1457
+ agent --root <dir> [--root ...] choose the folders for this run
1458
+
1459
+ Settings live in ~/.persistmemory/agent.json, and apply every time:
1460
+
1461
+ {
1462
+ "roots": ["~/code", "~/Documents"],
1463
+ "mode": "ask", ask | auto-read | plan
1464
+ "allow": ["swift"], programs this machine treats as reads
1465
+ "deny": ["rm"] programs it refuses, whatever is approved
1466
+ }
1455
1467
 
1456
1468
  search <query> search your memory
1457
1469
  list memories the most recent memories
@@ -1610,8 +1622,8 @@ async function startLoopback(options = {}) {
1610
1622
  const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
1611
1623
  let resolveCallback;
1612
1624
  let rejectCallback;
1613
- const received = new Promise((resolve8, reject) => {
1614
- resolveCallback = resolve8;
1625
+ const received = new Promise((resolve9, reject) => {
1626
+ resolveCallback = resolve9;
1615
1627
  rejectCallback = reject;
1616
1628
  });
1617
1629
  const server = createServer((request, response) => {
@@ -1647,9 +1659,9 @@ async function startLoopback(options = {}) {
1647
1659
  response.end(donePage(callback));
1648
1660
  resolveCallback?.(callback);
1649
1661
  });
1650
- await new Promise((resolve8, reject) => {
1662
+ await new Promise((resolve9, reject) => {
1651
1663
  server.once("error", reject);
1652
- server.listen(0, "127.0.0.1", resolve8);
1664
+ server.listen(0, "127.0.0.1", resolve9);
1653
1665
  });
1654
1666
  const address = server.address();
1655
1667
  if (address === null || typeof address === "string") {
@@ -1913,7 +1925,7 @@ function safeEqual(a, b) {
1913
1925
  }
1914
1926
  async function openBrowser(url) {
1915
1927
  const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
1916
- await new Promise((resolve8, reject) => {
1928
+ await new Promise((resolve9, reject) => {
1917
1929
  const child = spawn(command, args, {
1918
1930
  stdio: "ignore",
1919
1931
  // Detached so closing the terminal does not close the browser, and so
@@ -1922,7 +1934,7 @@ async function openBrowser(url) {
1922
1934
  });
1923
1935
  child.once("error", reject);
1924
1936
  child.unref();
1925
- resolve8();
1937
+ resolve9();
1926
1938
  });
1927
1939
  }
1928
1940
  async function describe(response) {
@@ -1984,16 +1996,16 @@ async function currentCredential(resolved, deps) {
1984
1996
  // src/context.ts
1985
1997
  import { createInterface } from "node:readline";
1986
1998
  async function askOnTty(prompt, input = process.stdin, output = process.stdout) {
1987
- return new Promise((resolve8) => {
1999
+ return new Promise((resolve9) => {
1988
2000
  const readline = createInterface({ input, output });
1989
2001
  let answered = false;
1990
2002
  readline.question(prompt, (answer3) => {
1991
2003
  answered = true;
1992
- resolve8(answer3.trim());
2004
+ resolve9(answer3.trim());
1993
2005
  readline.close();
1994
2006
  });
1995
2007
  readline.once("close", () => {
1996
- if (!answered) resolve8("");
2008
+ if (!answered) resolve9("");
1997
2009
  });
1998
2010
  });
1999
2011
  }
@@ -2003,16 +2015,16 @@ var BACKSPACE = "\b";
2003
2015
  async function readSecretFromTty(prompt) {
2004
2016
  const input = process.stdin;
2005
2017
  if (!input.isTTY) {
2006
- return new Promise((resolve8) => {
2018
+ return new Promise((resolve9) => {
2007
2019
  const readline = createInterface({ input });
2008
2020
  let answered = false;
2009
2021
  readline.once("line", (line) => {
2010
2022
  answered = true;
2011
- resolve8(line.trim());
2023
+ resolve9(line.trim());
2012
2024
  readline.close();
2013
2025
  });
2014
2026
  readline.once("close", () => {
2015
- if (!answered) resolve8("");
2027
+ if (!answered) resolve9("");
2016
2028
  });
2017
2029
  });
2018
2030
  }
@@ -2021,14 +2033,14 @@ async function readSecretFromTty(prompt) {
2021
2033
  input.setRawMode?.(true);
2022
2034
  input.resume();
2023
2035
  input.setEncoding("utf8");
2024
- return new Promise((resolve8) => {
2036
+ return new Promise((resolve9) => {
2025
2037
  let value = "";
2026
2038
  const finish2 = () => {
2027
2039
  input.removeListener("data", onData);
2028
2040
  input.setRawMode?.(previouslyRaw);
2029
2041
  input.pause();
2030
2042
  process.stdout.write("\n");
2031
- resolve8(value.trim());
2043
+ resolve9(value.trim());
2032
2044
  };
2033
2045
  const onData = (chunk) => {
2034
2046
  for (const character of chunk) {
@@ -2064,9 +2076,9 @@ async function readStdin() {
2064
2076
 
2065
2077
  // src/commands/agent.ts
2066
2078
  import { hostname } from "node:os";
2067
- import { homedir as homedir2 } from "node:os";
2068
- import { basename, join as join4, resolve as resolve3 } from "node:path";
2069
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
2079
+ import { homedir as homedir3 } from "node:os";
2080
+ import { basename, join as join6, resolve as resolve4 } from "node:path";
2081
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
2070
2082
 
2071
2083
  // src/files.ts
2072
2084
  import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
@@ -2174,6 +2186,335 @@ ${head.join("\n")}`;
2174
2186
  ].join("\n");
2175
2187
  }
2176
2188
 
2189
+ // src/commands/run-command.ts
2190
+ import { spawn as spawn2 } from "node:child_process";
2191
+ import { existsSync as existsSync4, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
2192
+ import { isAbsolute as isAbsolute2, join as join4, resolve as resolvePath } from "node:path";
2193
+ var CLASSES = new Map([
2194
+ // Reads. Report on the filesystem and change nothing.
2195
+ ...[
2196
+ "ls",
2197
+ "cat",
2198
+ "head",
2199
+ "tail",
2200
+ "wc",
2201
+ "file",
2202
+ "stat",
2203
+ "du",
2204
+ "df",
2205
+ "find",
2206
+ "grep",
2207
+ "rg",
2208
+ "fd",
2209
+ "tree",
2210
+ "pwd",
2211
+ "date",
2212
+ "uname",
2213
+ "which",
2214
+ "echo",
2215
+ "sort",
2216
+ "uniq",
2217
+ "diff",
2218
+ "basename",
2219
+ "dirname",
2220
+ "realpath",
2221
+ "ps",
2222
+ "env"
2223
+ ].map((name) => [name, "read"]),
2224
+ // Writes. Recoverable or not, they change the machine.
2225
+ ...[
2226
+ "rm",
2227
+ "mv",
2228
+ "cp",
2229
+ "mkdir",
2230
+ "rmdir",
2231
+ "touch",
2232
+ "chmod",
2233
+ "chown",
2234
+ "ln",
2235
+ "tee",
2236
+ "truncate",
2237
+ "dd",
2238
+ "kill",
2239
+ "killall",
2240
+ "make",
2241
+ "cargo",
2242
+ "swift",
2243
+ "xcodebuild",
2244
+ "gradle",
2245
+ "docker",
2246
+ "brew",
2247
+ "apt",
2248
+ "yum"
2249
+ ].map((name) => [name, "write"]),
2250
+ // A way out. See the trifecta note above.
2251
+ ...["curl", "wget", "nc", "ncat", "telnet", "ssh", "scp", "rsync", "ftp", "http", "httpie"].map((name) => [name, "network"]),
2252
+ // Every command at once, wearing one name.
2253
+ ...["sh", "bash", "zsh", "fish", "python", "python3", "node", "ruby", "perl", "osascript", "eval"].map((name) => [name, "interpreter"])
2254
+ ]);
2255
+ var SUBCOMMAND_READS = /* @__PURE__ */ new Map([
2256
+ [
2257
+ "git",
2258
+ /* @__PURE__ */ new Set([
2259
+ "status",
2260
+ "log",
2261
+ "diff",
2262
+ "show",
2263
+ "branch",
2264
+ "remote",
2265
+ "describe",
2266
+ "blame",
2267
+ "shortlog",
2268
+ "ls-files",
2269
+ "rev-parse",
2270
+ "config"
2271
+ ])
2272
+ ],
2273
+ ["npm", /* @__PURE__ */ new Set(["ls", "list", "view", "outdated", "why", "config"])],
2274
+ ["yarn", /* @__PURE__ */ new Set(["list", "why", "info", "config"])],
2275
+ ["docker", /* @__PURE__ */ new Set(["ps", "images", "logs", "inspect"])]
2276
+ ]);
2277
+ var MAX_OUTPUT_BYTES = 256 * 1024;
2278
+ var TIMEOUT_MS = 2e4;
2279
+ function describe2(argv) {
2280
+ return argv.join(" ");
2281
+ }
2282
+ function programOf(argv) {
2283
+ const first = argv[0] ?? "";
2284
+ return first.split("/").pop() ?? first;
2285
+ }
2286
+ function classify(argv, policy) {
2287
+ const program = programOf(argv);
2288
+ if (policy.allow.includes(program)) return "read";
2289
+ const reads = SUBCOMMAND_READS.get(program);
2290
+ if (reads) {
2291
+ const sub = argv.slice(1).find((one) => !one.startsWith("-"));
2292
+ return sub && reads.has(sub) ? "read" : "write";
2293
+ }
2294
+ return CLASSES.get(program) ?? "unknown";
2295
+ }
2296
+ function judge(argv, policy) {
2297
+ const program = programOf(argv);
2298
+ if (!argv[0]) {
2299
+ return { commandClass: "unknown", automatic: false, refusal: "No command was given.", reason: "empty" };
2300
+ }
2301
+ if (policy.deny.includes(program)) {
2302
+ return {
2303
+ commandClass: "unknown",
2304
+ automatic: false,
2305
+ refusal: `This machine refuses "${program}".`,
2306
+ reason: "on this machine's deny list"
2307
+ };
2308
+ }
2309
+ const commandClass = classify(argv, policy);
2310
+ if (commandClass === "network") {
2311
+ return {
2312
+ commandClass,
2313
+ automatic: false,
2314
+ refusal: `"${program}" can reach the network. This machine will not run it, and no approval enables it: a command that reads private files and can also send them is the one combination nobody can review by looking at it.`,
2315
+ reason: "reads private data and has a way out"
2316
+ };
2317
+ }
2318
+ if (commandClass === "interpreter") {
2319
+ return {
2320
+ commandClass,
2321
+ automatic: false,
2322
+ refusal: `"${program}" runs a language, which is every command at once. Ask for the specific command instead.`,
2323
+ reason: "an interpreter is not one command"
2324
+ };
2325
+ }
2326
+ const outside = pathOutsideRoots(argv, policy.roots);
2327
+ if (outside) {
2328
+ return {
2329
+ commandClass,
2330
+ automatic: false,
2331
+ refusal: `${outside} is outside the folders this machine may read.`,
2332
+ reason: "outside the roots"
2333
+ };
2334
+ }
2335
+ if (policy.mode === "plan") {
2336
+ return {
2337
+ commandClass,
2338
+ automatic: false,
2339
+ reason: "this machine is in plan mode and runs nothing"
2340
+ };
2341
+ }
2342
+ return {
2343
+ commandClass,
2344
+ automatic: policy.mode === "auto-read" && commandClass === "read",
2345
+ reason: commandClass === "read" ? "reads and changes nothing" : commandClass === "write" ? "changes this machine" : "not a command this machine recognises"
2346
+ };
2347
+ }
2348
+ function pathOutsideRoots(argv, roots) {
2349
+ for (const argument of argv.slice(1)) {
2350
+ if (argument.startsWith("-")) continue;
2351
+ if (!argument.startsWith("/") && !argument.startsWith("~") && !argument.startsWith(".") && !argument.includes("/")) {
2352
+ continue;
2353
+ }
2354
+ const resolved = expand(argument, roots[0] ?? process.cwd());
2355
+ if (!roots.some((root) => resolved === root || resolved.startsWith(`${root}/`))) {
2356
+ return argument;
2357
+ }
2358
+ }
2359
+ return void 0;
2360
+ }
2361
+ function expand(argument, base) {
2362
+ const home = process.env["HOME"] ?? "";
2363
+ const withHome = argument.startsWith("~") ? join4(home, argument.slice(1)) : argument;
2364
+ return isAbsolute2(withHome) ? resolvePath(withHome) : resolvePath(base, withHome);
2365
+ }
2366
+ async function runCommand(argv, policy) {
2367
+ const verdict = judge(argv, policy);
2368
+ if (verdict.refusal) return { ok: false, text: verdict.refusal };
2369
+ if (policy.mode === "plan") {
2370
+ return { ok: false, text: `Plan mode: this machine did not run \`${describe2(argv)}\`.` };
2371
+ }
2372
+ const cwd = policy.roots[0];
2373
+ if (!cwd) return { ok: false, text: "This machine has no folders it may read." };
2374
+ try {
2375
+ if (!statSync2(cwd).isDirectory()) return { ok: false, text: `${cwd} is not a folder.` };
2376
+ } catch {
2377
+ return { ok: false, text: `${cwd} does not exist.` };
2378
+ }
2379
+ return new Promise((resolve9) => {
2380
+ const child = spawn2(argv[0], argv.slice(1), {
2381
+ cwd,
2382
+ // NO shell. With one, every character a model can produce is a character
2383
+ // the shell can act on, and the argument list stops meaning anything.
2384
+ shell: false,
2385
+ /*
2386
+ A bare environment.
2387
+
2388
+ This process holds the credential that authorises the machine, and
2389
+ handing it to a subprocess a model chose would mean `env` — or anything
2390
+ that prints its environment — returning that key.
2391
+ */
2392
+ env: {
2393
+ PATH: process.env["PATH"] ?? "/usr/bin:/bin",
2394
+ HOME: process.env["HOME"] ?? "",
2395
+ LANG: process.env["LANG"] ?? "C"
2396
+ }
2397
+ });
2398
+ let output = "";
2399
+ let truncated = false;
2400
+ const collect = (chunk) => {
2401
+ if (truncated) return;
2402
+ output += chunk.toString("utf8");
2403
+ if (output.length > MAX_OUTPUT_BYTES) {
2404
+ output = output.slice(0, MAX_OUTPUT_BYTES);
2405
+ truncated = true;
2406
+ child.kill("SIGKILL");
2407
+ }
2408
+ };
2409
+ child.stdout.on("data", collect);
2410
+ child.stderr.on("data", collect);
2411
+ const timer = setTimeout(() => child.kill("SIGKILL"), TIMEOUT_MS);
2412
+ child.on("error", (error) => {
2413
+ clearTimeout(timer);
2414
+ resolve9({ ok: false, text: `Could not run it: ${error.message}` });
2415
+ });
2416
+ child.on("close", (code) => {
2417
+ clearTimeout(timer);
2418
+ const notes = [
2419
+ truncated ? `
2420
+
2421
+ [output cut off at ${MAX_OUTPUT_BYTES} bytes]` : "",
2422
+ code !== 0 && code !== null ? `
2423
+
2424
+ [exit code ${code}]` : ""
2425
+ ].join("");
2426
+ resolve9({ ok: code === 0, text: `$ ${describe2(argv)}
2427
+
2428
+ ${output}${notes}` });
2429
+ });
2430
+ });
2431
+ }
2432
+
2433
+ // src/commands/agent-config.ts
2434
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync, statSync as statSync3 } from "node:fs";
2435
+ import { homedir as homedir2 } from "node:os";
2436
+ import { join as join5, resolve as resolve3 } from "node:path";
2437
+ var NOT_MATERIAL = /* @__PURE__ */ new Set([
2438
+ "Library",
2439
+ "Applications",
2440
+ "System",
2441
+ "Public",
2442
+ "node_modules",
2443
+ "go",
2444
+ "Parallels",
2445
+ "VirtualBox VMs"
2446
+ ]);
2447
+ function discoverRoots(home = homedir2(), list = (path) => {
2448
+ try {
2449
+ return readdirSync(path);
2450
+ } catch {
2451
+ return [];
2452
+ }
2453
+ }, isDirectory = (path) => {
2454
+ try {
2455
+ return statSync3(path).isDirectory();
2456
+ } catch {
2457
+ return false;
2458
+ }
2459
+ }) {
2460
+ return list(home).filter((name) => !name.startsWith(".")).filter((name) => !NOT_MATERIAL.has(name)).map((name) => join5(home, name)).filter(isDirectory).sort();
2461
+ }
2462
+ function expandHome(path, home = homedir2()) {
2463
+ return path.startsWith("~") ? resolve3(home, path.slice(1).replace(/^[/\\]/, "")) : resolve3(path);
2464
+ }
2465
+ var MODES = /* @__PURE__ */ new Set(["ask", "auto-read", "plan"]);
2466
+ function readAgentConfig(path, read2 = (at) => readFileSync5(at, "utf8"), exists = (at) => existsSync5(at), home = homedir2()) {
2467
+ if (!exists(path)) return {};
2468
+ let parsed;
2469
+ try {
2470
+ parsed = JSON.parse(read2(path));
2471
+ } catch (error) {
2472
+ return { error: `${path} is not valid JSON: ${error instanceof Error ? error.message : ""}` };
2473
+ }
2474
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2475
+ return { error: `${path} should hold an object, like {"roots": ["~/code"]}.` };
2476
+ }
2477
+ const held = parsed;
2478
+ const config = {};
2479
+ if (held["roots"] !== void 0) {
2480
+ if (!Array.isArray(held["roots"]) || held["roots"].some((one) => typeof one !== "string")) {
2481
+ return { error: `${path}: "roots" should be a list of folders.` };
2482
+ }
2483
+ config.roots = held["roots"].map((one) => expandHome(one, home));
2484
+ }
2485
+ if (held["mode"] !== void 0) {
2486
+ if (typeof held["mode"] !== "string" || !MODES.has(held["mode"])) {
2487
+ return { error: `${path}: "mode" should be "ask", "auto-read" or "plan".` };
2488
+ }
2489
+ config.mode = held["mode"];
2490
+ }
2491
+ for (const key of ["allow", "deny"]) {
2492
+ if (held[key] === void 0) continue;
2493
+ if (!Array.isArray(held[key]) || held[key].some((one) => typeof one !== "string")) {
2494
+ return { error: `${path}: "${key}" should be a list of program names.` };
2495
+ }
2496
+ config[key] = held[key];
2497
+ }
2498
+ return { config };
2499
+ }
2500
+ function resolveAgentConfig(args) {
2501
+ const roots = args.flagRoots && args.flagRoots.length > 0 ? args.flagRoots : args.stored?.roots && args.stored.roots.length > 0 ? args.stored.roots : args.discovered ?? [];
2502
+ return {
2503
+ roots,
2504
+ /*
2505
+ `ask` unless the owner said otherwise.
2506
+
2507
+ The mode decides how much happens with nobody watching, so the default
2508
+ is the one that does least. `auto-read` is a reasonable thing to choose
2509
+ and an unreasonable thing to be given.
2510
+ */
2511
+ mode: args.stored?.mode ?? "ask",
2512
+ allow: args.stored?.allow ?? [],
2513
+ deny: args.stored?.deny ?? [],
2514
+ source: args.flagRoots && args.flagRoots.length > 0 ? "flags" : args.stored?.roots && args.stored.roots.length > 0 ? "config" : "discovered"
2515
+ };
2516
+ }
2517
+
2177
2518
  // src/commands/agent.ts
2178
2519
  async function said(response, fallback) {
2179
2520
  const body = await response.json().catch(() => void 0);
@@ -2181,7 +2522,7 @@ async function said(response, fallback) {
2181
2522
  return message2 ?? `${fallback} (${response.status})`;
2182
2523
  }
2183
2524
  function locate(roots, requested) {
2184
- const expanded = requested.startsWith("~") ? resolve3(homedir2(), requested.slice(1).replace(/^[/\\]/, "")) : requested;
2525
+ const expanded = requested.startsWith("~") ? resolve4(homedir3(), requested.slice(1).replace(/^[/\\]/, "")) : requested;
2185
2526
  if (!expanded.includes("/") && !expanded.includes("\\")) {
2186
2527
  const wanted2 = expanded.trim().toLowerCase();
2187
2528
  const named = roots.find((root) => basename(root).toLowerCase() === wanted2);
@@ -2197,18 +2538,38 @@ function locate(roots, requested) {
2197
2538
  throw new Error(`${requested} is not inside any allowed folder (${roots.join(", ")})`);
2198
2539
  }
2199
2540
  function uncontested(target) {
2200
- if (!existsSync4(target)) return target;
2541
+ if (!existsSync6(target)) return target;
2201
2542
  const dot = target.lastIndexOf(".");
2202
2543
  const stem = dot > target.lastIndexOf("/") && dot !== -1 ? target.slice(0, dot) : target;
2203
2544
  const extension = stem === target ? "" : target.slice(dot);
2204
2545
  for (let n = 1; n < 1e3; n += 1) {
2205
2546
  const candidate = `${stem} (${n})${extension}`;
2206
- if (!existsSync4(candidate)) return candidate;
2547
+ if (!existsSync6(candidate)) return candidate;
2207
2548
  }
2208
2549
  throw new Error(`${target} and a thousand names beside it are taken.`);
2209
2550
  }
2210
- async function answer(context, apiUrl, token, roots, request) {
2551
+ async function answer(context, apiUrl, token, settings, request) {
2552
+ const roots = settings.roots;
2211
2553
  let located;
2554
+ if (request.kind === "run_command") {
2555
+ if (!request.argv || request.argv.length === 0) {
2556
+ return { ok: false, error: "No command was given." };
2557
+ }
2558
+ const policy = {
2559
+ mode: settings.mode,
2560
+ allow: settings.allow,
2561
+ deny: settings.deny,
2562
+ roots
2563
+ };
2564
+ const outcome = await runCommand(request.argv, policy);
2565
+ if (!outcome.ok) return { ok: false, error: outcome.text };
2566
+ return upload(
2567
+ apiUrl,
2568
+ token,
2569
+ `${(request.argv[0] ?? "output").split("/").pop()}.txt`,
2570
+ Buffer.from(outcome.text, "utf8")
2571
+ );
2572
+ }
2212
2573
  try {
2213
2574
  located = locate(roots, request.path);
2214
2575
  } catch (error) {
@@ -2229,10 +2590,10 @@ async function answer(context, apiUrl, token, roots, request) {
2229
2590
  }
2230
2591
  try {
2231
2592
  let target = located;
2232
- if (existsSync4(located) && statSync2(located).isDirectory()) {
2593
+ if (existsSync6(located) && statSync4(located).isDirectory()) {
2233
2594
  const disposition = fetched.headers.get("content-disposition") ?? "";
2234
2595
  const named = /filename="([^"]+)"/.exec(disposition)?.[1];
2235
- target = join4(located, basename(named ?? "file"));
2596
+ target = join6(located, basename(named ?? "file"));
2236
2597
  }
2237
2598
  target = uncontested(target);
2238
2599
  writeFileSync4(target, downloaded, { flag: "wx" });
@@ -2255,12 +2616,12 @@ ${downloaded.length} bytes
2255
2616
  let filename = request.path.split("/").pop() ?? "file";
2256
2617
  if (request.kind === "list_dir") {
2257
2618
  try {
2258
- const stats = statSync2(located);
2619
+ const stats = statSync4(located);
2259
2620
  if (!stats.isDirectory()) return { ok: false, error: `${request.path} is not a folder.` };
2260
- const entries = readdirSync(located, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).slice(0, MAX_LISTED).map((entry) => {
2621
+ const entries = readdirSync2(located, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).slice(0, MAX_LISTED).map((entry) => {
2261
2622
  if (entry.isDirectory()) return `${entry.name}/`;
2262
2623
  try {
2263
- return `${entry.name} ${sizeOf(join4(located, entry.name))}`;
2624
+ return `${entry.name} ${sizeOf(join6(located, entry.name))}`;
2264
2625
  } catch {
2265
2626
  return entry.name;
2266
2627
  }
@@ -2278,9 +2639,9 @@ ${listing}
2278
2639
  }
2279
2640
  }
2280
2641
  try {
2281
- const stats = statSync2(located);
2642
+ const stats = statSync4(located);
2282
2643
  if (!stats.isFile()) return { ok: false, error: `${request.path} is not a file.` };
2283
- bytes = readFileSync4(located);
2644
+ bytes = readFileSync6(located);
2284
2645
  } catch (error) {
2285
2646
  return { ok: false, error: error instanceof Error ? error.message : "could not read it" };
2286
2647
  }
@@ -2322,7 +2683,7 @@ async function upload(apiUrl, token, filename, bytes) {
2322
2683
  }
2323
2684
  var MAX_LISTED = 200;
2324
2685
  function sizeOf(path) {
2325
- const size = statSync2(path).size;
2686
+ const size = statSync4(path).size;
2326
2687
  if (size < 1024) return `${size} B`;
2327
2688
  if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
2328
2689
  return `${(size / (1024 * 1024)).toFixed(1)} MB`;
@@ -2334,25 +2695,41 @@ async function agentCommand(context) {
2334
2695
  return 1;
2335
2696
  }
2336
2697
  const roots = (listFlag(context.args, "root") ?? []).map(
2337
- (one) => resolve3(one.startsWith("~") ? resolve3(homedir2(), one.slice(1).replace(/^[/\\]/, "")) : one)
2698
+ (one) => resolve4(one.startsWith("~") ? resolve4(homedir3(), one.slice(1).replace(/^[/\\]/, "")) : one)
2338
2699
  );
2339
- if (roots.length === 0) {
2700
+ const configPath = join6(context.paths.dir, "agent.json");
2701
+ const stored = readAgentConfig(configPath);
2702
+ if (stored.error) {
2703
+ context.error(stored.error);
2704
+ return 1;
2705
+ }
2706
+ const settings = resolveAgentConfig({
2707
+ flagRoots: roots,
2708
+ ...stored.config ? { stored: stored.config } : {},
2709
+ discovered: discoverRoots()
2710
+ });
2711
+ if (settings.roots.length === 0) {
2340
2712
  context.error(
2341
- "Say which folders this machine may read from, and nothing outside them will be:"
2713
+ "There is nothing in your home directory this machine would offer, so say which folders it may read from:"
2342
2714
  );
2343
2715
  context.error("");
2344
- context.error(" pm agent --root ~/Desktop --root ~/Documents");
2345
- context.error("");
2346
- context.error("It stays in the foreground. To leave it running:");
2716
+ context.error(" pm agent --root ~/projects");
2347
2717
  context.error("");
2348
- context.error(" nohup pm agent --root ~/Desktop > ~/agent.log 2>&1 &");
2718
+ context.error(`Or write them once in ${configPath}:`);
2349
2719
  context.error("");
2350
- context.error('And to stop it later: pkill -f "pm agent"');
2720
+ context.error(' { "roots": ["~/projects"], "mode": "ask" }');
2351
2721
  return 1;
2352
2722
  }
2723
+ roots.length = 0;
2724
+ roots.push(...settings.roots);
2725
+ const shown = settings.roots.map((path) => path.replace(homedir3(), "~"));
2726
+ const where = settings.source === "flags" ? "" : settings.source === "config" ? ` (from ${configPath})` : " (found in your home directory)";
2727
+ context.print(
2728
+ `Reading ${shown.join(", ")} \u2014 nothing outside them${where}. Mode: ${settings.mode}.`
2729
+ );
2353
2730
  for (const root of roots) {
2354
2731
  try {
2355
- if (!statSync2(root).isDirectory()) {
2732
+ if (!statSync4(root).isDirectory()) {
2356
2733
  context.error(`${root} is not a folder.`);
2357
2734
  return 1;
2358
2735
  }
@@ -2426,7 +2803,7 @@ async function agentCommand(context) {
2426
2803
  const { items } = await claimed.json();
2427
2804
  for (const request of items) {
2428
2805
  context.print(`Reading ${request.path}`);
2429
- const outcome = await answer(context, apiUrl, await authorization(), roots, request);
2806
+ const outcome = await answer(context, apiUrl, await authorization(), settings, request);
2430
2807
  const done = await call(
2431
2808
  `complete/${encodeURIComponent(request.id)}`,
2432
2809
  outcome.ok ? { result: { attachToken: outcome.attachToken } } : { error: outcome.error }
@@ -2448,8 +2825,8 @@ async function agentCommand(context) {
2448
2825
 
2449
2826
  // src/commands/google.ts
2450
2827
  import { writeFileSync as writeFileSync5 } from "node:fs";
2451
- import { basename as basename2, resolve as resolve4 } from "node:path";
2452
- import { readFileSync as readFileSync5 } from "node:fs";
2828
+ import { basename as basename2, resolve as resolve5 } from "node:path";
2829
+ import { readFileSync as readFileSync7 } from "node:fs";
2453
2830
  async function callApi(context, path, init = {}) {
2454
2831
  const credential = context.resolved.credential;
2455
2832
  if (!credential) {
@@ -2514,7 +2891,7 @@ async function driveGet(context, fileId) {
2514
2891
  const disposition = response.headers.get("content-disposition") ?? "";
2515
2892
  const named = /filename="([^"]+)"/.exec(disposition)?.[1];
2516
2893
  const out = stringFlag(context.args, "out");
2517
- const target = resolve4(out ?? basename2(named ?? fileId));
2894
+ const target = resolve5(out ?? basename2(named ?? fileId));
2518
2895
  writeFileSync5(target, Buffer.from(await response.arrayBuffer()));
2519
2896
  context.print(target);
2520
2897
  return 0;
@@ -2526,7 +2903,7 @@ async function drivePut(context, path) {
2526
2903
  }
2527
2904
  let bytes;
2528
2905
  try {
2529
- bytes = readFileSync5(resolve4(path));
2906
+ bytes = readFileSync7(resolve5(path));
2530
2907
  } catch {
2531
2908
  context.error(`Cannot read ${path}.`);
2532
2909
  return 1;
@@ -2610,8 +2987,8 @@ async function mailCommand(context) {
2610
2987
  }
2611
2988
 
2612
2989
  // src/commands/requests.ts
2613
- import { existsSync as existsSync5, writeFileSync as writeFileSync6 } from "node:fs";
2614
- import { resolve as resolve5 } from "node:path";
2990
+ import { existsSync as existsSync7, writeFileSync as writeFileSync6 } from "node:fs";
2991
+ import { resolve as resolve6 } from "node:path";
2615
2992
  async function requestsCommand(context) {
2616
2993
  if (context.args.words[1] === "get") return collectCommand(context);
2617
2994
  const credential = context.resolved.credential;
@@ -2679,8 +3056,8 @@ async function collectCommand(context) {
2679
3056
  return 1;
2680
3057
  }
2681
3058
  const name = stringFlag(context.args, "output", "o") ?? filename;
2682
- const target = resolve5(name);
2683
- if (existsSync5(target)) {
3059
+ const target = resolve6(name);
3060
+ if (existsSync7(target)) {
2684
3061
  context.error(`${target} already exists. Pass --output to write somewhere else.`);
2685
3062
  return 1;
2686
3063
  }
@@ -2690,14 +3067,14 @@ async function collectCommand(context) {
2690
3067
  }
2691
3068
 
2692
3069
  // src/workspace.ts
2693
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "node:fs";
2694
- import { dirname as dirname3, join as join5, resolve as resolvePath } from "node:path";
3070
+ import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
3071
+ import { dirname as dirname3, join as join7, resolve as resolvePath2 } from "node:path";
2695
3072
  var WORKSPACE_FILE = ".persistmemory.json";
2696
3073
  function findWorkspace(from = process.cwd()) {
2697
- let dir = resolvePath(from);
3074
+ let dir = resolvePath2(from);
2698
3075
  for (; ; ) {
2699
- const file = join5(dir, WORKSPACE_FILE);
2700
- if (existsSync6(file)) {
3076
+ const file = join7(dir, WORKSPACE_FILE);
3077
+ if (existsSync8(file)) {
2701
3078
  const config = readWorkspace(file);
2702
3079
  if (config) return { file, dir, config };
2703
3080
  }
@@ -2708,7 +3085,7 @@ function findWorkspace(from = process.cwd()) {
2708
3085
  }
2709
3086
  function readWorkspace(file) {
2710
3087
  try {
2711
- const parsed = JSON.parse(readFileSync6(file, "utf8"));
3088
+ const parsed = JSON.parse(readFileSync8(file, "utf8"));
2712
3089
  const space = parsed.space;
2713
3090
  if (space && typeof space === "object" && typeof space.id === "string" && space.id !== "" && typeof space.name === "string") {
2714
3091
  return { space: { id: space.id, name: space.name } };
@@ -2719,7 +3096,7 @@ function readWorkspace(file) {
2719
3096
  }
2720
3097
  }
2721
3098
  function writeWorkspace(dir, config) {
2722
- const file = join5(dir, WORKSPACE_FILE);
3099
+ const file = join7(dir, WORKSPACE_FILE);
2723
3100
  writeFileSync7(file, `${JSON.stringify(config, null, 2)}
2724
3101
  `, "utf8");
2725
3102
  return file;
@@ -2989,9 +3366,9 @@ function message(error) {
2989
3366
  }
2990
3367
 
2991
3368
  // src/commands/maintain.ts
2992
- import { existsSync as existsSync7, rmSync } from "node:fs";
3369
+ import { existsSync as existsSync9, rmSync } from "node:fs";
2993
3370
  import { spawnSync } from "node:child_process";
2994
- import { dirname as dirname4, resolve as resolve7 } from "node:path";
3371
+ import { dirname as dirname4, resolve as resolve8 } from "node:path";
2995
3372
  import { fileURLToPath } from "node:url";
2996
3373
  function installArgs(pkg = PACKAGE) {
2997
3374
  return ["install", "-g", "--prefer-online", `${pkg}@latest`];
@@ -3055,7 +3432,7 @@ Delete the file it runs from: ${processPath()}`
3055
3432
  return 0;
3056
3433
  }
3057
3434
  async function deleteCommand(context) {
3058
- if (!existsSync7(context.paths.dir)) {
3435
+ if (!existsSync9(context.paths.dir)) {
3059
3436
  context.print(`Nothing to delete: ${context.paths.dir} does not exist.`);
3060
3437
  return 0;
3061
3438
  }
@@ -3084,7 +3461,7 @@ async function deleteCommand(context) {
3084
3461
  return 0;
3085
3462
  }
3086
3463
  function removeEverything(context) {
3087
- const dir = resolve7(context.paths.dir);
3464
+ const dir = resolve8(context.paths.dir);
3088
3465
  if (dir === "/" || dir.split("/").filter(Boolean).length < 2) {
3089
3466
  context.error(`Refusing to delete ${dir}: that does not look like a data directory.`);
3090
3467
  return;
@@ -3096,7 +3473,7 @@ function installer() {
3096
3473
  }
3097
3474
  function processPath() {
3098
3475
  try {
3099
- return resolve7(dirname4(fileURLToPath(import.meta.url)));
3476
+ return resolve8(dirname4(fileURLToPath(import.meta.url)));
3100
3477
  } catch {
3101
3478
  return process.argv[1] ?? "";
3102
3479
  }
@@ -3108,12 +3485,12 @@ import { randomUUID } from "node:crypto";
3108
3485
  import { relative as relative2 } from "node:path";
3109
3486
 
3110
3487
  // src/events.ts
3111
- import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
3112
- import { join as join6 } from "node:path";
3488
+ import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync9 } from "node:fs";
3489
+ import { join as join8 } from "node:path";
3113
3490
  function openSessionLog(paths, id) {
3114
- const directory = join6(paths.dir, "sessions");
3491
+ const directory = join8(paths.dir, "sessions");
3115
3492
  mkdirSync3(directory, { recursive: true, mode: 448 });
3116
- const path = join6(directory, `${id}.jsonl`);
3493
+ const path = join8(directory, `${id}.jsonl`);
3117
3494
  return {
3118
3495
  id,
3119
3496
  path,
@@ -3125,8 +3502,8 @@ function openSessionLog(paths, id) {
3125
3502
  }
3126
3503
  },
3127
3504
  read() {
3128
- if (!existsSync8(path)) return [];
3129
- return readFileSync7(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
3505
+ if (!existsSync10(path)) return [];
3506
+ return readFileSync9(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
3130
3507
  try {
3131
3508
  return [JSON.parse(line)];
3132
3509
  } catch {
@@ -3203,9 +3580,9 @@ async function sessionCommand(context) {
3203
3580
  context.print(` Ask anything. /help for commands, /exit to leave.
3204
3581
  `);
3205
3582
  const readline = createInterface2({ input: process.stdin, output: process.stdout });
3206
- const ask = (prompt) => new Promise((resolve8) => {
3207
- readline.question(prompt, resolve8);
3208
- readline.once("close", () => resolve8(void 0));
3583
+ const ask = (prompt) => new Promise((resolve9) => {
3584
+ readline.question(prompt, resolve9);
3585
+ readline.once("close", () => resolve9(void 0));
3209
3586
  });
3210
3587
  const root = process.cwd();
3211
3588
  let running = true;
@@ -3389,7 +3766,7 @@ async function write2(args) {
3389
3766
  }
3390
3767
 
3391
3768
  // src/commands/memory.ts
3392
- import { readFileSync as readFileSync8 } from "node:fs";
3769
+ import { readFileSync as readFileSync10 } from "node:fs";
3393
3770
 
3394
3771
  // src/spaces.ts
3395
3772
  async function spacesFor(context, client, env = process.env) {
@@ -3453,7 +3830,7 @@ async function rememberCommand(context) {
3453
3830
  let text;
3454
3831
  if (file) {
3455
3832
  try {
3456
- text = readFileSync8(file, "utf8");
3833
+ text = readFileSync10(file, "utf8");
3457
3834
  } catch {
3458
3835
  context.error(`Could not read ${file}.`);
3459
3836
  return 1;