@persistmemory/cli 0.8.1 → 0.9.1

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
@@ -1470,7 +1470,8 @@ function pathsFor(home = homedir()) {
1470
1470
  return {
1471
1471
  dir,
1472
1472
  config: join(dir, "config.json"),
1473
- credentials: join(dir, "credentials.json")
1473
+ credentials: join(dir, "credentials.json"),
1474
+ agentPid: join(dir, "agent.pid")
1474
1475
  };
1475
1476
  }
1476
1477
  function readConfig(paths) {
@@ -1710,7 +1711,7 @@ function shortDate(iso) {
1710
1711
  }
1711
1712
 
1712
1713
  // src/help.ts
1713
- var VERSION = true ? "0.8.1" : versionFromManifest();
1714
+ var VERSION = true ? "0.9.1" : versionFromManifest();
1714
1715
  var PACKAGE = "@persistmemory/cli";
1715
1716
  var HELP = `
1716
1717
  pm \u2014 PersistMemory from your terminal
@@ -1779,9 +1780,10 @@ var HELP = `
1779
1780
  approve every request first, and see the
1780
1781
  exact path or command before you do.
1781
1782
  Stays in the foreground; background it
1782
- and stop it later with:
1783
+ with:
1783
1784
  nohup pm agent </dev/null >> ~/agent.log 2>&1 &
1784
- pkill -f "pm agent"
1785
+ agent stop stop the agent running on this machine.
1786
+ Lets the request in flight finish first.
1785
1787
  agent --root <dir> [--root ...] narrow it to these folders, for this run
1786
1788
  agent enable [hostname] turn this machine back on after it was
1787
1789
  switched off for going quiet. Asks you to
@@ -2514,7 +2516,8 @@ async function readStdin() {
2514
2516
  import { hostname } from "node:os";
2515
2517
  import { homedir as homedir2 } from "node:os";
2516
2518
  import { basename, join as join6, resolve as resolve3 } from "node:path";
2517
- import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
2519
+ import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
2520
+ import { execFileSync } from "node:child_process";
2518
2521
 
2519
2522
  // src/files.ts
2520
2523
  import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
@@ -7288,8 +7291,8 @@ var ENDS = /,|\s+(?:and|then|so|but|because|which|where|that|with|to)\s+/i;
7288
7291
  var ASKS_FOR_A_SPACE = /\b(?:make|create|start|open|add|set\s?up|setup|spin\s?up|new)\b[^.;!?]{0,40}?\b(?:work[\s-]?)?spaces?\b/gi;
7289
7292
  var NAMED_A_SPACE = /\b(?:make|create|start|open|add|set\s?up|setup|spin\s?up|new)\b(?:\s+(?:me|us))?(?:\s+(?:a|an|the|my|our|another|one|new|second|separate|empty))*\s+(?:work[\s-]?)?spaces?\b(?:\s*(?:called|named|titled|for|about))?\s*(?:[:\-–—]\s*)?([^.;!?]*)/gi;
7290
7293
  var SPACE_NAMED_FIRST = /\b(?:make|create|start|open|add|set\s?up|setup|spin\s?up|new)\b(?:\s+(?:a|an|the|my|our|another|one|new|second|separate|empty))*\s+((?:[\p{L}\p{N}][\p{L}\p{N}&'.-]*\s+){0,2}[\p{L}\p{N}][\p{L}\p{N}&'.-]*)\s+(?:work\s?)?spaces?\b/giu;
7291
- function flatten(said2) {
7292
- return said2.normalize("NFKC").replace(/[‘’‚‛′]/g, "'").replace(/[“”„‟″«»]/g, '"').replace(/\s+/g, " ").trim();
7294
+ function flatten(said3) {
7295
+ return said3.normalize("NFKC").replace(/[‘’‚‛′]/g, "'").replace(/[“”„‟″«»]/g, '"').replace(/\s+/g, " ").trim();
7293
7296
  }
7294
7297
  function tidy(name) {
7295
7298
  return name.replace(/^["']+|["']+$/g, "").replace(/[.,;:!?]+$/g, "").replace(/\s+/g, " ").trim().toLowerCase();
@@ -7320,9 +7323,9 @@ function readings(tail) {
7320
7323
  }
7321
7324
  return found;
7322
7325
  }
7323
- function spacesTheyAskedToMake(said2) {
7324
- if (typeof said2 !== "string") return [];
7325
- const text = flatten(said2);
7326
+ function spacesTheyAskedToMake(said3) {
7327
+ if (typeof said3 !== "string") return [];
7328
+ const text = flatten(said3);
7326
7329
  if (text === "" || text.length > AT_MOST) return [];
7327
7330
  if ([...text.matchAll(ASKS_FOR_A_SPACE)].length !== 1) return [];
7328
7331
  const named = [...text.matchAll(NAMED_A_SPACE)];
@@ -7335,11 +7338,40 @@ function spacesTheyAskedToMake(said2) {
7335
7338
  const only = tidy((first[0]?.[1] ?? "").replace(LEADING, ""));
7336
7339
  return nameable(only) ? [only] : [];
7337
7340
  }
7338
- function askedToMakeIt(said2, name) {
7339
- if (typeof said2 !== "string" || typeof name !== "string") return false;
7341
+ function askedToMakeIt(said3, name) {
7342
+ if (typeof said3 !== "string" || typeof name !== "string") return false;
7340
7343
  const wanted2 = tidy(name);
7341
7344
  if (!nameable(wanted2)) return false;
7342
- return spacesTheyAskedToMake(said2).includes(wanted2);
7345
+ return spacesTheyAskedToMake(said3).includes(wanted2);
7346
+ }
7347
+ var ASKS_TO_WORK_IN = /\b(?:work(?:ing)?\s+in|select|choose|pick|switch\s+to|move\s+to|use)\b[^.;!?]{0,40}?\bspaces?\b|\bwork(?:ing)?\s+in\b/gi;
7348
+ var WORK_IN_NAMED = /\b(?:select|choose|pick|switch\s+to|move\s+to|use|work(?:ing)?\s+in)\s+(?:the\s+|my\s+|our\s+)?spaces?\s+(?:called\s+|named\s+)?(.{1,200})$/i;
7349
+ var WORK_IN_FIRST = /\b(?:select|choose|pick|switch\s+to|move\s+to|use|work(?:ing)?\s+in)\s+(?:the\s+|my\s+|our\s+)?(.{1,200}?)\s+spaces?\b/i;
7350
+ var WORK_IN_BARE = /\bwork(?:ing)?\s+in\s+(?:the\s+|my\s+|our\s+)?(.{1,200})$/i;
7351
+ function spacesTheyAskedToWorkIn(said3) {
7352
+ if (typeof said3 !== "string") return [];
7353
+ const text = flatten(said3);
7354
+ if (text === "" || text.length > AT_MOST) return [];
7355
+ if ([...text.matchAll(ASKS_TO_WORK_IN)].length !== 1) return [];
7356
+ const first = WORK_IN_FIRST.exec(text);
7357
+ if (first) {
7358
+ const only = tidy((first[1] ?? "").replace(LEADING, ""));
7359
+ return nameable(only) ? [only] : [];
7360
+ }
7361
+ const named = WORK_IN_NAMED.exec(text);
7362
+ if (named) {
7363
+ const found = readings(named[1] ?? "");
7364
+ if (found.length > 0) return found;
7365
+ }
7366
+ const bare = WORK_IN_BARE.exec(text);
7367
+ if (bare) return readings(bare[1] ?? "");
7368
+ return [];
7369
+ }
7370
+ function askedToWorkIn(said3, name) {
7371
+ if (typeof said3 !== "string" || typeof name !== "string") return false;
7372
+ const wanted2 = tidy(name);
7373
+ if (!nameable(wanted2)) return false;
7374
+ return spacesTheyAskedToWorkIn(said3).includes(wanted2);
7343
7375
  }
7344
7376
 
7345
7377
  // ../tools/src/catalogue.ts
@@ -7791,10 +7823,10 @@ var TOOLS = [
7791
7823
  },
7792
7824
  ...context.origin?.askedBy ? { confirmedBy: context.origin.askedBy } : {}
7793
7825
  });
7794
- const said2 = `Forwarded to ${String(args.to)}. It went whole, with everything that was attached to it.`;
7826
+ const said3 = `Forwarded to ${String(args.to)}. It went whole, with everything that was attached to it.`;
7795
7827
  return {
7796
- model: `${said2} Report what happened and stop. Do not forward anything else and do not offer to \u2014 another message, or another address, is a new decision that is theirs to make.`,
7797
- person: `${said2} There is no unsending it \u2014 if it went to the wrong address, tell them.`
7828
+ model: `${said3} Report what happened and stop. Do not forward anything else and do not offer to \u2014 another message, or another address, is a new decision that is theirs to make.`,
7829
+ person: `${said3} There is no unsending it \u2014 if it went to the wrong address, tell them.`
7798
7830
  };
7799
7831
  }
7800
7832
  },
@@ -8238,15 +8270,15 @@ var TOOLS = [
8238
8270
  a wider search that still finds the file beats a refusal.
8239
8271
  */
8240
8272
  argsFrom: (rest) => {
8241
- const said2 = rest.trim();
8242
- if (!said2) return void 0;
8243
- const at = said2.lastIndexOf(" in ");
8273
+ const said3 = rest.trim();
8274
+ if (!said3) return void 0;
8275
+ const at = said3.lastIndexOf(" in ");
8244
8276
  if (at > 0) {
8245
- const what = said2.slice(0, at).trim();
8246
- const where = said2.slice(at + 4).trim();
8277
+ const what = said3.slice(0, at).trim();
8278
+ const where = said3.slice(at + 4).trim();
8247
8279
  if (what && where && !where.includes(" ")) return { what, in: where };
8248
8280
  }
8249
- return { what: said2 };
8281
+ return { what: said3 };
8250
8282
  },
8251
8283
  missing: "Say what to look for, like: /find deployment notes in ~/Documents"
8252
8284
  },
@@ -9113,10 +9145,10 @@ var TOOLS = [
9113
9145
  ...context.origin?.askedBy ? { askedBy: context.origin.askedBy } : {}
9114
9146
  });
9115
9147
  if (!offered.ok) return offered.error;
9116
- const said2 = `Invited ${offered.value.email} to \u201C${held.space.name}\u201D as ${offered.value.role}. They cannot see anything yet \u2014 the invitation has to be accepted first.`;
9148
+ const said3 = `Invited ${offered.value.email} to \u201C${held.space.name}\u201D as ${offered.value.role}. They cannot see anything yet \u2014 the invitation has to be accepted first.`;
9117
9149
  return {
9118
- model: `${said2} That grant covers EVERYTHING in that Space, now and later. Report what happened and stop. Do not share anything else, and do not offer to \u2014 another Space, another address, or a wider role is a new decision that is theirs to make.`,
9119
- person: `${said2} While it stands they will see everything in \u201C${held.space.name}\u201D, including memories filed into it after today. Undo it with /unshare ${held.space.name} ${offered.value.email}`
9150
+ model: `${said3} That grant covers EVERYTHING in that Space, now and later. Report what happened and stop. Do not share anything else, and do not offer to \u2014 another Space, another address, or a wider role is a new decision that is theirs to make.`,
9151
+ person: `${said3} While it stands they will see everything in \u201C${held.space.name}\u201D, including memories filed into it after today. Undo it with /unshare ${held.space.name} ${offered.value.email}`
9120
9152
  };
9121
9153
  }
9122
9154
  },
@@ -9261,8 +9293,8 @@ var TOOLS = [
9261
9293
  ...context.origin?.askedBy ? { askedBy: context.origin.askedBy } : {}
9262
9294
  });
9263
9295
  if (!changed.ok) return changed.error;
9264
- const said2 = `${changed.value.email} is now ${changed.value.role} on \u201C${held.space.name}\u201D.`;
9265
- return changed.value.role === "owner" ? `${said2} As an owner they can share it onward and revoke anybody, including you.` : said2;
9296
+ const said3 = `${changed.value.email} is now ${changed.value.role} on \u201C${held.space.name}\u201D.`;
9297
+ return changed.value.role === "owner" ? `${said3} As an owner they can share it onward and revoke anybody, including you.` : said3;
9266
9298
  }
9267
9299
  },
9268
9300
  /* --------------------- which Space this is filing into --------------------- */
@@ -9438,12 +9470,14 @@ The other Spaces on this account: ${others.join(", ")}. They can move this conve
9438
9470
  };
9439
9471
  }
9440
9472
  const to = found[0];
9473
+ const readable = look.spaces.shared ? await look.spaces.shared(look.userId, to.id).then((one) => one.ok ? one.value : true).catch(() => true) : true;
9441
9474
  return {
9442
9475
  ok: true,
9443
9476
  facts: {
9444
9477
  from,
9445
9478
  to: to.name,
9446
9479
  toId: to.id,
9480
+ shared: readable,
9447
9481
  where: here.value.where,
9448
9482
  unchanged: to.id === (here.value.chosen?.id ?? ""),
9449
9483
  leaving: Boolean(here.value.chosen)
@@ -9516,11 +9550,13 @@ RIGHT NOW: ${where} is filing into ${from}.`;
9516
9550
  Both conditions, and every unclear case is false. See
9517
9551
  `their-own-words.ts` for what this does not cover.
9518
9552
  */
9519
- directly(args, facts, said2) {
9520
- if (facts["creating"] !== true) return false;
9553
+ directly(args, facts, said3, alsoRead) {
9521
9554
  const wanted2 = oneLine2(args.space, 200);
9522
9555
  if (wanted2 === void 0) return false;
9523
- return askedToMakeIt(said2, wanted2);
9556
+ if (facts["creating"] === true) return askedToMakeIt(said3, wanted2);
9557
+ if (facts["shared"] !== false) return false;
9558
+ const named = (alsoRead ?? []).map((one) => one.trim().toLowerCase());
9559
+ return askedToWorkIn(said3, wanted2) || named.includes(wanted2.trim().toLowerCase());
9524
9560
  }
9525
9561
  },
9526
9562
  description: "Files everything sent from this conversation into one Space from now on, and answers questions asked here from that Space alone. NOTHING IS KEPT AT ALL until this is done: there is no default Space and nothing picks one, so a conversation with no Space chosen refuses every note. Use it when the person says something like 'work in Acme', 'put all of this in my Work space', or when they have been told nothing is being kept and they name where it should go. Pass no Space to stop working in one, which means nothing sent here is kept until they choose again. Set `create` ONLY when they are asking for a Space that does not exist yet \u2014 usually because the account has none at all \u2014 and never to resolve a name you are unsure of. WHAT HAPPENS WHEN YOU CALL THIS DEPENDS ON THE ACT, AND THE ANSWER SAYS WHICH: moving into a Space that already exists is put in front of the person and they have to release it themselves, so say it is waiting for them and stop; making a NEW one, when they asked for it in their own message, simply happens, and the answer says so. Read what comes back and repeat what it says. Never say something is done unless it says so, never say something is waiting when it says it is done, and never invent a confirmation code.",
@@ -9940,11 +9976,31 @@ function avoided(path) {
9940
9976
  return path.split(sep).some((segment) => segment.startsWith(".") || NEVER2.has(segment));
9941
9977
  }
9942
9978
 
9943
- // src/commands/agent.ts
9979
+ // src/said.ts
9944
9980
  async function said(response, fallback) {
9945
- const body = await response.json().catch(() => void 0);
9946
- const message2 = typeof body === "object" && body !== null && "error" in body ? body.error?.message : void 0;
9947
- return message2 ?? `${fallback} (${response.status})`;
9981
+ const raw = await response.text().catch(() => "");
9982
+ return reasonFrom(raw, response.status, fallback);
9983
+ }
9984
+ function reasonFrom(raw, status2, fallback) {
9985
+ let body;
9986
+ try {
9987
+ body = JSON.parse(raw);
9988
+ } catch {
9989
+ body = void 0;
9990
+ }
9991
+ if (typeof body?.error === "string") {
9992
+ const description = body.error_description?.trim();
9993
+ return description ? `${description} (${body.error})` : `${body.error} (${status2})`;
9994
+ }
9995
+ const message2 = typeof body?.error === "object" ? body.error?.message?.trim() : void 0;
9996
+ if (message2) return message2;
9997
+ const excerpt = raw.trim().replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").slice(0, 160);
9998
+ return excerpt ? `${fallback} (${status2}): ${excerpt}` : `${fallback} (${status2})`;
9999
+ }
10000
+
10001
+ // src/commands/agent.ts
10002
+ async function said2(response, fallback) {
10003
+ return said(response, fallback);
9948
10004
  }
9949
10005
  function locate(roots, requested) {
9950
10006
  const expanded = requested.startsWith("~") ? resolve3(homedir2(), requested.slice(1).replace(/^[/\\]/, "")) : requested;
@@ -10019,7 +10075,7 @@ async function answer(context, apiUrl, token, roots, request) {
10019
10075
  try {
10020
10076
  fetched = await fetch(request.sourceUrl);
10021
10077
  if (!fetched.ok) {
10022
- return { ok: false, error: await said(fetched, "The file could not be fetched") };
10078
+ return { ok: false, error: await said2(fetched, "The file could not be fetched") };
10023
10079
  }
10024
10080
  downloaded = Buffer.from(await fetched.arrayBuffer());
10025
10081
  } catch {
@@ -10088,7 +10144,7 @@ async function upload(apiUrl, token, filename, bytes) {
10088
10144
  })
10089
10145
  });
10090
10146
  if (!grant.ok) {
10091
- return { ok: false, error: await said(grant, "could not get an upload url") };
10147
+ return { ok: false, error: await said2(grant, "could not get an upload url") };
10092
10148
  }
10093
10149
  const { uploadUrl, contentType, maxBytes } = await grant.json();
10094
10150
  const limit = maxBytes ?? MAX_TRANSFER_BYTES;
@@ -10144,7 +10200,7 @@ async function enableCommand(context) {
10144
10200
  headers: { authorization: `Bearer ${await token()}` }
10145
10201
  });
10146
10202
  if (!listed.ok) {
10147
- context.error(await said(listed, "could not read this account's machines"));
10203
+ context.error(await said2(listed, "could not read this account's machines"));
10148
10204
  return 1;
10149
10205
  }
10150
10206
  const { items } = await listed.json();
@@ -10187,7 +10243,7 @@ Run this on ${machine.hostname} itself, or turn it back on from your dashboard.`
10187
10243
  { method: "POST", headers: { authorization: `Bearer ${await token()}` } }
10188
10244
  );
10189
10245
  if (!enabled.ok) {
10190
- context.error(await said(enabled, "could not turn that machine back on"));
10246
+ context.error(await said2(enabled, "could not turn that machine back on"));
10191
10247
  return 1;
10192
10248
  }
10193
10249
  const result = await enabled.json();
@@ -10200,8 +10256,94 @@ Run this on ${machine.hostname} itself, or turn it back on from your dashboard.`
10200
10256
  );
10201
10257
  return 0;
10202
10258
  }
10259
+ function agentIsRunningAs(pid) {
10260
+ try {
10261
+ process.kill(pid, 0);
10262
+ } catch {
10263
+ return false;
10264
+ }
10265
+ if (process.platform === "win32") return void 0;
10266
+ try {
10267
+ const line2 = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
10268
+ encoding: "utf8",
10269
+ timeout: 5e3
10270
+ });
10271
+ return /\bpm\b|persistmemory|\bagent\b/i.test(line2);
10272
+ } catch {
10273
+ return void 0;
10274
+ }
10275
+ }
10276
+ function processState(pid) {
10277
+ if (process.platform === "win32") return void 0;
10278
+ try {
10279
+ return execFileSync("ps", ["-p", String(pid), "-o", "stat="], {
10280
+ encoding: "utf8",
10281
+ timeout: 5e3
10282
+ }).trim();
10283
+ } catch {
10284
+ return void 0;
10285
+ }
10286
+ }
10287
+ async function stopCommand(context) {
10288
+ const file = context.paths.agentPid;
10289
+ if (!existsSync5(file)) {
10290
+ context.print("No agent is running on this machine.");
10291
+ context.print('If one is running from another home directory, stop it with: pkill -f "pm agent"');
10292
+ return 0;
10293
+ }
10294
+ const pid = Number.parseInt(readFileSync6(file, "utf8").trim(), 10);
10295
+ if (!Number.isInteger(pid) || pid <= 0) {
10296
+ unlinkSync2(file);
10297
+ context.error("The agent's pid file was unreadable, so it has been removed.");
10298
+ context.error('If an agent is still running, stop it with: pkill -f "pm agent"');
10299
+ return 1;
10300
+ }
10301
+ const mine = agentIsRunningAs(pid);
10302
+ if (mine === false) {
10303
+ unlinkSync2(file);
10304
+ context.print("No agent is running. Cleared a stale pid file.");
10305
+ return 0;
10306
+ }
10307
+ if (mine === void 0) {
10308
+ context.error(`Found a pid file naming process ${pid}, but could not confirm it is the agent.`);
10309
+ context.error("Refusing to signal a process this command cannot identify.");
10310
+ context.error(`If that is the agent, stop it with: kill ${pid}`);
10311
+ return 1;
10312
+ }
10313
+ try {
10314
+ process.kill(pid, "SIGCONT");
10315
+ } catch {
10316
+ }
10317
+ process.kill(pid, "SIGTERM");
10318
+ context.print(`Asked the agent (${pid}) to stop. Any request in flight will finish first.`);
10319
+ for (let waited = 0; waited < 1e4; waited += 250) {
10320
+ await new Promise((r) => setTimeout(r, 250));
10321
+ if (agentIsRunningAs(pid) === false) {
10322
+ if (existsSync5(file)) unlinkSync2(file);
10323
+ context.print("Stopped.");
10324
+ return 0;
10325
+ }
10326
+ }
10327
+ if (processState(pid)?.startsWith("T")) {
10328
+ context.error(`The agent (${pid}) is suspended and did not respond to being resumed.`);
10329
+ context.error(`Stop it with: kill -CONT ${pid} && kill -9 ${pid}`);
10330
+ return 1;
10331
+ }
10332
+ context.print("Still finishing a request. It will exit on its own; nothing more to do.");
10333
+ return 0;
10334
+ }
10203
10335
  async function agentCommand(context) {
10204
10336
  if (context.args.words[1] === "enable") return enableCommand(context);
10337
+ if (context.args.words[1] === "stop") return stopCommand(context);
10338
+ const word = context.args.words[1];
10339
+ if (word !== void 0) {
10340
+ context.error(`pm agent: there is no "${word}" subcommand.`);
10341
+ context.error("");
10342
+ context.error(" pm agent start answering requests here");
10343
+ context.error(" pm agent stop stop the agent running here");
10344
+ context.error(" pm agent enable [host] turn a machine back on after it was switched off");
10345
+ return 2;
10346
+ }
10205
10347
  const write3 = (message2, error = false) => {
10206
10348
  const at = (/* @__PURE__ */ new Date()).toISOString();
10207
10349
  for (const line2 of message2.split("\n")) {
@@ -10242,7 +10384,20 @@ async function agentCommand(context) {
10242
10384
  const every = Math.max(2, asked ?? 5) * 1e3;
10243
10385
  write3(`Answering as ${name}, from: ${roots.join(", ")}`);
10244
10386
  write3("Nothing outside those folders can be read. Ctrl-C to stop.");
10387
+ writeFileSync5(context.paths.agentPid, `${process.pid}
10388
+ `, { mode: 384 });
10389
+ const forgetPid = () => {
10390
+ try {
10391
+ if (existsSync5(context.paths.agentPid)) {
10392
+ const held = Number.parseInt(readFileSync6(context.paths.agentPid, "utf8").trim(), 10);
10393
+ if (held === process.pid) unlinkSync2(context.paths.agentPid);
10394
+ }
10395
+ } catch {
10396
+ }
10397
+ };
10398
+ process.on("exit", forgetPid);
10245
10399
  let running = true;
10400
+ let signedOut = false;
10246
10401
  const stop = () => {
10247
10402
  if (!running) return;
10248
10403
  running = false;
@@ -10282,7 +10437,7 @@ async function agentCommand(context) {
10282
10437
  version: VERSION
10283
10438
  });
10284
10439
  if (!beat.ok) {
10285
- complain2(await said(beat, "the service refused this machine"));
10440
+ complain2(await said2(beat, "the service refused this machine"));
10286
10441
  } else {
10287
10442
  const state = await beat.json();
10288
10443
  if (state.status === "disabled") {
@@ -10299,7 +10454,7 @@ async function agentCommand(context) {
10299
10454
  }
10300
10455
  const claimed = await call("claim", { hostname: name, limit: 5 });
10301
10456
  if (!claimed.ok) {
10302
- complain2(await said(claimed, "could not pick up work"));
10457
+ complain2(await said2(claimed, "could not pick up work"));
10303
10458
  } else {
10304
10459
  working();
10305
10460
  const { items } = await claimed.json();
@@ -10313,18 +10468,28 @@ async function agentCommand(context) {
10313
10468
  outcome.ok ? { result: { attachToken: outcome.attachToken } } : { error: outcome.error }
10314
10469
  );
10315
10470
  if (!done.ok) {
10316
- write3(`Stored, but the service did not record it: ${await said(done, "refused")}`, true);
10471
+ write3(`Stored, but the service did not record it: ${await said2(done, "refused")}`, true);
10317
10472
  continue;
10318
10473
  }
10319
10474
  write3(outcome.ok ? `Sent ${outcome.bytes} bytes` : `Refused: ${outcome.error}`);
10320
10475
  }
10321
10476
  }
10322
10477
  } catch (error) {
10323
- complain2(error instanceof Error ? error.message : "connection failed");
10478
+ const message2 = error instanceof Error ? error.message : "connection failed";
10479
+ if (/invalid_grant/i.test(message2)) {
10480
+ write3(`This machine is signed out: ${message2}`, true);
10481
+ write3("Sign in again with: pm auth login", true);
10482
+ write3("The agent is stopping \u2014 retrying cannot renew a revoked token.", true);
10483
+ running = false;
10484
+ signedOut = true;
10485
+ continue;
10486
+ }
10487
+ complain2(message2);
10324
10488
  }
10325
10489
  if (running) await new Promise((r) => setTimeout(r, every));
10326
10490
  }
10327
- return 0;
10491
+ forgetPid();
10492
+ return signedOut ? 1 : 0;
10328
10493
  }
10329
10494
 
10330
10495
  // src/commands/google.ts
@@ -10347,8 +10512,7 @@ async function callApi(context, path, init = {}) {
10347
10512
  });
10348
10513
  }
10349
10514
  async function complain(context, response) {
10350
- const body = await response.json().catch(() => void 0);
10351
- context.error(body?.error?.message ?? `That failed (${response.status}).`);
10515
+ context.error(await said(response, "That failed"));
10352
10516
  return response.status === 409 ? 3 : 1;
10353
10517
  }
10354
10518
  async function driveCommand(context) {
@@ -10549,8 +10713,7 @@ async function collectCommand(context) {
10549
10713
  { headers: { authorization: `Bearer ${credential.token}` } }
10550
10714
  );
10551
10715
  if (!link.ok) {
10552
- const body = await link.json().catch(() => void 0);
10553
- context.error(body?.error?.message ?? `Could not prepare that file (${link.status}).`);
10716
+ context.error(await said(link, "Could not prepare that file"));
10554
10717
  return 1;
10555
10718
  }
10556
10719
  const { downloadUrl, filename } = await link.json();
@@ -10645,8 +10808,7 @@ async function setupCommand(context) {
10645
10808
  }
10646
10809
  async function chooseSpace(context, client, spaces) {
10647
10810
  const { print, error } = context;
10648
- const found = findWorkspace();
10649
- const current = found?.config.space;
10811
+ const current = findWorkspace()?.config.space;
10650
10812
  const named = stringFlag(context.args, "space");
10651
10813
  const creating = stringFlag(context.args, "new-space");
10652
10814
  if (creating !== void 0) {
@@ -10668,48 +10830,46 @@ async function chooseSpace(context, client, spaces) {
10668
10830
  print(` Pass --space "<name>" or --new-space "<name>" to pick one.`);
10669
10831
  return 0;
10670
10832
  }
10671
- if (current) {
10672
- print(` This folder currently uses the Space "${current.name}".`);
10833
+ const live = current ? spaces.find((one) => one.id === current.id) : void 0;
10834
+ if (current && !live) {
10835
+ print(` This folder points at a Space called "${current.name}", which is no longer`);
10836
+ print(" in your account. Choose another one.");
10837
+ print("");
10838
+ } else if (live) {
10839
+ print(` This folder currently uses the Space "${live.name}".`);
10840
+ print("");
10841
+ }
10842
+ if (spaces.length === 0) {
10843
+ print(" You have no Spaces yet. Every memory is filed in one \u2014 a capture with");
10844
+ print(" no Space is refused, and names the Spaces you have \u2014 so this makes the first.");
10673
10845
  print("");
10846
+ return finish(context, await askForNewSpace(context, client), current);
10674
10847
  }
10675
10848
  print(" Which Space should this folder use?");
10676
10849
  print("");
10677
10850
  spaces.forEach((space, index) => {
10678
- const mark = current?.id === space.id ? " (current)" : "";
10851
+ const mark = live?.id === space.id ? " (current)" : "";
10679
10852
  const count = space.memoryCount === void 0 ? "" : `, ${space.memoryCount} memories`;
10680
10853
  print(` ${index + 1}. ${space.name}${mark} ${space.kind}${count}`);
10681
10854
  });
10682
10855
  const createIndex = spaces.length + 1;
10683
- const noneIndex = spaces.length + 2;
10684
10856
  print(` ${createIndex}. Create a new Space`);
10685
- print(` ${noneIndex}. No Space \u2014 use everything in my account`);
10686
10857
  print("");
10687
- const fallback = current ? "keep the current one" : String(createIndex);
10688
- const answer3 = await context.ask(` Choose 1-${noneIndex} [${fallback}]: `);
10858
+ const fallback = live ? "keep the current one" : String(createIndex);
10859
+ const answer3 = await context.ask(` Choose 1-${createIndex} [${fallback}]: `);
10689
10860
  if (answer3 === "") {
10690
- if (current) {
10861
+ if (live) {
10691
10862
  print("");
10692
- print(` Keeping "${current.name}".`);
10863
+ print(` Keeping "${live.name}".`);
10693
10864
  return 0;
10694
10865
  }
10695
10866
  return finish(context, await askForNewSpace(context, client), current);
10696
10867
  }
10697
10868
  const choice = Number(answer3);
10698
- if (!Number.isInteger(choice) || choice < 1 || choice > noneIndex) {
10869
+ if (!Number.isInteger(choice) || choice < 1 || choice > createIndex) {
10699
10870
  error(`"${answer3}" is not one of the choices. Nothing was changed.`);
10700
10871
  return 2;
10701
10872
  }
10702
- if (choice === noneIndex) {
10703
- if (found) {
10704
- writeWorkspace(found.dir, {});
10705
- print("");
10706
- print(` Cleared the Space in ${WORKSPACE_FILE}. This folder now uses everything.`);
10707
- } else {
10708
- print("");
10709
- print(" No Space. This folder uses everything in your account.");
10710
- }
10711
- return 0;
10712
- }
10713
10873
  if (choice === createIndex) {
10714
10874
  return finish(context, await askForNewSpace(context, client), current);
10715
10875
  }
@@ -11003,8 +11163,8 @@ Delete the file it runs from: ${processPath()}`
11003
11163
  }
11004
11164
  const result = run2(manager, ["uninstall", "-g", PACKAGE]);
11005
11165
  if (result.status !== 0) {
11006
- const said2 = npmSaid(result);
11007
- if (said2) context.error(said2);
11166
+ const said3 = npmSaid(result);
11167
+ if (said3) context.error(said3);
11008
11168
  context.error(`npm could not remove ${PACKAGE}.`);
11009
11169
  if (alsoDisconnect) reportDisconnect(context, disconnected, stubborn);
11010
11170
  return result.status ?? 1;
@@ -11040,8 +11200,8 @@ function reportDisconnect(context, disconnected, stubborn) {
11040
11200
  }
11041
11201
  function npmSaid(result) {
11042
11202
  if (result.error) return result.error.message;
11043
- const said2 = [result.stdout, result.stderr].map((stream) => (stream ?? "").trim()).filter((stream) => stream.length > 0).join("\n");
11044
- return said2.length > 0 ? said2 : void 0;
11203
+ const said3 = [result.stdout, result.stderr].map((stream) => (stream ?? "").trim()).filter((stream) => stream.length > 0).join("\n");
11204
+ return said3.length > 0 ? said3 : void 0;
11045
11205
  }
11046
11206
  async function deleteCommand(context) {
11047
11207
  if (!existsSync8(context.paths.dir)) {
@@ -11477,7 +11637,7 @@ function say(context, working, profile, what, surface) {
11477
11637
  what === "changed" ? `No longer working in a Space on ${where}.` : `No Space chosen on ${where}.`
11478
11638
  );
11479
11639
  context.print(
11480
- surface === "email" ? "Mail forwarded to your ingest address is HELD and not filed \u2014 there is no default and nothing picks one. Each held message is kept whole and readable, and forwarding it again once you have chosen is what captures it." : "Nothing `pm remember` sends is kept \u2014 there is no default and nothing picks one. Questions are still answered, from everything."
11640
+ surface === "email" ? "Mail forwarded to your ingest address is HELD and not filed \u2014 there is no default and nothing picks one. Each held message is kept whole and readable, and forwarding it again once you have chosen is what captures it." : "`pm remember` from here is REFUSED, naming the Spaces you have \u2014 it is not silently dropped, and nothing is lost \u2014 there is no default and nothing picks one. Questions are still answered, from everything."
11481
11641
  );
11482
11642
  context.print(
11483
11643
  surface === "email" ? 'Run `pm space --email "Work"` to choose, or `pm spaces create "Work"` first.' : 'Run `pm space "Work"` to choose, or `pm spaces create "Work"` if you have none.'