memgineering 0.25.1 → 0.25.2

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/CHANGELOG.md CHANGED
@@ -11,6 +11,62 @@ language the reader wants. The bilingual rule the monorepo applies to
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.25.2] — 2026-10-05
15
+
16
+ Small fixes found by an independent review of 0.25.0 and 0.25.1. Nothing here
17
+ changes what you type.
18
+
19
+ ### Fixed
20
+
21
+ - **A refresh `setup` keeps your choices.** `memgineering setup` run again
22
+ without `--hook` or `--auto-update` used to turn both back on. It now keeps
23
+ the hook and auto-update choices recorded on this machine, including a hook
24
+ choice made on the `setup --web` screen. On a machine set
25
+ up before this release, the hook choice is read from whether the hook is
26
+ installed. A kept choice is no longer listed as a default nobody was asked
27
+ about.
28
+ - **A command no longer waits on the update check.** Before a background
29
+ install, the CLI asks npm, pnpm and bun where they keep global packages. That
30
+ could hold your command open for up to 15 seconds after its output, and
31
+ longer behind a version manager. The CLI now asks all three at once and gives
32
+ up after 1.5 seconds. If it cannot tell how memgineering was installed, it
33
+ installs nothing and prints the usual "is available — `memgineering update`"
34
+ line.
35
+ - **The before-edit hook never starts an update**, even when it is called with
36
+ a flag first (`memgineering --json guard`). The same holds for `resurface`.
37
+ - **Two commands starting together cannot both start a background install.**
38
+ This could happen when the previous install had been left unfinished. If the
39
+ lock behind this fix is ever left in place, `memgineering update` says so
40
+ (`update_claim_stuck`) instead of reporting another install in progress.
41
+ - **Both sides of a conflict appear in recall.** When a note contradicted an
42
+ unfilled base template, the limit of two template cards could hide one side
43
+ while recall said "Both sides are above". Both sides now show, even if that
44
+ means more than two template cards, and the line counting held-back
45
+ templates says so.
46
+ - **Hosted recall by meaning no longer drops a base note you have filled in.**
47
+ A `USER.md` or other `01_BASE/` note you rewrote in your own words was
48
+ treated as an untouched template and left out. Only an exact, untouched
49
+ template is held back now.
50
+ - **A chat-app connector cannot change a rule by other routes.** Retiring,
51
+ restoring or undoing a rule from a connector is now refused with
52
+ `rule_needs_a_person`, as editing one already was. That includes undoing the
53
+ `remember` that created a rule. A connector's `undo` with no id undoes only
54
+ that app's own changes, including those made before its sign-in was
55
+ refreshed. If it has made none, it is told so and nothing is undone.
56
+ - Hosted `retire` and `unretire` no longer say that `undo` cannot reverse
57
+ them. It can, and their hint now names both ways back.
58
+ - `setup --help` gives ChatGPT its own route: Settings → Developer mode → add
59
+ an MCP server. Claude.ai, Claude Desktop and Cowork use a custom connector.
60
+ - `setup --web` prints its terminal lines in English.
61
+
62
+ ### Agent guidance
63
+
64
+ - `memgineering-setup`: turning auto-update off with
65
+ `setup --agent --auto-update off` no longer needs `--hook`, because the hook
66
+ stays as this machine recorded it. It also explains `update_claim_stuck`.
67
+ - `memgineering-recall`: a template card that is one side of a conflict comes
68
+ on top of the two-card limit.
69
+
14
70
  ## [0.25.1] — 2026-10-05
15
71
 
16
72
  A security release. Upgrade if you use `setup --web`, or a hosted brain with a
@@ -2,7 +2,7 @@
2
2
  name: memgineering
3
3
  description: Use whenever the user refers to something they told you before, asks what was decided, tells you something worth keeping, or settles something that should hold next time. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
4
4
  type: skill
5
- version: 0.25.1
5
+ version: 0.25.2
6
6
  ---
7
7
 
8
8
  # memgineering
@@ -210,8 +210,9 @@ it, so it is evidence of nothing — do not quote it back as what they think. Wh
210
210
  every card carries that mark, recall says so above them (`--json`:
211
211
  `scaffolding: true`), and that is the moment to offer to fill them in rather
212
212
  than answer from them — see `memgineering-writing`. At most two such cards come
213
- back per answer; the rest are counted (`templates_withheld`), never silently
214
- dropped, and nothing anyone wrote is ever held back.
213
+ back per answer, plus any that is one side of a conflict, so both sides of a
214
+ conflict always show; the rest are counted (`templates_withheld`), never
215
+ silently dropped, and nothing anyone wrote is ever held back.
215
216
 
216
217
  The mark disappears once the user writes in the file. A filled-in base note is
217
218
  often the best answer in the brain: _who am I_, _what am I not allowed to do_,
@@ -127,7 +127,8 @@ or npx copy is never touched — it only gets the notice; nor is anything instal
127
127
  from a session hook or under CI). One install runs at a time: `memgineering
128
128
  update` run while a background install is going answers `update_in_progress`
129
129
  and installs nothing — wait a few minutes and run it again; it reports whether
130
- that install worked. `--hook on` means note
130
+ that install worked. `update_claim_stuck` means a crashed install's leftover
131
+ could not be cleared; its hint names the folder to delete. `--hook on` means note
131
132
  summaries and their standing rules reach an agent unasked. Recommend both, but
132
133
  they are theirs to decide.
133
134
 
@@ -513,7 +514,7 @@ answers without changing its name and the index has to be laid down again.
513
514
  ## Turning things off
514
515
 
515
516
  ```
516
- memgineering setup --agent --auto-update off --hook <on|off> # --agent sets both; keep the hook as it was
517
+ memgineering setup --agent --auto-update off # the hook stays as this machine recorded it
517
518
  memgineering unlink # stop reading a brain; notes untouched
518
519
  memgineering unlink --purge # also delete the local index and undo snapshots
519
520
  ```
package/dist/index.js CHANGED
@@ -2441,14 +2441,6 @@ var init_planner = __esm({
2441
2441
  }
2442
2442
  });
2443
2443
 
2444
- // ../../packages/memory-engine/src/recall/semantic.ts
2445
- var init_semantic = __esm({
2446
- "../../packages/memory-engine/src/recall/semantic.ts"() {
2447
- "use strict";
2448
- init_markdown_mapping();
2449
- }
2450
- });
2451
-
2452
2444
  // ../../packages/memory-engine/src/base-templates.ts
2453
2445
  function base(id, title, summary, body) {
2454
2446
  return [
@@ -2556,6 +2548,14 @@ var init_base_templates = __esm({
2556
2548
  }
2557
2549
  });
2558
2550
 
2551
+ // ../../packages/memory-engine/src/recall/semantic.ts
2552
+ var init_semantic = __esm({
2553
+ "../../packages/memory-engine/src/recall/semantic.ts"() {
2554
+ "use strict";
2555
+ init_base_templates();
2556
+ }
2557
+ });
2558
+
2559
2559
  // ../../packages/memory-engine/src/recall/unfilled.ts
2560
2560
  var UNFILLED_FIELDS;
2561
2561
  var init_unfilled = __esm({
@@ -2577,6 +2577,13 @@ var init_unfilled = __esm({
2577
2577
  }
2578
2578
  });
2579
2579
 
2580
+ // ../../packages/memory-engine/src/setup-progress.ts
2581
+ var init_setup_progress = __esm({
2582
+ "../../packages/memory-engine/src/setup-progress.ts"() {
2583
+ "use strict";
2584
+ }
2585
+ });
2586
+
2580
2587
  // ../../packages/memory-engine/src/recall/query-prefix.ts
2581
2588
  var init_query_prefix = __esm({
2582
2589
  "../../packages/memory-engine/src/recall/query-prefix.ts"() {
@@ -4905,6 +4912,7 @@ var init_src = __esm({
4905
4912
  init_planner();
4906
4913
  init_semantic();
4907
4914
  init_unfilled();
4915
+ init_setup_progress();
4908
4916
  init_query_prefix();
4909
4917
  init_base_templates();
4910
4918
  init_scope_spread();
@@ -5507,6 +5515,13 @@ var init_config = __esm({
5507
5515
  * answer as "older than this CLI" and wants the same one command.
5508
5516
  */
5509
5517
  guidanceVersion: z8.string().optional(),
5518
+ /**
5519
+ * The person's answer to "arm the hooks?", as they gave it — not whether
5520
+ * this machine's tools could take one. A flag-less `setup` reads it back,
5521
+ * so a refresh keeps what they chose instead of setup's default (0.25.2).
5522
+ * Absent on a machine set up before it was recorded.
5523
+ */
5524
+ hook: z8.enum(["on", "off"]).optional().catch(void 0),
5510
5525
  /**
5511
5526
  * Notes `setup --web` was asked to upload, not yet sent.
5512
5527
  *
@@ -7864,13 +7879,208 @@ var init_setup_signin = __esm({
7864
7879
  }
7865
7880
  });
7866
7881
 
7882
+ // src/lib/next-step.ts
7883
+ import { mkdir as mkdir15, readFile as readFile24, writeFile as writeFile13 } from "fs/promises";
7884
+ import { join as join23 } from "path";
7885
+ function isSetUp(cfg) {
7886
+ return Object.keys(cfg.system.installed).length > 0;
7887
+ }
7888
+ function connectedMarker(at) {
7889
+ return join23(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
7890
+ }
7891
+ async function connectedNoticeShown(at) {
7892
+ return readFile24(connectedMarker(at), "utf8").then(
7893
+ () => true,
7894
+ () => false
7895
+ );
7896
+ }
7897
+ async function markConnectedNoticeShown(at) {
7898
+ try {
7899
+ await mkdir15(brandHome(), { recursive: true });
7900
+ await writeFile13(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
7901
+ `);
7902
+ } catch {
7903
+ }
7904
+ }
7905
+ async function claimFullSetupNotice() {
7906
+ const marker = join23(brandHome(), "setup-notice-shown");
7907
+ try {
7908
+ await readFile24(marker, "utf8");
7909
+ return false;
7910
+ } catch {
7911
+ }
7912
+ try {
7913
+ await mkdir15(brandHome(), { recursive: true });
7914
+ await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
7915
+ `);
7916
+ } catch {
7917
+ }
7918
+ return true;
7919
+ }
7920
+ function shellQuote(value) {
7921
+ return `'${value.replace(/'/g, `'\\''`)}'`;
7922
+ }
7923
+ function confirmUploadNotice(p) {
7924
+ const who = p.account_email ?? "the account `setup --web` connected";
7925
+ const brain = p.brain_id && UUID.test(p.brain_id) ? ` --brain ${p.brain_id}` : "";
7926
+ return `## Notes are waiting to be uploaded \u2014 after a check.
7927
+
7928
+ \`memgineering setup --web\` chose to upload \`${p.path}\` to a brain on **${who}**.
7929
+ This machine has not sent them: an upload decided on the setup screen waits
7930
+ until somebody here has seen which account would receive the notes.
7931
+
7932
+ If that is your user's account, this uploads them:
7933
+
7934
+ \`\`\`bash
7935
+ memgineering push --vault ${shellQuote(p.path)}${brain}
7936
+ \`\`\`
7937
+
7938
+ If it is not, \`memgineering logout\` disconnects this machine from that account
7939
+ and nothing is uploaded. If you are not sure which it is, ask them.`;
7940
+ }
7941
+ function setupConnectedNotice(c2) {
7942
+ const who = c2.email ?? "an account with no email on record";
7943
+ return `## This machine is signed in as **${who}**.
7944
+
7945
+ \`memgineering setup --web\` connected it to that account. If that is not your user's account,
7946
+ \`memgineering logout\` disconnects it, and nothing they write reaches that brain.`;
7947
+ }
7948
+ function nextStepFor(cfg, signedIn = false, seen = {}) {
7949
+ if (cfg.system.pendingUpload) {
7950
+ return { id: "confirm_upload", markdown: confirmUploadNotice(cfg.system.pendingUpload) };
7951
+ }
7952
+ if (cfg.system.setupConnected && !seen.connected) {
7953
+ return { id: "setup_connected", markdown: setupConnectedNotice(cfg.system.setupConnected) };
7954
+ }
7955
+ if (signedIn && cfg.brain.cloud === void 0) {
7956
+ return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
7957
+ }
7958
+ if (isSetUp(cfg) && cfg.system.guidanceVersion !== VERSION) {
7959
+ return { id: "refresh_guidance", markdown: REFRESH_GUIDANCE };
7960
+ }
7961
+ if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
7962
+ if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
7963
+ if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
7964
+ return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
7965
+ }
7966
+ return null;
7967
+ }
7968
+ async function currentNextStep() {
7969
+ try {
7970
+ const stored = await readCredentials().catch(() => null);
7971
+ const cfg = await loadConfig();
7972
+ const connected = cfg.system.setupConnected ? await connectedNoticeShown(cfg.system.setupConnected.at) : false;
7973
+ return nextStepFor(cfg, stored !== null && stored.api_url === apiUrl(), { connected });
7974
+ } catch {
7975
+ return null;
7976
+ }
7977
+ }
7978
+ var FINISH_SETUP, SETUP_NUDGE, REFRESH_GUIDANCE, CONNECT_A_BRAIN, POINT_AT_A_BRAIN, UUID;
7979
+ var init_next_step = __esm({
7980
+ "src/lib/next-step.ts"() {
7981
+ "use strict";
7982
+ init_brand();
7983
+ init_api_client();
7984
+ init_credentials();
7985
+ init_config();
7986
+ init_version();
7987
+ FINISH_SETUP = `## memgineering is installed, but not set up yet.
7988
+
7989
+ Nothing is connected: there is no brain to read, and the agent tools on this
7990
+ machine have no memgineering guidance yet. Setup is one command:
7991
+
7992
+ \`\`\`bash
7993
+ memgineering setup --web
7994
+ \`\`\`
7995
+
7996
+ It does NOT wait for them. It returns in a few seconds \u2014 measured \u2014 and leaves
7997
+ a setup screen open in their browser at memgineering.com, where they choose
7998
+ everything: signing in, where the memory lives, which notes. Nothing is
7999
+ installed until they finish there, and this machine picks it up on its own.
8000
+
8001
+ If they have not already asked you to set it up, ask them first \u2014 it opens a
8002
+ browser window on their computer. Once you run it, tell them in their language
8003
+ that the setup screen is open. It prints a short code first: show it to them,
8004
+ because the screen asks them to compare it.
8005
+
8006
+ No browser on this machine (a server, a container, a sandbox)? Run
8007
+ \`memgineering setup --web --print-url\` and give them the link to open on the
8008
+ computer they are actually sitting at.
8009
+
8010
+ \`setup --agent\` answers every one of those questions on their behalf, and
8011
+ \`--no-login\` answers the biggest one \u2014 no account, so the memory cannot follow
8012
+ them to another machine. Use it only when they have said they would rather you
8013
+ decided, or when this machine cannot reach a browser and \`--print-url\` is no
8014
+ use to them either.
8015
+
8016
+ Setup's output then says how to connect their brain \u2014 create one, pick one, or
8017
+ move existing notes up.
8018
+
8019
+ The \`memgineering-setup\` skill covers what comes after.`;
8020
+ SETUP_NUDGE = "memgineering is installed, but not set up yet. `memgineering setup --web` opens a setup screen in their browser and does NOT wait for them \u2014 ask them first unless they asked you to set it up.\nThe full notes: run `memgineering` on its own.";
8021
+ REFRESH_GUIDANCE = `## The guidance in your agent tools is older than this memgineering.
8022
+
8023
+ The hub and skills this CLI installs into your tool's home were written by an
8024
+ earlier release. One command rewrites them:
8025
+
8026
+ \`\`\`bash
8027
+ memgineering setup
8028
+ \`\`\`
8029
+
8030
+ It does not change which brain this machine reads, or any note. Two things it
8031
+ does change, both worth knowing before it runs:
8032
+
8033
+ - A setup-owned file your user edited is overwritten; setup names it afterwards.
8034
+ - Run with no flags where there is no terminal (an agent's shell), it keeps the
8035
+ auto-update and hook choices recorded on this machine, and installs into
8036
+ every agent tool it finds \u2014 \`--tools\` naming every tool they use keeps it to
8037
+ those (a tool left out keeps its old guidance, and this notice stops
8038
+ appearing anyway). On a machine not signed in, it starts a sign-in and prints
8039
+ a code; \`--no-login\` declines that for a machine they keep local.
8040
+
8041
+ If you are not sure what they chose, ask them. If you run it,
8042
+ mention it to your user in one line: it is upkeep for a tool they already set
8043
+ up, and they should not have to discover it later.
8044
+
8045
+ Guidance files are read at the start of a session, so what changes today
8046
+ arrives in the next one.`;
8047
+ CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
8048
+
8049
+ A brain is a folder of markdown notes. Until one is connected there is nothing
8050
+ to recall and nowhere to remember. Which folder is your user's choice; the
8051
+ command is one you can run for them:
8052
+
8053
+ \`\`\`bash
8054
+ memgineering link <their-notes-folder> # they already keep notes
8055
+ memgineering init <path> # they do not \u2014 creates one, laid out
8056
+ \`\`\`
8057
+
8058
+ \`link\` reports what would be stored before anything is indexed. An existing
8059
+ folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
8060
+
8061
+ Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
8062
+ it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
8063
+
8064
+ Read \`memgineering-setup\` for accounts and which of their tools this reaches.
8065
+
8066
+ Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
8067
+ POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
8068
+
8069
+ \`memgineering brains\` lists what is on the account.
8070
+ \`memgineering use --brain <name>\` points this machine at one.
8071
+ \`memgineering create <name>\` makes one and points here, when the account has none yet.
8072
+ `;
8073
+ UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8074
+ }
8075
+ });
8076
+
7867
8077
  // src/lib/provenance.ts
7868
- import { execFile } from "child_process";
8078
+ import { execFile, spawn as spawn2 } from "child_process";
7869
8079
  import { realpath as realpath3 } from "fs/promises";
7870
- import { dirname as dirname8, join as join23, resolve as resolve14 } from "path";
8080
+ import { dirname as dirname8, join as join24, resolve as resolve14 } from "path";
7871
8081
  import { fileURLToPath as fileURLToPath2 } from "url";
7872
8082
  import { promisify } from "util";
7873
- async function detectProvenance(selfPath) {
8083
+ async function detectProvenance(selfPath, opts = {}) {
7874
8084
  const entry = selfPath ?? fileURLToPath2(import.meta.url);
7875
8085
  const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
7876
8086
  if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
@@ -7882,9 +8092,11 @@ async function detectProvenance(selfPath) {
7882
8092
  reason: "this is a one-off npx run, not an installed copy"
7883
8093
  };
7884
8094
  }
7885
- for (const manager of MANAGERS) {
7886
- const prefix = await prefixOf(manager.bin, manager.prefixArgs);
7887
- if (prefix === null) continue;
8095
+ const deadline = opts.deadlineMs;
8096
+ const answers = deadline === void 0 ? null : await probeAllWithin(MANAGERS.map((m) => [m.bin, m.prefixArgs]), deadline);
8097
+ for (const [i, manager] of MANAGERS.entries()) {
8098
+ const prefix = answers === null ? await prefixOf(manager.bin, manager.prefixArgs) : answers.prefixes[i];
8099
+ if (prefix === null || prefix === void 0) continue;
7888
8100
  if (isInside2(selfDir, manager.moduleRoot(prefix))) {
7889
8101
  return {
7890
8102
  kind: manager.kind,
@@ -7895,6 +8107,15 @@ async function detectProvenance(selfPath) {
7895
8107
  };
7896
8108
  }
7897
8109
  }
8110
+ if (answers?.timedOut === true) {
8111
+ return {
8112
+ kind: "undetermined",
8113
+ selfDir,
8114
+ prefix: null,
8115
+ command: "npm i -g memgineering@latest",
8116
+ reason: "the package managers did not answer in time to tell which one installed this copy"
8117
+ };
8118
+ }
7898
8119
  const local = selfDir.includes(`${"node_modules"}`);
7899
8120
  return {
7900
8121
  kind: local ? "local" : "unknown",
@@ -7907,6 +8128,62 @@ async function detectProvenance(selfPath) {
7907
8128
  function canSelfUpdate(p) {
7908
8129
  return p.kind === "npm" || p.kind === "pnpm" || p.kind === "bun";
7909
8130
  }
8131
+ async function probeAllWithin(probes, deadlineMs) {
8132
+ const controller = new AbortController();
8133
+ const timer = setTimeout(() => controller.abort(), deadlineMs);
8134
+ timer.unref();
8135
+ try {
8136
+ const prefixes = await Promise.all(
8137
+ probes.map(([bin, args]) => probeOnce(bin, args, controller.signal))
8138
+ );
8139
+ return { prefixes, timedOut: controller.signal.aborted };
8140
+ } finally {
8141
+ clearTimeout(timer);
8142
+ }
8143
+ }
8144
+ function probeOnce(bin, args, signal) {
8145
+ return new Promise((settle) => {
8146
+ let child;
8147
+ try {
8148
+ child = spawn2(bin, args, { detached: true, stdio: ["ignore", "pipe", "ignore"] });
8149
+ } catch {
8150
+ settle(null);
8151
+ return;
8152
+ }
8153
+ let out = "";
8154
+ let done = false;
8155
+ const finish = (value) => {
8156
+ if (done) return;
8157
+ done = true;
8158
+ signal.removeEventListener("abort", stop);
8159
+ settle(value);
8160
+ };
8161
+ function stop() {
8162
+ try {
8163
+ if (process.platform === "win32" || child.pid === void 0) child.kill("SIGKILL");
8164
+ else process.kill(-child.pid, "SIGKILL");
8165
+ } catch {
8166
+ }
8167
+ child.stdout?.destroy();
8168
+ child.unref();
8169
+ finish(null);
8170
+ }
8171
+ child.stdout?.on("data", (chunk) => {
8172
+ out += chunk.toString("utf8");
8173
+ });
8174
+ child.on("error", () => finish(null));
8175
+ child.on("close", (code) => {
8176
+ const value = out.trim();
8177
+ if (code !== 0 || value === "") {
8178
+ finish(null);
8179
+ return;
8180
+ }
8181
+ void realpath3(value).catch(() => value).then(finish);
8182
+ });
8183
+ if (signal.aborted) stop();
8184
+ else signal.addEventListener("abort", stop, { once: true });
8185
+ });
8186
+ }
7910
8187
  async function prefixOf(bin, args) {
7911
8188
  try {
7912
8189
  const { stdout } = await run(bin, args, { timeout: 5e3 });
@@ -7928,7 +8205,7 @@ async function unreachableByName(selfPath) {
7928
8205
  const p = await detectProvenance(entry);
7929
8206
  return p.kind === "local" || p.kind === "npx" ? p.reason : null;
7930
8207
  }
7931
- var run, MANAGERS;
8208
+ var run, MANAGERS, BACKGROUND_PROBE_DEADLINE_MS;
7932
8209
  var init_provenance = __esm({
7933
8210
  "src/lib/provenance.ts"() {
7934
8211
  "use strict";
@@ -7938,7 +8215,7 @@ var init_provenance = __esm({
7938
8215
  kind: "npm",
7939
8216
  bin: "npm",
7940
8217
  prefixArgs: ["prefix", "-g"],
7941
- moduleRoot: (prefix) => process.platform === "win32" ? join23(prefix, "node_modules") : join23(prefix, "lib", "node_modules"),
8218
+ moduleRoot: (prefix) => process.platform === "win32" ? join24(prefix, "node_modules") : join24(prefix, "lib", "node_modules"),
7942
8219
  install: "npm i -g memgineering@latest"
7943
8220
  },
7944
8221
  {
@@ -7957,6 +8234,7 @@ var init_provenance = __esm({
7957
8234
  install: "bun add -g memgineering"
7958
8235
  }
7959
8236
  ];
8237
+ BACKGROUND_PROBE_DEADLINE_MS = 1500;
7960
8238
  }
7961
8239
  });
7962
8240
 
@@ -7972,24 +8250,29 @@ __export(setup_cloud_exports, {
7972
8250
  runSetupCloudChild: () => runSetupCloudChild,
7973
8251
  waitAndApply: () => waitAndApply
7974
8252
  });
7975
- import { spawn as spawn2 } from "child_process";
8253
+ import { spawn as spawn3 } from "child_process";
7976
8254
  import { realpathSync as realpathSync2 } from "fs";
7977
8255
  import { readdir as readdir7, stat as stat6 } from "fs/promises";
7978
8256
  import { homedir as homedir5 } from "os";
7979
- import { basename as basename3, dirname as dirname9, join as join24, resolve as resolve15, sep as sep4 } from "path";
7980
- async function report2(setupToken, state, detail) {
8257
+ import { basename as basename3, dirname as dirname9, join as join25, resolve as resolve15, sep as sep4 } from "path";
8258
+ async function report2(setupToken, state, detail, code) {
7981
8259
  try {
7982
8260
  await apiRequest({
7983
8261
  method: "POST",
7984
8262
  path: "/v1/setup/session/report",
7985
- body: { setup_token: setupToken, state, ...detail === void 0 ? {} : { detail } }
8263
+ body: {
8264
+ setup_token: setupToken,
8265
+ state,
8266
+ ...detail === void 0 ? {} : { detail },
8267
+ ...code === void 0 ? {} : { code }
8268
+ }
7986
8269
  });
7987
8270
  } catch {
7988
8271
  }
7989
8272
  }
7990
8273
  async function folderCandidates() {
7991
8274
  const home = homedir5();
7992
- const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join24(home, d));
8275
+ const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join25(home, d));
7993
8276
  const found = [];
7994
8277
  const countNotes = async (dir) => {
7995
8278
  let count = 0;
@@ -7998,7 +8281,7 @@ async function folderCandidates() {
7998
8281
  if (entry.name.startsWith(".")) continue;
7999
8282
  if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
8000
8283
  else if (entry.isDirectory()) {
8001
- const inner = await readdir7(join24(dir, entry.name)).catch(() => []);
8284
+ const inner = await readdir7(join25(dir, entry.name)).catch(() => []);
8002
8285
  count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
8003
8286
  }
8004
8287
  if (count > 999) break;
@@ -8058,17 +8341,24 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
8058
8341
  ...cfg,
8059
8342
  system: {
8060
8343
  ...cfg.system,
8061
- setupConnected: { account_id: account.id, email: account.email, at: (/* @__PURE__ */ new Date()).toISOString() }
8344
+ setupConnected: {
8345
+ account_id: account.id,
8346
+ email: account.email,
8347
+ at: (/* @__PURE__ */ new Date()).toISOString()
8348
+ }
8062
8349
  }
8063
8350
  });
8064
8351
  }
8065
8352
  }
8066
- await report2(setupToken, "applying", "\uC124\uC815\uC744 \uC4F0\uB294 \uC911\uC774\uC5D0\uC694.");
8353
+ await report2(setupToken, "applying", "\uC124\uC815\uC744 \uC4F0\uB294 \uC911\uC774\uC5D0\uC694.", "writing_config");
8067
8354
  const choice = {
8068
8355
  tools: toolsFromAnswers(answers, detected),
8069
8356
  autoUpdate: answers.auto_update ? "on" : "off",
8070
8357
  errorTelemetry: answers.error_telemetry === null ? "undecided" : answers.error_telemetry ? "on" : "off",
8071
- hook: answers.hook
8358
+ hook: answers.hook,
8359
+ // The screen always shows the hook switch, so this is the person's answer,
8360
+ // and a later flag-less refresh keeps it only if it is recorded as one.
8361
+ hookAnswer: answers.hook ? "on" : "off"
8072
8362
  };
8073
8363
  await applySetup(choice, false);
8074
8364
  const parts = [];
@@ -8096,7 +8386,7 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
8096
8386
  const email = account?.email ?? null;
8097
8387
  const yes = guards.confirmUpload ? await guards.confirmUpload({ path, email }) : null;
8098
8388
  if (yes === true) {
8099
- await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.");
8389
+ await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.", "uploading");
8100
8390
  const { pushCommand: pushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
8101
8391
  await pushCommand2().parseAsync(
8102
8392
  ["--vault", path, ...claim.brain?.id ? ["--brain", claim.brain.id] : []],
@@ -8119,7 +8409,12 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
8119
8409
  }
8120
8410
  }
8121
8411
  });
8122
- await report2(setupToken, "applying", "\uB178\uD2B8 \uC62C\uB9AC\uAE30\uB294 \uC774 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uD655\uC778\uD55C \uB4A4\uC5D0 \uC2DC\uC791\uB3FC\uC694.");
8412
+ await report2(
8413
+ setupToken,
8414
+ "applying",
8415
+ "\uB178\uD2B8 \uC62C\uB9AC\uAE30\uB294 \uC774 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uD655\uC778\uD55C \uB4A4\uC5D0 \uC2DC\uC791\uB3FC\uC694.",
8416
+ "upload_waits_for_check"
8417
+ );
8123
8418
  parts.push(`upload of ${path} waits for a check on this machine`);
8124
8419
  } else {
8125
8420
  parts.push(`not uploaded: ${path}`);
@@ -8189,11 +8484,12 @@ async function waitAndApply(setupToken, opts) {
8189
8484
  candidates: offered,
8190
8485
  confirmUpload: opts.confirmUpload ?? null
8191
8486
  });
8192
- await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.");
8487
+ await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.", "done");
8193
8488
  return done;
8194
8489
  } catch (err) {
8195
8490
  const message = err instanceof Error ? err.message : String(err);
8196
- await report2(setupToken, "failed", message.slice(0, 400));
8491
+ const code = err.code === "conflict" ? "different_account" : "failed";
8492
+ await report2(setupToken, "failed", message.slice(0, 400), code);
8197
8493
  throw err;
8198
8494
  }
8199
8495
  }
@@ -8205,7 +8501,7 @@ async function waitAndApply(setupToken, opts) {
8205
8501
  }).catch(() => null);
8206
8502
  if (state?.body?.folders_requested === true) {
8207
8503
  sentFolders = true;
8208
- opts.onProgress?.("\uD3F4\uB354\uB97C \uCC3E\uC544\uBCF4\uB294 \uC911\u2026");
8504
+ opts.onProgress?.("Looking for note folders on this machine\u2026");
8209
8505
  const candidates = await folderCandidates();
8210
8506
  offered = candidates.map((c2) => c2.path);
8211
8507
  await apiRequest({
@@ -8236,7 +8532,7 @@ async function startSession(detected) {
8236
8532
  return body;
8237
8533
  }
8238
8534
  function detachChild(setupToken) {
8239
- const child = spawn2(process.execPath, [process.argv[1] ?? "", "setup", "--web"], {
8535
+ const child = spawn3(process.execPath, [process.argv[1] ?? "", "setup", "--web"], {
8240
8536
  detached: true,
8241
8537
  stdio: ["pipe", "ignore", "ignore"],
8242
8538
  env: { ...process.env, [SETUP_CHILD_ENV]: "1" }
@@ -8255,7 +8551,7 @@ async function runSetupCloud(opts) {
8255
8551
  const session = await startSession(opts.detected);
8256
8552
  const intervalMs = Math.max(1, session.interval ?? 5) * 1e3;
8257
8553
  printHuman(`
8258
- \uC124\uCE58 \uCF54\uB4DC: ${c.bold(session.user_code)}
8554
+ Setup code: ${c.bold(session.user_code)}
8259
8555
  `);
8260
8556
  const opened = opts.printUrl ? false : await openBrowser(session.setup_url);
8261
8557
  const foreground = opts.wait || process.stdout.isTTY === true && process.stderr.isTTY === true;
@@ -8271,14 +8567,14 @@ async function runSetupCloud(opts) {
8271
8567
  waiting: foreground
8272
8568
  },
8273
8569
  human: () => {
8274
- if (opened) printHuman("\uBE0C\uB77C\uC6B0\uC800\uC5D0 \uC124\uCE58 \uD654\uBA74\uC744 \uC5F4\uC5C8\uC5B4\uC694.");
8570
+ if (opened) printHuman("Opened the setup screen in the browser.");
8275
8571
  else {
8276
- printHuman("\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC774 \uC8FC\uC18C\uB97C \uC5F4\uC5B4 \uC124\uCE58\uB97C \uB9C8\uCCD0 \uC8FC\uC138\uC694:\n");
8572
+ printHuman("Open this link in a browser to finish the setup:\n");
8277
8573
  printHuman(` ${session.setup_url}`);
8278
8574
  }
8279
8575
  printHuman(
8280
8576
  c.gray(
8281
- foreground ? "\n \uD654\uBA74\uC5D0\uC11C \uB9C8\uCE58\uBA74 \uC5EC\uAE30\uC11C \uC774\uC5B4\uC11C \uC124\uCE58\uD560\uAC8C\uC694. \uAE30\uB2E4\uB9AC\uB294 \uAC78 \uBA48\uCD94\uB824\uBA74 Ctrl-C." : "\n \uD654\uBA74\uC5D0\uC11C \uB9C8\uCE58\uBA74 \uC774 \uCEF4\uD4E8\uD130\uC5D0 \uC790\uB3D9\uC73C\uB85C \uC124\uCE58\uB3FC\uC694. \uCC3D\uC744 \uB2EB\uC544\uB3C4 \uAD1C\uCC2E\uC544\uC694."
8577
+ foreground ? "\n When the screen is done, the install continues here. Ctrl-C stops waiting." : "\n When the screen is done, this machine installs on its own. This window can be closed."
8282
8578
  )
8283
8579
  );
8284
8580
  }
@@ -8296,7 +8592,7 @@ async function runSetupCloud(opts) {
8296
8592
  confirmUpload: interactive ? askBeforeUpload : null
8297
8593
  });
8298
8594
  if (outcome === "expired") {
8299
- const line = "\uC124\uCE58 \uD654\uBA74\uC774 \uB9CC\uB8CC\uB410\uC5B4\uC694. \uC544\uBB34\uAC83\uB3C4 \uBC14\uB00C\uC9C0 \uC54A\uC558\uC5B4\uC694 \u2014 `memgineering setup --web`\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uC0C8 \uD654\uBA74\uC774 \uC5F4\uB824\uC694.";
8595
+ const line = "The setup screen expired and nothing was changed. `memgineering setup --web` opens a new one.";
8300
8596
  if (isJsonMode()) notice(JSON.stringify({ setup_web: "expired" }));
8301
8597
  else printHuman(c.gray(`
8302
8598
  ${line}`));
@@ -8304,10 +8600,10 @@ ${line}`));
8304
8600
  }
8305
8601
  if (isJsonMode()) notice(JSON.stringify({ setup_web: "applied" }));
8306
8602
  else {
8307
- printHuman(c.green("\n\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694."));
8603
+ printHuman(c.green("\nSetup finished."));
8308
8604
  printHuman(
8309
8605
  c.gray(
8310
- " \uC9C0\uAE08 \uBC14\uB85C \uC4F8 \uC218 \uC788\uC5B4\uC694 \u2014 recall\uB3C4 remember\uB3C4 \uB429\uB2C8\uB2E4.\n \uC0C8\uB85C \uC124\uCE58\uB41C \uC548\uB0B4\uBB38\uC740 \uB2E4\uC74C \uB300\uD654\uBD80\uD130 \uD568\uAED8 \uC2E4\uB824\uC694."
8606
+ " recall and remember work now.\n The newly installed guidance is read from the next conversation on."
8311
8607
  )
8312
8608
  );
8313
8609
  }
@@ -8342,7 +8638,7 @@ async function askBeforeUpload(q) {
8342
8638
  const { yes } = await prompts6({
8343
8639
  type: "confirm",
8344
8640
  name: "yes",
8345
- message: `${q.path} \uC758 \uB178\uD2B8\uB97C ${q.email ?? "\uC774 \uACC4\uC815"} \uBE0C\uB808\uC778\uC73C\uB85C \uC62C\uB9B4\uAE4C\uC694?`,
8641
+ message: `Upload the notes in ${q.path} to the brain on ${q.email ?? "this account"}?`,
8346
8642
  initial: false
8347
8643
  });
8348
8644
  return yes === true;
@@ -8377,8 +8673,8 @@ var init_setup_cloud = __esm({
8377
8673
 
8378
8674
  // src/commands/setup.ts
8379
8675
  import { createHash as createHash9 } from "crypto";
8380
- import { access as access2, mkdir as mkdir15, readFile as readFile24 } from "fs/promises";
8381
- import { dirname as dirname10, join as join25 } from "path";
8676
+ import { access as access2, mkdir as mkdir16, readFile as readFile25 } from "fs/promises";
8677
+ import { dirname as dirname10, join as join26 } from "path";
8382
8678
  import { fileURLToPath as fileURLToPath3 } from "url";
8383
8679
  import { Command as Command40 } from "commander";
8384
8680
  import prompts5 from "prompts";
@@ -8403,10 +8699,11 @@ function setupCommand() {
8403
8699
  "after",
8404
8700
  [
8405
8701
  "",
8406
- "Chat apps with no shell \u2014 ChatGPT, Claude.ai, Claude Desktop, Cowork \u2014 need no",
8407
- `setup: add ${HOSTED_CONNECTOR_URL} as a custom connector in the app and`,
8408
- "sign in. It reads and writes the brain on your account (not a folder on this",
8409
- "machine)."
8702
+ "Chat apps with no shell need no setup. They read and write the brain on your",
8703
+ `account (not a folder on this machine) at ${HOSTED_CONNECTOR_URL}:`,
8704
+ " ChatGPT: Settings \u2192 Developer mode \u2192 add an MCP server with that URL, sign in.",
8705
+ " Claude.ai, Claude Desktop, Cowork: Customize \u2192 Connectors \u2192 Add custom",
8706
+ " connector with that URL, sign in."
8410
8707
  ].join("\n")
8411
8708
  ).action(async (opts) => {
8412
8709
  const detected = await detectTools();
@@ -8441,16 +8738,18 @@ function setupCommand() {
8441
8738
  dryRun: opts.dryRun === true
8442
8739
  });
8443
8740
  const mode = await resolveMode(opts);
8444
- const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts);
8741
+ const recorded = mode.kind === "human" ? {} : await recordedChoices(detected);
8742
+ const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts, recorded);
8445
8743
  const defaultsTaken = !mode.chosen ? [
8446
- ...opts.autoUpdate === void 0 ? ["auto-update: on"] : [],
8744
+ // Only what nobody chose, here or on an earlier run.
8745
+ ...opts.autoUpdate === void 0 && recorded.autoUpdate === void 0 ? ["auto-update: on"] : [],
8447
8746
  // Every callback this flag arms, named — on Claude Code and Codex
8448
8747
  // that is two (note summaries at session start, the user's own
8449
8748
  // rules before an edit), and saying only the first would be
8450
8749
  // reporting half of what was decided on their behalf. On Grok it
8451
8750
  // is one, at the end of a turn, and naming the other two there
8452
8751
  // reported callbacks that were never installed.
8453
- ...opts.hook === void 0 && choice?.hook ? hooksFor(choice.tools).map((h) => `${h.what}: on`) : []
8752
+ ...opts.hook === void 0 && recorded.hook === void 0 && choice?.hook ? hooksFor(choice.tools).map((h) => `${h.what}: on`) : []
8454
8753
  ] : [];
8455
8754
  if (choice === null) {
8456
8755
  printHuman(c.yellow("Cancelled \u2014 nothing was changed."));
@@ -8716,6 +9015,7 @@ async function applySetup(choice, dryRun) {
8716
9015
  system: {
8717
9016
  ...cfg.system,
8718
9017
  autoUpdate: choice.autoUpdate,
9018
+ ...choice.hookAnswer === void 0 ? {} : { hook: choice.hookAnswer },
8719
9019
  installed,
8720
9020
  // Stamped whether or not anything was rewritten. A run that finds every
8721
9021
  // file already current is exactly the run that must clear the notice —
@@ -8741,7 +9041,7 @@ async function applySetup(choice, dryRun) {
8741
9041
  }
8742
9042
  async function write2() {
8743
9043
  for (const tool of choice.tools) {
8744
- const hubPath = join25(agentHome(), tool.hubFile);
9044
+ const hubPath = join26(agentHome(), tool.hubFile);
8745
9045
  const written = await injectBlock(
8746
9046
  hubPath,
8747
9047
  hub + tailFor(tool, choice.hook),
@@ -8756,8 +9056,8 @@ async function applySetup(choice, dryRun) {
8756
9056
  });
8757
9057
  if (!dryRun && written.after !== null) mark(hubPath, written.after);
8758
9058
  for (const skill of BRAND.subSkills) {
8759
- const content = await loadAsset(join25(skill, "SKILL.md"));
8760
- const path = join25(agentHome(), tool.skillsDir, skill, "SKILL.md");
9059
+ const content = await loadAsset(join26(skill, "SKILL.md"));
9060
+ const path = join26(agentHome(), tool.skillsDir, skill, "SKILL.md");
8761
9061
  const out = await writeIfDifferent(path, content, dryRun);
8762
9062
  results.push({
8763
9063
  tool: tool.label,
@@ -8830,6 +9130,23 @@ function globalInstallHint(reason, hooks) {
8830
9130
  return `${hooks ? "The hooks and the guide call" : "The guide tells your agent to run"} \`memgineering\` by name, and ${reason} \u2014
8831
9131
  so nothing reaches it until a global install exists: \`npm i -g memgineering\`. Nothing needs re-running after that.`;
8832
9132
  }
9133
+ async function recordedChoices(detected) {
9134
+ const cfg = await loadConfig();
9135
+ if (!isSetUp(cfg)) return {};
9136
+ const hook = cfg.system.hook ?? await hooksArmed(detected);
9137
+ return { autoUpdate: cfg.system.autoUpdate, ...hook === void 0 ? {} : { hook } };
9138
+ }
9139
+ async function hooksArmed(tools) {
9140
+ const capable = tools.filter((t) => t.hooks && t.hooksFile);
9141
+ if (capable.length === 0) return void 0;
9142
+ for (const tool of capable) {
9143
+ const raw = await readFile25(join26(agentHome(), ...(tool.hooksFile ?? "").split("/")), "utf8").catch(
9144
+ () => ""
9145
+ );
9146
+ if (hooksFor([tool]).some((spec) => raw.includes(spec.signature))) return "on";
9147
+ }
9148
+ return "off";
9149
+ }
8833
9150
  async function resolveMode(opts) {
8834
9151
  if (opts.agent) return { kind: "agent", chosen: true };
8835
9152
  if (opts.human) {
@@ -8863,11 +9180,11 @@ async function detectTools() {
8863
9180
  const home = agentHome();
8864
9181
  const found = [];
8865
9182
  for (const tool of TOOLS) {
8866
- if (await exists2(join25(home, tool.dir))) found.push(tool);
9183
+ if (await exists2(join26(home, tool.dir))) found.push(tool);
8867
9184
  }
8868
9185
  return found;
8869
9186
  }
8870
- function fromFlags(detected, opts) {
9187
+ function fromFlags(detected, opts, recorded = {}) {
8871
9188
  const tools = opts.tools ? opts.tools.split(",").map((t) => t.trim().toLowerCase()).map((id) => {
8872
9189
  const tool = detected.find((t) => t.id === id);
8873
9190
  if (tool) return tool;
@@ -8886,14 +9203,16 @@ function fromFlags(detected, opts) {
8886
9203
  `${known.label} creates ~/${known.dir} the first time it runs. If it is installed, open it once, then run setup again. ${detectedList}`
8887
9204
  );
8888
9205
  }) : detected;
9206
+ const hookAnswer = parseChoice(opts.hook, recorded.hook ?? "on", "--hook");
8889
9207
  return {
8890
9208
  tools,
8891
- autoUpdate: parseChoice(opts.autoUpdate, "on", "--auto-update"),
9209
+ autoUpdate: parseChoice(opts.autoUpdate, recorded.autoUpdate ?? "on", "--auto-update"),
8892
9210
  errorTelemetry: opts.errorTelemetry === void 0 ? "undecided" : parseChoice(opts.errorTelemetry, "off", "--error-telemetry"),
8893
9211
  // Only Claude Code can run something at session start. For the others the
8894
9212
  // skill tells the agent to call resurface itself, which is why asking for
8895
9213
  // a hook there is not an error — it simply has nowhere to go.
8896
- hook: parseChoice(opts.hook, "on", "--hook") === "on" && tools.some((t) => t.hooks)
9214
+ hook: hookAnswer === "on" && tools.some((t) => t.hooks),
9215
+ hookAnswer
8897
9216
  };
8898
9217
  }
8899
9218
  function parseChoice(value, fallback, flag2) {
@@ -8952,17 +9271,19 @@ async function askHuman(detected, opts) {
8952
9271
  autoUpdate: answers.autoUpdate === false ? "off" : parseChoice(opts.autoUpdate, "on", "--auto-update"),
8953
9272
  // An aborted prompt is not an answer — undecided, never a silent yes.
8954
9273
  errorTelemetry: answers.errorTelemetry === void 0 ? "undecided" : answers.errorTelemetry ? "on" : "off",
8955
- hook: answers.hook !== false
9274
+ hook: answers.hook !== false,
9275
+ // Only a question that was put is an answer to record.
9276
+ ...answers.hook === void 0 ? {} : { hookAnswer: answers.hook ? "on" : "off" }
8956
9277
  };
8957
9278
  }
8958
9279
  async function injectBlock(filePath, content, dryRun, weWroteThis) {
8959
9280
  const block = `${BRAND.hubMarkerStart}
8960
9281
  ${content.trimEnd()}
8961
9282
  ${BRAND.hubMarkerEnd}`;
8962
- const existing = await readFile24(filePath, "utf8").catch(() => null);
9283
+ const existing = await readFile25(filePath, "utf8").catch(() => null);
8963
9284
  if (existing === null) {
8964
9285
  if (!dryRun) {
8965
- await mkdir15(dirname10(filePath), { recursive: true });
9286
+ await mkdir16(dirname10(filePath), { recursive: true });
8966
9287
  await writeThenRename(filePath, `${block}
8967
9288
  `);
8968
9289
  }
@@ -9002,11 +9323,11 @@ function looksLikeOurGuide(text) {
9002
9323
  return text.includes("memgineering recall") && text.includes("memgineering remember");
9003
9324
  }
9004
9325
  async function writeIfDifferent(filePath, content, dryRun) {
9005
- const existing = await readFile24(filePath, "utf8").catch(() => null);
9326
+ const existing = await readFile25(filePath, "utf8").catch(() => null);
9006
9327
  const after = fingerprint(content);
9007
9328
  if (existing === content) return { outcome: "unchanged", before: existing, after };
9008
9329
  if (!dryRun) {
9009
- await mkdir15(dirname10(filePath), { recursive: true });
9330
+ await mkdir16(dirname10(filePath), { recursive: true });
9010
9331
  await writeThenRename(filePath, content);
9011
9332
  }
9012
9333
  return { outcome: existing === null ? "injected" : "updated", before: existing, after };
@@ -9017,8 +9338,8 @@ function hooksFor(tools) {
9017
9338
  );
9018
9339
  }
9019
9340
  async function installHook(spec, tool, dryRun) {
9020
- const settingsPath = join25(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9021
- const raw = await readFile24(settingsPath, "utf8").catch(() => null);
9341
+ const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9342
+ const raw = await readFile25(settingsPath, "utf8").catch(() => null);
9022
9343
  let settings = {};
9023
9344
  if (raw !== null) {
9024
9345
  try {
@@ -9060,15 +9381,15 @@ async function installHook(spec, tool, dryRun) {
9060
9381
  }
9061
9382
  const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
9062
9383
  if (!dryRun) {
9063
- await mkdir15(dirname10(settingsPath), { recursive: true });
9384
+ await mkdir16(dirname10(settingsPath), { recursive: true });
9064
9385
  await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
9065
9386
  `);
9066
9387
  }
9067
9388
  return raw === null ? "injected" : "updated";
9068
9389
  }
9069
9390
  async function removeHook(spec, tool, dryRun) {
9070
- const settingsPath = join25(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9071
- const raw = await readFile24(settingsPath, "utf8").catch(() => null);
9391
+ const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9392
+ const raw = await readFile25(settingsPath, "utf8").catch(() => null);
9072
9393
  if (raw === null) return "unchanged";
9073
9394
  let parsed;
9074
9395
  try {
@@ -9092,7 +9413,7 @@ async function removeHook(spec, tool, dryRun) {
9092
9413
  return "removed";
9093
9414
  }
9094
9415
  async function loadAsset(relPath) {
9095
- const raw = await readFile24(join25(await assetRoot(), relPath), "utf8");
9416
+ const raw = await readFile25(join26(await assetRoot(), relPath), "utf8");
9096
9417
  if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
9097
9418
  const end = raw.indexOf("\n---", 4);
9098
9419
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -9101,12 +9422,12 @@ async function loadAsset(relPath) {
9101
9422
  }
9102
9423
  async function assetRoot() {
9103
9424
  const here = dirname10(fileURLToPath3(import.meta.url));
9104
- const bundled = join25(here, "..", "assets");
9105
- if (await exists2(join25(bundled, BRAND.hubFileName))) return bundled;
9425
+ const bundled = join26(here, "..", "assets");
9426
+ if (await exists2(join26(bundled, BRAND.hubFileName))) return bundled;
9106
9427
  let dir = here;
9107
9428
  for (; ; ) {
9108
- const candidate = join25(dir, "packages", "skills");
9109
- if (await exists2(join25(candidate, BRAND.hubFileName))) return candidate;
9429
+ const candidate = join26(dir, "packages", "skills");
9430
+ if (await exists2(join26(candidate, BRAND.hubFileName))) return candidate;
9110
9431
  const parent = dirname10(dir);
9111
9432
  if (parent === dir) break;
9112
9433
  dir = parent;
@@ -9136,6 +9457,7 @@ var init_setup = __esm({
9136
9457
  init_setup_brain();
9137
9458
  init_setup_signin();
9138
9459
  init_config();
9460
+ init_next_step();
9139
9461
  init_provenance();
9140
9462
  init_ui();
9141
9463
  init_version();
@@ -13040,12 +13362,12 @@ var BASE_FILES = [
13040
13362
  ];
13041
13363
  var HUB_PATH = "00_HUB/HUB.md";
13042
13364
  var TEMPLATE_SLOTS = 2;
13043
- function capTemplates(cards, isTemplate) {
13365
+ function capTemplates(cards, isTemplate, inConflict = () => false) {
13044
13366
  const kept = [];
13045
13367
  const withheld = [];
13046
13368
  let templates = 0;
13047
13369
  for (const card of cards) {
13048
- if (isTemplate(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
13370
+ if (isTemplate(card) && !inConflict(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
13049
13371
  else kept.push(card);
13050
13372
  }
13051
13373
  return { kept, withheld };
@@ -22198,7 +22520,8 @@ function recallCommand() {
22198
22520
  })
22199
22521
  );
22200
22522
  isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
22201
- const capped2 = capTemplates(local.memory_cards, isScaffolding);
22523
+ const sides = new Set(local.conflicts.flatMap((conflict) => conflict.between));
22524
+ const capped2 = capTemplates(local.memory_cards, isScaffolding, (card) => sides.has(card.id));
22202
22525
  templatesWithheld = capped2.withheld.length;
22203
22526
  result = { ...local, memory_cards: capped2.kept };
22204
22527
  entries = built.entries;
@@ -22393,7 +22716,7 @@ ${unsearchedLocal.map(
22393
22716
  if (templatesWithheld > 0) {
22394
22717
  printHuman(
22395
22718
  c.gray(
22396
- ` ${templatesWithheld} more untouched template${templatesWithheld === 1 ? "" : "s"} matched and ${templatesWithheld === 1 ? "is" : "are"} not shown \u2014 at most ${TEMPLATE_SLOTS} per answer.
22719
+ ` ${templatesWithheld} more untouched template${templatesWithheld === 1 ? "" : "s"} matched and ${templatesWithheld === 1 ? "is" : "are"} not shown \u2014 at most ${TEMPLATE_SLOTS} per answer, plus any that is one side of a conflict.
22397
22720
  `
22398
22721
  )
22399
22722
  );
@@ -23024,205 +23347,14 @@ init_setup();
23024
23347
  init_brand();
23025
23348
  init_version();
23026
23349
  init_config();
23027
- import { spawn as spawn3 } from "child_process";
23350
+ init_next_step();
23351
+ init_provenance();
23352
+ import { spawn as spawn4 } from "child_process";
23028
23353
  import { randomBytes as randomBytes2 } from "crypto";
23029
- import { mkdir as mkdir18, open, readFile as readFile27, stat as stat7, unlink as unlink4 } from "fs/promises";
23354
+ import { mkdir as mkdir18, open, readFile as readFile27, rename as rename4, rm as rm8, stat as stat7, unlink as unlink4, writeFile as writeFile15 } from "fs/promises";
23030
23355
  import { join as join28 } from "path";
23031
23356
  import { Command as Command41 } from "commander";
23032
23357
 
23033
- // src/lib/next-step.ts
23034
- init_brand();
23035
- init_api_client();
23036
- init_credentials();
23037
- init_config();
23038
- init_version();
23039
- import { mkdir as mkdir16, readFile as readFile25, writeFile as writeFile13 } from "fs/promises";
23040
- import { join as join26 } from "path";
23041
- function isSetUp(cfg) {
23042
- return Object.keys(cfg.system.installed).length > 0;
23043
- }
23044
- var FINISH_SETUP = `## memgineering is installed, but not set up yet.
23045
-
23046
- Nothing is connected: there is no brain to read, and the agent tools on this
23047
- machine have no memgineering guidance yet. Setup is one command:
23048
-
23049
- \`\`\`bash
23050
- memgineering setup --web
23051
- \`\`\`
23052
-
23053
- It does NOT wait for them. It returns in a few seconds \u2014 measured \u2014 and leaves
23054
- a setup screen open in their browser at memgineering.com, where they choose
23055
- everything: signing in, where the memory lives, which notes. Nothing is
23056
- installed until they finish there, and this machine picks it up on its own.
23057
-
23058
- If they have not already asked you to set it up, ask them first \u2014 it opens a
23059
- browser window on their computer. Once you run it, tell them in their language
23060
- that the setup screen is open. It prints a short code first: show it to them,
23061
- because the screen asks them to compare it.
23062
-
23063
- No browser on this machine (a server, a container, a sandbox)? Run
23064
- \`memgineering setup --web --print-url\` and give them the link to open on the
23065
- computer they are actually sitting at.
23066
-
23067
- \`setup --agent\` answers every one of those questions on their behalf, and
23068
- \`--no-login\` answers the biggest one \u2014 no account, so the memory cannot follow
23069
- them to another machine. Use it only when they have said they would rather you
23070
- decided, or when this machine cannot reach a browser and \`--print-url\` is no
23071
- use to them either.
23072
-
23073
- Setup's output then says how to connect their brain \u2014 create one, pick one, or
23074
- move existing notes up.
23075
-
23076
- The \`memgineering-setup\` skill covers what comes after.`;
23077
- var SETUP_NUDGE = "memgineering is installed, but not set up yet. `memgineering setup --web` opens a setup screen in their browser and does NOT wait for them \u2014 ask them first unless they asked you to set it up.\nThe full notes: run `memgineering` on its own.";
23078
- function connectedMarker(at) {
23079
- return join26(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
23080
- }
23081
- async function connectedNoticeShown(at) {
23082
- return readFile25(connectedMarker(at), "utf8").then(
23083
- () => true,
23084
- () => false
23085
- );
23086
- }
23087
- async function markConnectedNoticeShown(at) {
23088
- try {
23089
- await mkdir16(brandHome(), { recursive: true });
23090
- await writeFile13(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
23091
- `);
23092
- } catch {
23093
- }
23094
- }
23095
- async function claimFullSetupNotice() {
23096
- const marker = join26(brandHome(), "setup-notice-shown");
23097
- try {
23098
- await readFile25(marker, "utf8");
23099
- return false;
23100
- } catch {
23101
- }
23102
- try {
23103
- await mkdir16(brandHome(), { recursive: true });
23104
- await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
23105
- `);
23106
- } catch {
23107
- }
23108
- return true;
23109
- }
23110
- var REFRESH_GUIDANCE = `## The guidance in your agent tools is older than this memgineering.
23111
-
23112
- The hub and skills this CLI installs into your tool's home were written by an
23113
- earlier release. One command rewrites them:
23114
-
23115
- \`\`\`bash
23116
- memgineering setup
23117
- \`\`\`
23118
-
23119
- It does not change which brain this machine reads, or any note. Two things it
23120
- does change, both worth knowing before it runs:
23121
-
23122
- - A setup-owned file your user edited is overwritten; setup names it afterwards.
23123
- - Run with no flags where there is no terminal (an agent's shell), it applies
23124
- setup's defaults over whatever they chose before: auto-update on, its hooks
23125
- on, every agent tool it finds \u2014 and on a machine not signed in, it starts a
23126
- sign-in and prints a code. To keep what they chose, pass it:
23127
- \`--auto-update off\`, \`--hook off\`, \`--no-login\` for a machine they keep
23128
- local, and \`--tools\` naming every tool they use (a tool left out keeps its
23129
- old guidance, and this notice stops appearing anyway).
23130
-
23131
- If you are not sure what they chose, ask them. If you run it,
23132
- mention it to your user in one line: it is upkeep for a tool they already set
23133
- up, and they should not have to discover it later.
23134
-
23135
- Guidance files are read at the start of a session, so what changes today
23136
- arrives in the next one.`;
23137
- var CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
23138
-
23139
- A brain is a folder of markdown notes. Until one is connected there is nothing
23140
- to recall and nowhere to remember. Which folder is your user's choice; the
23141
- command is one you can run for them:
23142
-
23143
- \`\`\`bash
23144
- memgineering link <their-notes-folder> # they already keep notes
23145
- memgineering init <path> # they do not \u2014 creates one, laid out
23146
- \`\`\`
23147
-
23148
- \`link\` reports what would be stored before anything is indexed. An existing
23149
- folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
23150
-
23151
- Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
23152
- it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
23153
-
23154
- Read \`memgineering-setup\` for accounts and which of their tools this reaches.
23155
-
23156
- Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
23157
- var POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
23158
-
23159
- \`memgineering brains\` lists what is on the account.
23160
- \`memgineering use --brain <name>\` points this machine at one.
23161
- \`memgineering create <name>\` makes one and points here, when the account has none yet.
23162
- `;
23163
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
23164
- function shellQuote(value) {
23165
- return `'${value.replace(/'/g, `'\\''`)}'`;
23166
- }
23167
- function confirmUploadNotice(p) {
23168
- const who = p.account_email ?? "the account `setup --web` connected";
23169
- const brain = p.brain_id && UUID.test(p.brain_id) ? ` --brain ${p.brain_id}` : "";
23170
- return `## Notes are waiting to be uploaded \u2014 after a check.
23171
-
23172
- \`memgineering setup --web\` chose to upload \`${p.path}\` to a brain on **${who}**.
23173
- This machine has not sent them: an upload decided on the setup screen waits
23174
- until somebody here has seen which account would receive the notes.
23175
-
23176
- If that is your user's account, this uploads them:
23177
-
23178
- \`\`\`bash
23179
- memgineering push --vault ${shellQuote(p.path)}${brain}
23180
- \`\`\`
23181
-
23182
- If it is not, \`memgineering logout\` disconnects this machine from that account
23183
- and nothing is uploaded. If you are not sure which it is, ask them.`;
23184
- }
23185
- function setupConnectedNotice(c2) {
23186
- const who = c2.email ?? "an account with no email on record";
23187
- return `## This machine is signed in as **${who}**.
23188
-
23189
- \`memgineering setup --web\` connected it to that account. If that is not your user's account,
23190
- \`memgineering logout\` disconnects it, and nothing they write reaches that brain.`;
23191
- }
23192
- function nextStepFor(cfg, signedIn = false, seen = {}) {
23193
- if (cfg.system.pendingUpload) {
23194
- return { id: "confirm_upload", markdown: confirmUploadNotice(cfg.system.pendingUpload) };
23195
- }
23196
- if (cfg.system.setupConnected && !seen.connected) {
23197
- return { id: "setup_connected", markdown: setupConnectedNotice(cfg.system.setupConnected) };
23198
- }
23199
- if (signedIn && cfg.brain.cloud === void 0) {
23200
- return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
23201
- }
23202
- if (isSetUp(cfg) && cfg.system.guidanceVersion !== VERSION) {
23203
- return { id: "refresh_guidance", markdown: REFRESH_GUIDANCE };
23204
- }
23205
- if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
23206
- if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
23207
- if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
23208
- return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
23209
- }
23210
- return null;
23211
- }
23212
- async function currentNextStep() {
23213
- try {
23214
- const stored = await readCredentials().catch(() => null);
23215
- const cfg = await loadConfig();
23216
- const connected = cfg.system.setupConnected ? await connectedNoticeShown(cfg.system.setupConnected.at) : false;
23217
- return nextStepFor(cfg, stored !== null && stored.api_url === apiUrl(), { connected });
23218
- } catch {
23219
- return null;
23220
- }
23221
- }
23222
-
23223
- // src/commands/update.ts
23224
- init_provenance();
23225
-
23226
23358
  // src/lib/update-check.ts
23227
23359
  init_atomic();
23228
23360
  init_brand();
@@ -23432,6 +23564,18 @@ async function runUpdate(opts, inherited) {
23432
23564
  return;
23433
23565
  }
23434
23566
  const held = await holdUpdateSlot(inherited);
23567
+ if (held === null && await blockedBy() === "stuck") {
23568
+ const lock = join28(brandHome(), RECLAIM_LOCK);
23569
+ const hint = `A previous update did not finish, and clearing what it left is blocked by ${lock}. If no memgineering update is running, delete that folder and run \`memgineering update\` again.`;
23570
+ printDual({
23571
+ json: { ...status, provenance: provenance.kind, updated: false, reason: "update_claim_stuck", hint },
23572
+ human: () => {
23573
+ printHuman(`memgineering ${status.latest} is available, but a previous update's leftover claim could not be cleared.`);
23574
+ printHuman(c.gray(hint));
23575
+ }
23576
+ });
23577
+ return;
23578
+ }
23435
23579
  if (held === null) {
23436
23580
  const started = (await claimOwner())?.split(" ")[0];
23437
23581
  const hint = "Another memgineering update is installing right now. Run `memgineering update` again in a few minutes \u2014 it reports whether that install worked.";
@@ -23487,10 +23631,10 @@ async function backgroundUpdateNotice(deps = {}) {
23487
23631
  const status = await (deps.check ?? checkForUpdate)(VERSION);
23488
23632
  await reportPreviousAttempt();
23489
23633
  if (status.updateAvailable && cfg.system.autoUpdate === "on" && isSetUp(cfg) && !process.env["CI"]) {
23490
- const provenance = await (deps.provenance ?? detectProvenance)();
23634
+ const provenance = await (deps.provenance ?? (() => detectProvenance(void 0, { deadlineMs: BACKGROUND_PROBE_DEADLINE_MS })))();
23491
23635
  const token = newClaimToken();
23492
23636
  if (canSelfUpdate(provenance) && await claimUpdateSlot(token)) {
23493
- (deps.startUpdate ?? ((t) => startDetachedUpdate(spawn3, t)))(token);
23637
+ (deps.startUpdate ?? ((t) => startDetachedUpdate(spawn4, t)))(token);
23494
23638
  notice(
23495
23639
  c.gray(
23496
23640
  `memgineering ${status.latest} is available \u2014 installing it in the background (auto-update is on). The next run uses it.`
@@ -23513,7 +23657,7 @@ async function backgroundUpdateNotice(deps = {}) {
23513
23657
  } catch {
23514
23658
  }
23515
23659
  }
23516
- function startDetachedUpdate(spawnImpl = spawn3, token) {
23660
+ function startDetachedUpdate(spawnImpl = spawn4, token) {
23517
23661
  const child = spawnImpl(process.execPath, [process.argv[1] ?? "", "update"], {
23518
23662
  detached: true,
23519
23663
  stdio: "ignore",
@@ -23554,7 +23698,7 @@ async function holdUpdateSlot(inherited) {
23554
23698
  }
23555
23699
  var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
23556
23700
  var UPDATE_CLAIM_MS = 15 * 6e4;
23557
- async function claimUpdateSlot(token = newClaimToken()) {
23701
+ async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
23558
23702
  const path = join28(brandHome(), "update-running");
23559
23703
  try {
23560
23704
  await mkdir18(brandHome(), { recursive: true });
@@ -23567,17 +23711,86 @@ async function claimUpdateSlot(token = newClaimToken()) {
23567
23711
  if (err?.code !== "EEXIST") return false;
23568
23712
  }
23569
23713
  try {
23570
- if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS) await unlink4(path);
23714
+ if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS) {
23715
+ await hooks.afterObserve?.();
23716
+ await withReclaimLock(async () => {
23717
+ if (Date.now() - (await stat7(path)).mtimeMs < UPDATE_CLAIM_MS) return;
23718
+ await hooks.afterRecheck?.();
23719
+ await unlink4(path);
23720
+ });
23721
+ }
23571
23722
  } catch {
23572
23723
  }
23573
23724
  return false;
23574
23725
  }
23726
+ var RECLAIM_LOCK = "update-running.reclaim";
23727
+ var RECLAIM_LOCK_MAX_MS = 10 * 6e4;
23728
+ async function withReclaimLock(fn) {
23729
+ const home = brandHome();
23730
+ const lock = join28(home, RECLAIM_LOCK);
23731
+ const staging = join28(home, `${RECLAIM_LOCK}.${process.pid}.${randomBytes2(4).toString("hex")}`);
23732
+ await mkdir18(staging, { recursive: true });
23733
+ await writeFile15(join28(staging, "owner"), `${process.pid} ${Date.now()}
23734
+ `);
23735
+ try {
23736
+ await rename4(staging, lock);
23737
+ } catch {
23738
+ await rm8(staging, { recursive: true, force: true }).catch(() => void 0);
23739
+ await breakAbandonedLock(lock);
23740
+ return null;
23741
+ }
23742
+ try {
23743
+ return await fn();
23744
+ } finally {
23745
+ const aside = `${lock}.done.${randomBytes2(4).toString("hex")}`;
23746
+ await rename4(lock, aside).then(() => rm8(aside, { recursive: true, force: true })).catch(() => void 0);
23747
+ }
23748
+ }
23749
+ async function breakAbandonedLock(lock) {
23750
+ try {
23751
+ const seen = (await readFile27(join28(lock, "owner"), "utf8")).trim();
23752
+ if (!lockAbandoned(seen)) return;
23753
+ const aside = `${lock}.dead.${randomBytes2(4).toString("hex")}`;
23754
+ await rename4(lock, aside);
23755
+ const moved = (await readFile27(join28(aside, "owner"), "utf8").catch(() => "")).trim();
23756
+ if (moved !== seen) {
23757
+ await rename4(aside, lock).catch(() => void 0);
23758
+ return;
23759
+ }
23760
+ await rm8(aside, { recursive: true, force: true });
23761
+ } catch {
23762
+ }
23763
+ }
23764
+ function lockAbandoned(owner, now = Date.now()) {
23765
+ const [pidText, atText] = owner.split(" ");
23766
+ const pid = Number(pidText);
23767
+ const at = Number(atText);
23768
+ if (!Number.isFinite(at) || now - at > RECLAIM_LOCK_MAX_MS) return true;
23769
+ if (!Number.isInteger(pid) || pid <= 0) return true;
23770
+ try {
23771
+ process.kill(pid, 0);
23772
+ return false;
23773
+ } catch (err) {
23774
+ return err?.code !== "EPERM";
23775
+ }
23776
+ }
23777
+ async function blockedBy() {
23778
+ try {
23779
+ const old = Date.now() - (await stat7(join28(brandHome(), "update-running"))).mtimeMs >= UPDATE_CLAIM_MS;
23780
+ if (old) {
23781
+ await stat7(join28(brandHome(), RECLAIM_LOCK));
23782
+ return "stuck";
23783
+ }
23784
+ } catch {
23785
+ }
23786
+ return "in_progress";
23787
+ }
23575
23788
  async function reportPreviousAttempt() {
23576
23789
  const previous = await readUpdateState();
23577
23790
  if (!previous || previous.ok) return;
23578
23791
  notice(
23579
23792
  c.yellow(`\u26A0 The last update attempt did not complete: ${previous.message}`) + c.gray(
23580
- "\n `memgineering update` shows why. `memgineering setup --agent --auto-update off --hook <on|off>` stops\n these attempts \u2014 name the hook setting you want kept, because `--agent` sets that too."
23793
+ "\n `memgineering update` shows why. `memgineering setup --agent --auto-update off` stops\n these attempts; the hooks stay as this machine recorded them."
23581
23794
  )
23582
23795
  );
23583
23796
  }
@@ -23597,7 +23810,7 @@ function reportBelowMinimum(below) {
23597
23810
 
23598
23811
  // src/commands/undo.ts
23599
23812
  init_src();
23600
- import { readFile as readFile28, rm as rm8 } from "fs/promises";
23813
+ import { readFile as readFile28, rm as rm9 } from "fs/promises";
23601
23814
  import { join as join29 } from "path";
23602
23815
  import { Command as Command42 } from "commander";
23603
23816
  init_atomic();
@@ -23767,7 +23980,7 @@ async function reverse(brain, op, current) {
23767
23980
  "Nothing to take back. `memgineering log` shows what this brain recorded."
23768
23981
  );
23769
23982
  }
23770
- await rm8(absolute, { force: true });
23983
+ await rm9(absolute, { force: true });
23771
23984
  return null;
23772
23985
  }
23773
23986
  if (op.verb === "exclude" || op.verb === "unexclude") {
@@ -24301,6 +24514,7 @@ function whoamiCommand() {
24301
24514
 
24302
24515
  // src/program.ts
24303
24516
  init_config();
24517
+ init_next_step();
24304
24518
  init_ui();
24305
24519
  init_brand();
24306
24520
 
@@ -24491,8 +24705,14 @@ async function run3(argv) {
24491
24705
  process.stderr.isTTY === true ? "person" : cleaned[2] === void 0 ? "full" : "brief"
24492
24706
  );
24493
24707
  }
24708
+ let ran;
24709
+ program.hook("preAction", (_root, action) => {
24710
+ let command = action;
24711
+ while (command.parent && command.parent !== program) command = command.parent;
24712
+ ran = command === program ? void 0 : command.name();
24713
+ });
24494
24714
  await program.parseAsync(cleaned);
24495
- if (argv[2] !== "update") await backgroundUpdateNotice(argv[2] === void 0 ? {} : { verb: argv[2] });
24715
+ if (ran !== "update") await backgroundUpdateNotice(ran === void 0 ? {} : { verb: ran });
24496
24716
  }
24497
24717
  async function handleError(err) {
24498
24718
  const failure = adaptRegistryError(err) ?? err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memgineering",
3
- "version": "0.25.1",
3
+ "version": "0.25.2",
4
4
  "private": false,
5
5
  "description": "One memory for the AI you connect. Recall, remember, and revise a brain your agents share — stored in your own folder.",
6
6
  "license": "Apache-2.0",