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