memgineering 0.25.1 → 0.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1065,22 +1065,6 @@ function mapNoteToMemory(input) {
1065
1065
  issues
1066
1066
  };
1067
1067
  }
1068
- function isUntouchedBase(path, content) {
1069
- if (!/^01_BASE\//.test(path)) return false;
1070
- const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/, "");
1071
- const lines2 = body.split(/\r?\n/).map((l) => l.trimEnd());
1072
- let seenHeading = false;
1073
- for (const line of lines2) {
1074
- if (line.trim() === "") continue;
1075
- if (!seenHeading && line.startsWith("# ")) {
1076
- seenHeading = true;
1077
- continue;
1078
- }
1079
- if (/^-\s*$/.test(line)) continue;
1080
- if (/^[-*+]\s+\S/.test(line) || /^#{1,6}\s/.test(line)) return false;
1081
- }
1082
- return seenHeading;
1083
- }
1084
1068
  var NAMESPACE, isRecord, asString, asStringArray, AUTHORABLE;
1085
1069
  var init_markdown_mapping = __esm({
1086
1070
  "../../packages/memory-engine/src/markdown-mapping.ts"() {
@@ -2441,11 +2425,18 @@ var init_planner = __esm({
2441
2425
  }
2442
2426
  });
2443
2427
 
2444
- // ../../packages/memory-engine/src/recall/semantic.ts
2445
- var init_semantic = __esm({
2446
- "../../packages/memory-engine/src/recall/semantic.ts"() {
2428
+ // ../../packages/memory-engine/src/note-body.ts
2429
+ function bodyOf(content) {
2430
+ const stripped = content.startsWith(BOM) ? content.slice(BOM.length) : content;
2431
+ const fence2 = FENCE.exec(stripped);
2432
+ return fence2 ? stripped.slice(fence2[0].length) : stripped;
2433
+ }
2434
+ var BOM, FENCE;
2435
+ var init_note_body = __esm({
2436
+ "../../packages/memory-engine/src/note-body.ts"() {
2447
2437
  "use strict";
2448
- init_markdown_mapping();
2438
+ BOM = String.fromCharCode(65279);
2439
+ FENCE = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/;
2449
2440
  }
2450
2441
  });
2451
2442
 
@@ -2469,10 +2460,31 @@ function base(id, title, summary, body) {
2469
2460
  function ownEntry(record, key2) {
2470
2461
  return Object.hasOwn(record, key2) ? record[key2] : void 0;
2471
2462
  }
2472
- var BASE_TEMPLATES, RETIRED_BASE_TEMPLATES;
2463
+ function wordingsFor(path) {
2464
+ const current = ownEntry(BASE_TEMPLATES, path);
2465
+ const retired = ownEntry(RETIRED_BASE_TEMPLATES, path) ?? [];
2466
+ return current === void 0 ? retired : [current, ...retired];
2467
+ }
2468
+ function isExactUntouchedBase(path, content) {
2469
+ const saved = asSaved(content);
2470
+ return wordingsFor(path).some((t) => asSaved(t) === saved);
2471
+ }
2472
+ function isTemplateBody(path, content) {
2473
+ const lines2 = bodyLines(content).join("\n");
2474
+ return wordingsFor(path).some((t) => bodyLines(t).join("\n") === lines2);
2475
+ }
2476
+ function isUnwrittenBody(path, content) {
2477
+ if (isTemplateBody(path, content)) return true;
2478
+ const [first, ...rest] = bodyLines(content);
2479
+ if (first === void 0) return false;
2480
+ const headings = wordingsFor(path).map((t) => bodyLines(t)[0]);
2481
+ return headings.includes(first) && rest.every((line) => /^-\s*$/.test(line));
2482
+ }
2483
+ var BASE_TEMPLATES, RETIRED_BASE_TEMPLATES, BOM2, asSaved, bodyLines;
2473
2484
  var init_base_templates = __esm({
2474
2485
  "../../packages/memory-engine/src/base-templates.ts"() {
2475
2486
  "use strict";
2487
+ init_note_body();
2476
2488
  BASE_TEMPLATES = {
2477
2489
  "01_BASE/CRITICAL_FACTS.md": base(
2478
2490
  "critical-facts",
@@ -2553,6 +2565,17 @@ var init_base_templates = __esm({
2553
2565
  ].join("\n")
2554
2566
  ]
2555
2567
  };
2568
+ BOM2 = String.fromCharCode(65279);
2569
+ asSaved = (text) => (text.startsWith(BOM2) ? text.slice(BOM2.length) : text).replace(/\r\n/g, "\n").trimEnd();
2570
+ bodyLines = (content) => bodyOf(content).split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
2571
+ }
2572
+ });
2573
+
2574
+ // ../../packages/memory-engine/src/recall/semantic.ts
2575
+ var init_semantic = __esm({
2576
+ "../../packages/memory-engine/src/recall/semantic.ts"() {
2577
+ "use strict";
2578
+ init_base_templates();
2556
2579
  }
2557
2580
  });
2558
2581
 
@@ -2577,6 +2600,13 @@ var init_unfilled = __esm({
2577
2600
  }
2578
2601
  });
2579
2602
 
2603
+ // ../../packages/memory-engine/src/setup-progress.ts
2604
+ var init_setup_progress = __esm({
2605
+ "../../packages/memory-engine/src/setup-progress.ts"() {
2606
+ "use strict";
2607
+ }
2608
+ });
2609
+
2580
2610
  // ../../packages/memory-engine/src/recall/query-prefix.ts
2581
2611
  var init_query_prefix = __esm({
2582
2612
  "../../packages/memory-engine/src/recall/query-prefix.ts"() {
@@ -4043,11 +4073,6 @@ function bodyStillAsserts(content, oldClaim, newClaim, normalize) {
4043
4073
  if (line === normalize(newClaim?.split("\n")[0] ?? "").trim()) return null;
4044
4074
  return bodyOf(content).includes(line) ? line : null;
4045
4075
  }
4046
- function bodyOf(content) {
4047
- const stripped = content.replace(/^/, "");
4048
- const fence2 = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
4049
- return fence2 ? stripped.slice(fence2[0].length) : stripped;
4050
- }
4051
4076
  function previousClaim(entry) {
4052
4077
  const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
4053
4078
  return current?.text ?? null;
@@ -4065,6 +4090,7 @@ var init_curate = __esm({
4065
4090
  init_planner();
4066
4091
  init_schema();
4067
4092
  init_rule_budget();
4093
+ init_note_body();
4068
4094
  NO_REASON_GIVEN = "revised directly";
4069
4095
  noLifecycleReason = (action) => `${action}d directly`;
4070
4096
  }
@@ -4905,6 +4931,7 @@ var init_src = __esm({
4905
4931
  init_planner();
4906
4932
  init_semantic();
4907
4933
  init_unfilled();
4934
+ init_setup_progress();
4908
4935
  init_query_prefix();
4909
4936
  init_base_templates();
4910
4937
  init_scope_spread();
@@ -5507,6 +5534,13 @@ var init_config = __esm({
5507
5534
  * answer as "older than this CLI" and wants the same one command.
5508
5535
  */
5509
5536
  guidanceVersion: z8.string().optional(),
5537
+ /**
5538
+ * The person's answer to "arm the hooks?", as they gave it — not whether
5539
+ * this machine's tools could take one. A flag-less `setup` reads it back,
5540
+ * so a refresh keeps what they chose instead of setup's default (0.25.2).
5541
+ * Absent on a machine set up before it was recorded.
5542
+ */
5543
+ hook: z8.enum(["on", "off"]).optional().catch(void 0),
5510
5544
  /**
5511
5545
  * Notes `setup --web` was asked to upload, not yet sent.
5512
5546
  *
@@ -7864,13 +7898,208 @@ var init_setup_signin = __esm({
7864
7898
  }
7865
7899
  });
7866
7900
 
7901
+ // src/lib/next-step.ts
7902
+ import { mkdir as mkdir15, readFile as readFile24, writeFile as writeFile13 } from "fs/promises";
7903
+ import { join as join23 } from "path";
7904
+ function isSetUp(cfg) {
7905
+ return Object.keys(cfg.system.installed).length > 0;
7906
+ }
7907
+ function connectedMarker(at) {
7908
+ return join23(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
7909
+ }
7910
+ async function connectedNoticeShown(at) {
7911
+ return readFile24(connectedMarker(at), "utf8").then(
7912
+ () => true,
7913
+ () => false
7914
+ );
7915
+ }
7916
+ async function markConnectedNoticeShown(at) {
7917
+ try {
7918
+ await mkdir15(brandHome(), { recursive: true });
7919
+ await writeFile13(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
7920
+ `);
7921
+ } catch {
7922
+ }
7923
+ }
7924
+ async function claimFullSetupNotice() {
7925
+ const marker = join23(brandHome(), "setup-notice-shown");
7926
+ try {
7927
+ await readFile24(marker, "utf8");
7928
+ return false;
7929
+ } catch {
7930
+ }
7931
+ try {
7932
+ await mkdir15(brandHome(), { recursive: true });
7933
+ await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
7934
+ `);
7935
+ } catch {
7936
+ }
7937
+ return true;
7938
+ }
7939
+ function shellQuote(value) {
7940
+ return `'${value.replace(/'/g, `'\\''`)}'`;
7941
+ }
7942
+ function confirmUploadNotice(p) {
7943
+ const who = p.account_email ?? "the account `setup --web` connected";
7944
+ const brain = p.brain_id && UUID.test(p.brain_id) ? ` --brain ${p.brain_id}` : "";
7945
+ return `## Notes are waiting to be uploaded \u2014 after a check.
7946
+
7947
+ \`memgineering setup --web\` chose to upload \`${p.path}\` to a brain on **${who}**.
7948
+ This machine has not sent them: an upload decided on the setup screen waits
7949
+ until somebody here has seen which account would receive the notes.
7950
+
7951
+ If that is your user's account, this uploads them:
7952
+
7953
+ \`\`\`bash
7954
+ memgineering push --vault ${shellQuote(p.path)}${brain}
7955
+ \`\`\`
7956
+
7957
+ If it is not, \`memgineering logout\` disconnects this machine from that account
7958
+ and nothing is uploaded. If you are not sure which it is, ask them.`;
7959
+ }
7960
+ function setupConnectedNotice(c2) {
7961
+ const who = c2.email ?? "an account with no email on record";
7962
+ return `## This machine is signed in as **${who}**.
7963
+
7964
+ \`memgineering setup --web\` connected it to that account. If that is not your user's account,
7965
+ \`memgineering logout\` disconnects it, and nothing they write reaches that brain.`;
7966
+ }
7967
+ function nextStepFor(cfg, signedIn = false, seen = {}) {
7968
+ if (cfg.system.pendingUpload) {
7969
+ return { id: "confirm_upload", markdown: confirmUploadNotice(cfg.system.pendingUpload) };
7970
+ }
7971
+ if (cfg.system.setupConnected && !seen.connected) {
7972
+ return { id: "setup_connected", markdown: setupConnectedNotice(cfg.system.setupConnected) };
7973
+ }
7974
+ if (signedIn && cfg.brain.cloud === void 0) {
7975
+ return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
7976
+ }
7977
+ if (isSetUp(cfg) && cfg.system.guidanceVersion !== VERSION) {
7978
+ return { id: "refresh_guidance", markdown: REFRESH_GUIDANCE };
7979
+ }
7980
+ if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
7981
+ if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
7982
+ if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
7983
+ return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
7984
+ }
7985
+ return null;
7986
+ }
7987
+ async function currentNextStep() {
7988
+ try {
7989
+ const stored = await readCredentials().catch(() => null);
7990
+ const cfg = await loadConfig();
7991
+ const connected = cfg.system.setupConnected ? await connectedNoticeShown(cfg.system.setupConnected.at) : false;
7992
+ return nextStepFor(cfg, stored !== null && stored.api_url === apiUrl(), { connected });
7993
+ } catch {
7994
+ return null;
7995
+ }
7996
+ }
7997
+ var FINISH_SETUP, SETUP_NUDGE, REFRESH_GUIDANCE, CONNECT_A_BRAIN, POINT_AT_A_BRAIN, UUID;
7998
+ var init_next_step = __esm({
7999
+ "src/lib/next-step.ts"() {
8000
+ "use strict";
8001
+ init_brand();
8002
+ init_api_client();
8003
+ init_credentials();
8004
+ init_config();
8005
+ init_version();
8006
+ FINISH_SETUP = `## memgineering is installed, but not set up yet.
8007
+
8008
+ Nothing is connected: there is no brain to read, and the agent tools on this
8009
+ machine have no memgineering guidance yet. Setup is one command:
8010
+
8011
+ \`\`\`bash
8012
+ memgineering setup --web
8013
+ \`\`\`
8014
+
8015
+ It does NOT wait for them. It returns in a few seconds \u2014 measured \u2014 and leaves
8016
+ a setup screen open in their browser at memgineering.com, where they choose
8017
+ everything: signing in, where the memory lives, which notes. Nothing is
8018
+ installed until they finish there, and this machine picks it up on its own.
8019
+
8020
+ If they have not already asked you to set it up, ask them first \u2014 it opens a
8021
+ browser window on their computer. Once you run it, tell them in their language
8022
+ that the setup screen is open. It prints a short code first: show it to them,
8023
+ because the screen asks them to compare it.
8024
+
8025
+ No browser on this machine (a server, a container, a sandbox)? Run
8026
+ \`memgineering setup --web --print-url\` and give them the link to open on the
8027
+ computer they are actually sitting at.
8028
+
8029
+ \`setup --agent\` answers every one of those questions on their behalf, and
8030
+ \`--no-login\` answers the biggest one \u2014 no account, so the memory cannot follow
8031
+ them to another machine. Use it only when they have said they would rather you
8032
+ decided, or when this machine cannot reach a browser and \`--print-url\` is no
8033
+ use to them either.
8034
+
8035
+ Setup's output then says how to connect their brain \u2014 create one, pick one, or
8036
+ move existing notes up.
8037
+
8038
+ The \`memgineering-setup\` skill covers what comes after.`;
8039
+ 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.";
8040
+ REFRESH_GUIDANCE = `## The guidance in your agent tools is older than this memgineering.
8041
+
8042
+ The hub and skills this CLI installs into your tool's home were written by an
8043
+ earlier release. One command rewrites them:
8044
+
8045
+ \`\`\`bash
8046
+ memgineering setup
8047
+ \`\`\`
8048
+
8049
+ It does not change which brain this machine reads, or any note. Two things it
8050
+ does change, both worth knowing before it runs:
8051
+
8052
+ - A setup-owned file your user edited is overwritten; setup names it afterwards.
8053
+ - Run with no flags where there is no terminal (an agent's shell), it keeps the
8054
+ auto-update and hook choices recorded on this machine, and installs into
8055
+ every agent tool it finds \u2014 \`--tools\` naming every tool they use keeps it to
8056
+ those (a tool left out keeps its old guidance, and this notice stops
8057
+ appearing anyway). On a machine not signed in, it starts a sign-in and prints
8058
+ a code; \`--no-login\` declines that for a machine they keep local.
8059
+
8060
+ If you are not sure what they chose, ask them. If you run it,
8061
+ mention it to your user in one line: it is upkeep for a tool they already set
8062
+ up, and they should not have to discover it later.
8063
+
8064
+ Guidance files are read at the start of a session, so what changes today
8065
+ arrives in the next one.`;
8066
+ CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
8067
+
8068
+ A brain is a folder of markdown notes. Until one is connected there is nothing
8069
+ to recall and nowhere to remember. Which folder is your user's choice; the
8070
+ command is one you can run for them:
8071
+
8072
+ \`\`\`bash
8073
+ memgineering link <their-notes-folder> # they already keep notes
8074
+ memgineering init <path> # they do not \u2014 creates one, laid out
8075
+ \`\`\`
8076
+
8077
+ \`link\` reports what would be stored before anything is indexed. An existing
8078
+ folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
8079
+
8080
+ Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
8081
+ it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
8082
+
8083
+ Read \`memgineering-setup\` for accounts and which of their tools this reaches.
8084
+
8085
+ Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
8086
+ POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
8087
+
8088
+ \`memgineering brains\` lists what is on the account.
8089
+ \`memgineering use --brain <name>\` points this machine at one.
8090
+ \`memgineering create <name>\` makes one and points here, when the account has none yet.
8091
+ `;
8092
+ UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8093
+ }
8094
+ });
8095
+
7867
8096
  // src/lib/provenance.ts
7868
- import { execFile } from "child_process";
8097
+ import { execFile, spawn as spawn2 } from "child_process";
7869
8098
  import { realpath as realpath3 } from "fs/promises";
7870
- import { dirname as dirname8, join as join23, resolve as resolve14 } from "path";
8099
+ import { dirname as dirname8, join as join24, resolve as resolve14 } from "path";
7871
8100
  import { fileURLToPath as fileURLToPath2 } from "url";
7872
8101
  import { promisify } from "util";
7873
- async function detectProvenance(selfPath) {
8102
+ async function detectProvenance(selfPath, opts = {}) {
7874
8103
  const entry = selfPath ?? fileURLToPath2(import.meta.url);
7875
8104
  const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
7876
8105
  if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
@@ -7882,9 +8111,11 @@ async function detectProvenance(selfPath) {
7882
8111
  reason: "this is a one-off npx run, not an installed copy"
7883
8112
  };
7884
8113
  }
7885
- for (const manager of MANAGERS) {
7886
- const prefix = await prefixOf(manager.bin, manager.prefixArgs);
7887
- if (prefix === null) continue;
8114
+ const deadline = opts.deadlineMs;
8115
+ const answers = deadline === void 0 ? null : await probeAllWithin(MANAGERS.map((m) => [m.bin, m.prefixArgs]), deadline);
8116
+ for (const [i, manager] of MANAGERS.entries()) {
8117
+ const prefix = answers === null ? await prefixOf(manager.bin, manager.prefixArgs) : answers.prefixes[i];
8118
+ if (prefix === null || prefix === void 0) continue;
7888
8119
  if (isInside2(selfDir, manager.moduleRoot(prefix))) {
7889
8120
  return {
7890
8121
  kind: manager.kind,
@@ -7895,6 +8126,15 @@ async function detectProvenance(selfPath) {
7895
8126
  };
7896
8127
  }
7897
8128
  }
8129
+ if (answers?.timedOut === true) {
8130
+ return {
8131
+ kind: "undetermined",
8132
+ selfDir,
8133
+ prefix: null,
8134
+ command: "npm i -g memgineering@latest",
8135
+ reason: "the package managers did not answer in time to tell which one installed this copy"
8136
+ };
8137
+ }
7898
8138
  const local = selfDir.includes(`${"node_modules"}`);
7899
8139
  return {
7900
8140
  kind: local ? "local" : "unknown",
@@ -7907,6 +8147,62 @@ async function detectProvenance(selfPath) {
7907
8147
  function canSelfUpdate(p) {
7908
8148
  return p.kind === "npm" || p.kind === "pnpm" || p.kind === "bun";
7909
8149
  }
8150
+ async function probeAllWithin(probes, deadlineMs) {
8151
+ const controller = new AbortController();
8152
+ const timer = setTimeout(() => controller.abort(), deadlineMs);
8153
+ timer.unref();
8154
+ try {
8155
+ const prefixes = await Promise.all(
8156
+ probes.map(([bin, args]) => probeOnce(bin, args, controller.signal))
8157
+ );
8158
+ return { prefixes, timedOut: controller.signal.aborted };
8159
+ } finally {
8160
+ clearTimeout(timer);
8161
+ }
8162
+ }
8163
+ function probeOnce(bin, args, signal) {
8164
+ return new Promise((settle) => {
8165
+ let child;
8166
+ try {
8167
+ child = spawn2(bin, args, { detached: true, stdio: ["ignore", "pipe", "ignore"] });
8168
+ } catch {
8169
+ settle(null);
8170
+ return;
8171
+ }
8172
+ let out = "";
8173
+ let done = false;
8174
+ const finish = (value) => {
8175
+ if (done) return;
8176
+ done = true;
8177
+ signal.removeEventListener("abort", stop);
8178
+ settle(value);
8179
+ };
8180
+ function stop() {
8181
+ try {
8182
+ if (process.platform === "win32" || child.pid === void 0) child.kill("SIGKILL");
8183
+ else process.kill(-child.pid, "SIGKILL");
8184
+ } catch {
8185
+ }
8186
+ child.stdout?.destroy();
8187
+ child.unref();
8188
+ finish(null);
8189
+ }
8190
+ child.stdout?.on("data", (chunk) => {
8191
+ out += chunk.toString("utf8");
8192
+ });
8193
+ child.on("error", () => finish(null));
8194
+ child.on("close", (code) => {
8195
+ const value = out.trim();
8196
+ if (code !== 0 || value === "") {
8197
+ finish(null);
8198
+ return;
8199
+ }
8200
+ void realpath3(value).catch(() => value).then(finish);
8201
+ });
8202
+ if (signal.aborted) stop();
8203
+ else signal.addEventListener("abort", stop, { once: true });
8204
+ });
8205
+ }
7910
8206
  async function prefixOf(bin, args) {
7911
8207
  try {
7912
8208
  const { stdout } = await run(bin, args, { timeout: 5e3 });
@@ -7928,7 +8224,7 @@ async function unreachableByName(selfPath) {
7928
8224
  const p = await detectProvenance(entry);
7929
8225
  return p.kind === "local" || p.kind === "npx" ? p.reason : null;
7930
8226
  }
7931
- var run, MANAGERS;
8227
+ var run, MANAGERS, BACKGROUND_PROBE_DEADLINE_MS;
7932
8228
  var init_provenance = __esm({
7933
8229
  "src/lib/provenance.ts"() {
7934
8230
  "use strict";
@@ -7938,7 +8234,7 @@ var init_provenance = __esm({
7938
8234
  kind: "npm",
7939
8235
  bin: "npm",
7940
8236
  prefixArgs: ["prefix", "-g"],
7941
- moduleRoot: (prefix) => process.platform === "win32" ? join23(prefix, "node_modules") : join23(prefix, "lib", "node_modules"),
8237
+ moduleRoot: (prefix) => process.platform === "win32" ? join24(prefix, "node_modules") : join24(prefix, "lib", "node_modules"),
7942
8238
  install: "npm i -g memgineering@latest"
7943
8239
  },
7944
8240
  {
@@ -7957,6 +8253,7 @@ var init_provenance = __esm({
7957
8253
  install: "bun add -g memgineering"
7958
8254
  }
7959
8255
  ];
8256
+ BACKGROUND_PROBE_DEADLINE_MS = 1500;
7960
8257
  }
7961
8258
  });
7962
8259
 
@@ -7972,24 +8269,29 @@ __export(setup_cloud_exports, {
7972
8269
  runSetupCloudChild: () => runSetupCloudChild,
7973
8270
  waitAndApply: () => waitAndApply
7974
8271
  });
7975
- import { spawn as spawn2 } from "child_process";
8272
+ import { spawn as spawn3 } from "child_process";
7976
8273
  import { realpathSync as realpathSync2 } from "fs";
7977
8274
  import { readdir as readdir7, stat as stat6 } from "fs/promises";
7978
8275
  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) {
8276
+ import { basename as basename3, dirname as dirname9, join as join25, resolve as resolve15, sep as sep4 } from "path";
8277
+ async function report2(setupToken, state, detail, code) {
7981
8278
  try {
7982
8279
  await apiRequest({
7983
8280
  method: "POST",
7984
8281
  path: "/v1/setup/session/report",
7985
- body: { setup_token: setupToken, state, ...detail === void 0 ? {} : { detail } }
8282
+ body: {
8283
+ setup_token: setupToken,
8284
+ state,
8285
+ ...detail === void 0 ? {} : { detail },
8286
+ ...code === void 0 ? {} : { code }
8287
+ }
7986
8288
  });
7987
8289
  } catch {
7988
8290
  }
7989
8291
  }
7990
8292
  async function folderCandidates() {
7991
8293
  const home = homedir5();
7992
- const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join24(home, d));
8294
+ const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join25(home, d));
7993
8295
  const found = [];
7994
8296
  const countNotes = async (dir) => {
7995
8297
  let count = 0;
@@ -7998,7 +8300,7 @@ async function folderCandidates() {
7998
8300
  if (entry.name.startsWith(".")) continue;
7999
8301
  if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
8000
8302
  else if (entry.isDirectory()) {
8001
- const inner = await readdir7(join24(dir, entry.name)).catch(() => []);
8303
+ const inner = await readdir7(join25(dir, entry.name)).catch(() => []);
8002
8304
  count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
8003
8305
  }
8004
8306
  if (count > 999) break;
@@ -8058,17 +8360,24 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
8058
8360
  ...cfg,
8059
8361
  system: {
8060
8362
  ...cfg.system,
8061
- setupConnected: { account_id: account.id, email: account.email, at: (/* @__PURE__ */ new Date()).toISOString() }
8363
+ setupConnected: {
8364
+ account_id: account.id,
8365
+ email: account.email,
8366
+ at: (/* @__PURE__ */ new Date()).toISOString()
8367
+ }
8062
8368
  }
8063
8369
  });
8064
8370
  }
8065
8371
  }
8066
- await report2(setupToken, "applying", "\uC124\uC815\uC744 \uC4F0\uB294 \uC911\uC774\uC5D0\uC694.");
8372
+ await report2(setupToken, "applying", "\uC124\uC815\uC744 \uC4F0\uB294 \uC911\uC774\uC5D0\uC694.", "writing_config");
8067
8373
  const choice = {
8068
8374
  tools: toolsFromAnswers(answers, detected),
8069
8375
  autoUpdate: answers.auto_update ? "on" : "off",
8070
8376
  errorTelemetry: answers.error_telemetry === null ? "undecided" : answers.error_telemetry ? "on" : "off",
8071
- hook: answers.hook
8377
+ hook: answers.hook,
8378
+ // The screen always shows the hook switch, so this is the person's answer,
8379
+ // and a later flag-less refresh keeps it only if it is recorded as one.
8380
+ hookAnswer: answers.hook ? "on" : "off"
8072
8381
  };
8073
8382
  await applySetup(choice, false);
8074
8383
  const parts = [];
@@ -8096,7 +8405,7 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
8096
8405
  const email = account?.email ?? null;
8097
8406
  const yes = guards.confirmUpload ? await guards.confirmUpload({ path, email }) : null;
8098
8407
  if (yes === true) {
8099
- await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.");
8408
+ await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.", "uploading");
8100
8409
  const { pushCommand: pushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
8101
8410
  await pushCommand2().parseAsync(
8102
8411
  ["--vault", path, ...claim.brain?.id ? ["--brain", claim.brain.id] : []],
@@ -8119,7 +8428,12 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
8119
8428
  }
8120
8429
  }
8121
8430
  });
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.");
8431
+ await report2(
8432
+ setupToken,
8433
+ "applying",
8434
+ "\uB178\uD2B8 \uC62C\uB9AC\uAE30\uB294 \uC774 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uD655\uC778\uD55C \uB4A4\uC5D0 \uC2DC\uC791\uB3FC\uC694.",
8435
+ "upload_waits_for_check"
8436
+ );
8123
8437
  parts.push(`upload of ${path} waits for a check on this machine`);
8124
8438
  } else {
8125
8439
  parts.push(`not uploaded: ${path}`);
@@ -8189,11 +8503,12 @@ async function waitAndApply(setupToken, opts) {
8189
8503
  candidates: offered,
8190
8504
  confirmUpload: opts.confirmUpload ?? null
8191
8505
  });
8192
- await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.");
8506
+ await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.", "done");
8193
8507
  return done;
8194
8508
  } catch (err) {
8195
8509
  const message = err instanceof Error ? err.message : String(err);
8196
- await report2(setupToken, "failed", message.slice(0, 400));
8510
+ const code = err.code === "conflict" ? "different_account" : "failed";
8511
+ await report2(setupToken, "failed", message.slice(0, 400), code);
8197
8512
  throw err;
8198
8513
  }
8199
8514
  }
@@ -8205,7 +8520,7 @@ async function waitAndApply(setupToken, opts) {
8205
8520
  }).catch(() => null);
8206
8521
  if (state?.body?.folders_requested === true) {
8207
8522
  sentFolders = true;
8208
- opts.onProgress?.("\uD3F4\uB354\uB97C \uCC3E\uC544\uBCF4\uB294 \uC911\u2026");
8523
+ opts.onProgress?.("Looking for note folders on this machine\u2026");
8209
8524
  const candidates = await folderCandidates();
8210
8525
  offered = candidates.map((c2) => c2.path);
8211
8526
  await apiRequest({
@@ -8236,7 +8551,7 @@ async function startSession(detected) {
8236
8551
  return body;
8237
8552
  }
8238
8553
  function detachChild(setupToken) {
8239
- const child = spawn2(process.execPath, [process.argv[1] ?? "", "setup", "--web"], {
8554
+ const child = spawn3(process.execPath, [process.argv[1] ?? "", "setup", "--web"], {
8240
8555
  detached: true,
8241
8556
  stdio: ["pipe", "ignore", "ignore"],
8242
8557
  env: { ...process.env, [SETUP_CHILD_ENV]: "1" }
@@ -8255,7 +8570,7 @@ async function runSetupCloud(opts) {
8255
8570
  const session = await startSession(opts.detected);
8256
8571
  const intervalMs = Math.max(1, session.interval ?? 5) * 1e3;
8257
8572
  printHuman(`
8258
- \uC124\uCE58 \uCF54\uB4DC: ${c.bold(session.user_code)}
8573
+ Setup code: ${c.bold(session.user_code)}
8259
8574
  `);
8260
8575
  const opened = opts.printUrl ? false : await openBrowser(session.setup_url);
8261
8576
  const foreground = opts.wait || process.stdout.isTTY === true && process.stderr.isTTY === true;
@@ -8271,14 +8586,14 @@ async function runSetupCloud(opts) {
8271
8586
  waiting: foreground
8272
8587
  },
8273
8588
  human: () => {
8274
- if (opened) printHuman("\uBE0C\uB77C\uC6B0\uC800\uC5D0 \uC124\uCE58 \uD654\uBA74\uC744 \uC5F4\uC5C8\uC5B4\uC694.");
8589
+ if (opened) printHuman("Opened the setup screen in the browser.");
8275
8590
  else {
8276
- printHuman("\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC774 \uC8FC\uC18C\uB97C \uC5F4\uC5B4 \uC124\uCE58\uB97C \uB9C8\uCCD0 \uC8FC\uC138\uC694:\n");
8591
+ printHuman("Open this link in a browser to finish the setup:\n");
8277
8592
  printHuman(` ${session.setup_url}`);
8278
8593
  }
8279
8594
  printHuman(
8280
8595
  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."
8596
+ 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
8597
  )
8283
8598
  );
8284
8599
  }
@@ -8296,7 +8611,7 @@ async function runSetupCloud(opts) {
8296
8611
  confirmUpload: interactive ? askBeforeUpload : null
8297
8612
  });
8298
8613
  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.";
8614
+ const line = "The setup screen expired and nothing was changed. `memgineering setup --web` opens a new one.";
8300
8615
  if (isJsonMode()) notice(JSON.stringify({ setup_web: "expired" }));
8301
8616
  else printHuman(c.gray(`
8302
8617
  ${line}`));
@@ -8304,10 +8619,10 @@ ${line}`));
8304
8619
  }
8305
8620
  if (isJsonMode()) notice(JSON.stringify({ setup_web: "applied" }));
8306
8621
  else {
8307
- printHuman(c.green("\n\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694."));
8622
+ printHuman(c.green("\nSetup finished."));
8308
8623
  printHuman(
8309
8624
  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."
8625
+ " recall and remember work now.\n The newly installed guidance is read from the next conversation on."
8311
8626
  )
8312
8627
  );
8313
8628
  }
@@ -8342,7 +8657,7 @@ async function askBeforeUpload(q) {
8342
8657
  const { yes } = await prompts6({
8343
8658
  type: "confirm",
8344
8659
  name: "yes",
8345
- message: `${q.path} \uC758 \uB178\uD2B8\uB97C ${q.email ?? "\uC774 \uACC4\uC815"} \uBE0C\uB808\uC778\uC73C\uB85C \uC62C\uB9B4\uAE4C\uC694?`,
8660
+ message: `Upload the notes in ${q.path} to the brain on ${q.email ?? "this account"}?`,
8346
8661
  initial: false
8347
8662
  });
8348
8663
  return yes === true;
@@ -8377,8 +8692,8 @@ var init_setup_cloud = __esm({
8377
8692
 
8378
8693
  // src/commands/setup.ts
8379
8694
  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";
8695
+ import { access as access2, mkdir as mkdir16, readFile as readFile25 } from "fs/promises";
8696
+ import { dirname as dirname10, join as join26 } from "path";
8382
8697
  import { fileURLToPath as fileURLToPath3 } from "url";
8383
8698
  import { Command as Command40 } from "commander";
8384
8699
  import prompts5 from "prompts";
@@ -8403,10 +8718,11 @@ function setupCommand() {
8403
8718
  "after",
8404
8719
  [
8405
8720
  "",
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)."
8721
+ "Chat apps with no shell need no setup. They read and write the brain on your",
8722
+ `account (not a folder on this machine) at ${HOSTED_CONNECTOR_URL}:`,
8723
+ " ChatGPT: Settings \u2192 Developer mode \u2192 add an MCP server with that URL, sign in.",
8724
+ " Claude.ai, Claude Desktop, Cowork: Customize \u2192 Connectors \u2192 Add custom",
8725
+ " connector with that URL, sign in."
8410
8726
  ].join("\n")
8411
8727
  ).action(async (opts) => {
8412
8728
  const detected = await detectTools();
@@ -8441,16 +8757,18 @@ function setupCommand() {
8441
8757
  dryRun: opts.dryRun === true
8442
8758
  });
8443
8759
  const mode = await resolveMode(opts);
8444
- const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts);
8760
+ const recorded = mode.kind === "human" ? {} : await recordedChoices(detected);
8761
+ const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts, recorded);
8445
8762
  const defaultsTaken = !mode.chosen ? [
8446
- ...opts.autoUpdate === void 0 ? ["auto-update: on"] : [],
8763
+ // Only what nobody chose, here or on an earlier run.
8764
+ ...opts.autoUpdate === void 0 && recorded.autoUpdate === void 0 ? ["auto-update: on"] : [],
8447
8765
  // Every callback this flag arms, named — on Claude Code and Codex
8448
8766
  // that is two (note summaries at session start, the user's own
8449
8767
  // rules before an edit), and saying only the first would be
8450
8768
  // reporting half of what was decided on their behalf. On Grok it
8451
8769
  // is one, at the end of a turn, and naming the other two there
8452
8770
  // reported callbacks that were never installed.
8453
- ...opts.hook === void 0 && choice?.hook ? hooksFor(choice.tools).map((h) => `${h.what}: on`) : []
8771
+ ...opts.hook === void 0 && recorded.hook === void 0 && choice?.hook ? hooksFor(choice.tools).map((h) => `${h.what}: on`) : []
8454
8772
  ] : [];
8455
8773
  if (choice === null) {
8456
8774
  printHuman(c.yellow("Cancelled \u2014 nothing was changed."));
@@ -8716,6 +9034,7 @@ async function applySetup(choice, dryRun) {
8716
9034
  system: {
8717
9035
  ...cfg.system,
8718
9036
  autoUpdate: choice.autoUpdate,
9037
+ ...choice.hookAnswer === void 0 ? {} : { hook: choice.hookAnswer },
8719
9038
  installed,
8720
9039
  // Stamped whether or not anything was rewritten. A run that finds every
8721
9040
  // file already current is exactly the run that must clear the notice —
@@ -8741,7 +9060,7 @@ async function applySetup(choice, dryRun) {
8741
9060
  }
8742
9061
  async function write2() {
8743
9062
  for (const tool of choice.tools) {
8744
- const hubPath = join25(agentHome(), tool.hubFile);
9063
+ const hubPath = join26(agentHome(), tool.hubFile);
8745
9064
  const written = await injectBlock(
8746
9065
  hubPath,
8747
9066
  hub + tailFor(tool, choice.hook),
@@ -8756,8 +9075,8 @@ async function applySetup(choice, dryRun) {
8756
9075
  });
8757
9076
  if (!dryRun && written.after !== null) mark(hubPath, written.after);
8758
9077
  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");
9078
+ const content = await loadAsset(join26(skill, "SKILL.md"));
9079
+ const path = join26(agentHome(), tool.skillsDir, skill, "SKILL.md");
8761
9080
  const out = await writeIfDifferent(path, content, dryRun);
8762
9081
  results.push({
8763
9082
  tool: tool.label,
@@ -8830,6 +9149,23 @@ function globalInstallHint(reason, hooks) {
8830
9149
  return `${hooks ? "The hooks and the guide call" : "The guide tells your agent to run"} \`memgineering\` by name, and ${reason} \u2014
8831
9150
  so nothing reaches it until a global install exists: \`npm i -g memgineering\`. Nothing needs re-running after that.`;
8832
9151
  }
9152
+ async function recordedChoices(detected) {
9153
+ const cfg = await loadConfig();
9154
+ if (!isSetUp(cfg)) return {};
9155
+ const hook = cfg.system.hook ?? await hooksArmed(detected);
9156
+ return { autoUpdate: cfg.system.autoUpdate, ...hook === void 0 ? {} : { hook } };
9157
+ }
9158
+ async function hooksArmed(tools) {
9159
+ const capable = tools.filter((t) => t.hooks && t.hooksFile);
9160
+ if (capable.length === 0) return void 0;
9161
+ for (const tool of capable) {
9162
+ const raw = await readFile25(join26(agentHome(), ...(tool.hooksFile ?? "").split("/")), "utf8").catch(
9163
+ () => ""
9164
+ );
9165
+ if (hooksFor([tool]).some((spec) => raw.includes(spec.signature))) return "on";
9166
+ }
9167
+ return "off";
9168
+ }
8833
9169
  async function resolveMode(opts) {
8834
9170
  if (opts.agent) return { kind: "agent", chosen: true };
8835
9171
  if (opts.human) {
@@ -8863,11 +9199,11 @@ async function detectTools() {
8863
9199
  const home = agentHome();
8864
9200
  const found = [];
8865
9201
  for (const tool of TOOLS) {
8866
- if (await exists2(join25(home, tool.dir))) found.push(tool);
9202
+ if (await exists2(join26(home, tool.dir))) found.push(tool);
8867
9203
  }
8868
9204
  return found;
8869
9205
  }
8870
- function fromFlags(detected, opts) {
9206
+ function fromFlags(detected, opts, recorded = {}) {
8871
9207
  const tools = opts.tools ? opts.tools.split(",").map((t) => t.trim().toLowerCase()).map((id) => {
8872
9208
  const tool = detected.find((t) => t.id === id);
8873
9209
  if (tool) return tool;
@@ -8886,14 +9222,16 @@ function fromFlags(detected, opts) {
8886
9222
  `${known.label} creates ~/${known.dir} the first time it runs. If it is installed, open it once, then run setup again. ${detectedList}`
8887
9223
  );
8888
9224
  }) : detected;
9225
+ const hookAnswer = parseChoice(opts.hook, recorded.hook ?? "on", "--hook");
8889
9226
  return {
8890
9227
  tools,
8891
- autoUpdate: parseChoice(opts.autoUpdate, "on", "--auto-update"),
9228
+ autoUpdate: parseChoice(opts.autoUpdate, recorded.autoUpdate ?? "on", "--auto-update"),
8892
9229
  errorTelemetry: opts.errorTelemetry === void 0 ? "undecided" : parseChoice(opts.errorTelemetry, "off", "--error-telemetry"),
8893
9230
  // Only Claude Code can run something at session start. For the others the
8894
9231
  // skill tells the agent to call resurface itself, which is why asking for
8895
9232
  // 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)
9233
+ hook: hookAnswer === "on" && tools.some((t) => t.hooks),
9234
+ hookAnswer
8897
9235
  };
8898
9236
  }
8899
9237
  function parseChoice(value, fallback, flag2) {
@@ -8952,17 +9290,19 @@ async function askHuman(detected, opts) {
8952
9290
  autoUpdate: answers.autoUpdate === false ? "off" : parseChoice(opts.autoUpdate, "on", "--auto-update"),
8953
9291
  // An aborted prompt is not an answer — undecided, never a silent yes.
8954
9292
  errorTelemetry: answers.errorTelemetry === void 0 ? "undecided" : answers.errorTelemetry ? "on" : "off",
8955
- hook: answers.hook !== false
9293
+ hook: answers.hook !== false,
9294
+ // Only a question that was put is an answer to record.
9295
+ ...answers.hook === void 0 ? {} : { hookAnswer: answers.hook ? "on" : "off" }
8956
9296
  };
8957
9297
  }
8958
9298
  async function injectBlock(filePath, content, dryRun, weWroteThis) {
8959
9299
  const block = `${BRAND.hubMarkerStart}
8960
9300
  ${content.trimEnd()}
8961
9301
  ${BRAND.hubMarkerEnd}`;
8962
- const existing = await readFile24(filePath, "utf8").catch(() => null);
9302
+ const existing = await readFile25(filePath, "utf8").catch(() => null);
8963
9303
  if (existing === null) {
8964
9304
  if (!dryRun) {
8965
- await mkdir15(dirname10(filePath), { recursive: true });
9305
+ await mkdir16(dirname10(filePath), { recursive: true });
8966
9306
  await writeThenRename(filePath, `${block}
8967
9307
  `);
8968
9308
  }
@@ -9002,11 +9342,11 @@ function looksLikeOurGuide(text) {
9002
9342
  return text.includes("memgineering recall") && text.includes("memgineering remember");
9003
9343
  }
9004
9344
  async function writeIfDifferent(filePath, content, dryRun) {
9005
- const existing = await readFile24(filePath, "utf8").catch(() => null);
9345
+ const existing = await readFile25(filePath, "utf8").catch(() => null);
9006
9346
  const after = fingerprint(content);
9007
9347
  if (existing === content) return { outcome: "unchanged", before: existing, after };
9008
9348
  if (!dryRun) {
9009
- await mkdir15(dirname10(filePath), { recursive: true });
9349
+ await mkdir16(dirname10(filePath), { recursive: true });
9010
9350
  await writeThenRename(filePath, content);
9011
9351
  }
9012
9352
  return { outcome: existing === null ? "injected" : "updated", before: existing, after };
@@ -9017,8 +9357,8 @@ function hooksFor(tools) {
9017
9357
  );
9018
9358
  }
9019
9359
  async function installHook(spec, tool, dryRun) {
9020
- const settingsPath = join25(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9021
- const raw = await readFile24(settingsPath, "utf8").catch(() => null);
9360
+ const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9361
+ const raw = await readFile25(settingsPath, "utf8").catch(() => null);
9022
9362
  let settings = {};
9023
9363
  if (raw !== null) {
9024
9364
  try {
@@ -9060,15 +9400,15 @@ async function installHook(spec, tool, dryRun) {
9060
9400
  }
9061
9401
  const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
9062
9402
  if (!dryRun) {
9063
- await mkdir15(dirname10(settingsPath), { recursive: true });
9403
+ await mkdir16(dirname10(settingsPath), { recursive: true });
9064
9404
  await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
9065
9405
  `);
9066
9406
  }
9067
9407
  return raw === null ? "injected" : "updated";
9068
9408
  }
9069
9409
  async function removeHook(spec, tool, dryRun) {
9070
- const settingsPath = join25(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9071
- const raw = await readFile24(settingsPath, "utf8").catch(() => null);
9410
+ const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9411
+ const raw = await readFile25(settingsPath, "utf8").catch(() => null);
9072
9412
  if (raw === null) return "unchanged";
9073
9413
  let parsed;
9074
9414
  try {
@@ -9092,7 +9432,7 @@ async function removeHook(spec, tool, dryRun) {
9092
9432
  return "removed";
9093
9433
  }
9094
9434
  async function loadAsset(relPath) {
9095
- const raw = await readFile24(join25(await assetRoot(), relPath), "utf8");
9435
+ const raw = await readFile25(join26(await assetRoot(), relPath), "utf8");
9096
9436
  if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
9097
9437
  const end = raw.indexOf("\n---", 4);
9098
9438
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -9101,12 +9441,12 @@ async function loadAsset(relPath) {
9101
9441
  }
9102
9442
  async function assetRoot() {
9103
9443
  const here = dirname10(fileURLToPath3(import.meta.url));
9104
- const bundled = join25(here, "..", "assets");
9105
- if (await exists2(join25(bundled, BRAND.hubFileName))) return bundled;
9444
+ const bundled = join26(here, "..", "assets");
9445
+ if (await exists2(join26(bundled, BRAND.hubFileName))) return bundled;
9106
9446
  let dir = here;
9107
9447
  for (; ; ) {
9108
- const candidate = join25(dir, "packages", "skills");
9109
- if (await exists2(join25(candidate, BRAND.hubFileName))) return candidate;
9448
+ const candidate = join26(dir, "packages", "skills");
9449
+ if (await exists2(join26(candidate, BRAND.hubFileName))) return candidate;
9110
9450
  const parent = dirname10(dir);
9111
9451
  if (parent === dir) break;
9112
9452
  dir = parent;
@@ -9136,6 +9476,7 @@ var init_setup = __esm({
9136
9476
  init_setup_brain();
9137
9477
  init_setup_signin();
9138
9478
  init_config();
9479
+ init_next_step();
9139
9480
  init_provenance();
9140
9481
  init_ui();
9141
9482
  init_version();
@@ -13040,34 +13381,21 @@ var BASE_FILES = [
13040
13381
  ];
13041
13382
  var HUB_PATH = "00_HUB/HUB.md";
13042
13383
  var TEMPLATE_SLOTS = 2;
13043
- function capTemplates(cards, isTemplate) {
13384
+ function capTemplates(cards, isTemplate, inConflict = () => false) {
13044
13385
  const kept = [];
13045
13386
  const withheld = [];
13046
13387
  let templates = 0;
13047
13388
  for (const card of cards) {
13048
- if (isTemplate(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
13389
+ if (isTemplate(card) && !inConflict(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
13049
13390
  else kept.push(card);
13050
13391
  }
13051
13392
  return { kept, withheld };
13052
13393
  }
13053
- function bodyOf2(content) {
13054
- const stripped = content.replace(/^/, "");
13055
- const fence2 = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
13056
- return (fence2 ? stripped.slice(fence2[0].length) : stripped).trim();
13057
- }
13058
- function templatesFor(path, current) {
13059
- const retired = ownEntry(RETIRED_BASE_TEMPLATES, path) ?? [];
13060
- return current === void 0 ? retired : [current, ...retired];
13061
- }
13062
13394
  function isBaseFilePath(path) {
13063
13395
  return path.startsWith("01_BASE/");
13064
13396
  }
13065
13397
  function bodyStillTemplate(path, content) {
13066
- const template = ownEntry(files("brain"), path);
13067
- if (template === void 0) return false;
13068
- const body = bodyOf2(content);
13069
- if (body === "") return false;
13070
- return templatesFor(path, template).some((t) => body === bodyOf2(t));
13398
+ return isTemplateBody(path, content);
13071
13399
  }
13072
13400
  async function scaffoldingAmong(adapter, notes) {
13073
13401
  const basePaths = new Set(BASE_FILES.map((f) => f.path));
@@ -13082,19 +13410,18 @@ async function scaffoldingAmong(adapter, notes) {
13082
13410
  async function untouchedHub(adapter, title) {
13083
13411
  const actual = await adapter.read(HUB_PATH).then((r) => r.content).catch(() => null);
13084
13412
  if (actual === null) return false;
13085
- return actual.trimEnd() === (files(title)[HUB_PATH] ?? "").trimEnd();
13413
+ return asSaved2(actual) === asSaved2(files(title)[HUB_PATH] ?? "");
13414
+ }
13415
+ function asSaved2(text) {
13416
+ const BOM3 = String.fromCharCode(65279);
13417
+ return (text.startsWith(BOM3) ? text.slice(BOM3.length) : text).replace(/\r\n/g, "\n").trimEnd();
13086
13418
  }
13087
13419
  async function untouchedBase(adapter, paths) {
13088
- const templates = files("brain");
13089
13420
  const out = /* @__PURE__ */ new Set();
13090
13421
  await Promise.all(
13091
13422
  paths.map(async (path) => {
13092
- const template = ownEntry(templates, path);
13093
- if (template === void 0) return;
13094
13423
  const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
13095
- if (actual === null) return;
13096
- const trimmed = actual.trimEnd();
13097
- if (templatesFor(path, template).some((t) => trimmed === t.trimEnd())) out.add(path);
13424
+ if (actual !== null && isExactUntouchedBase(path, actual)) out.add(path);
13098
13425
  })
13099
13426
  );
13100
13427
  return out;
@@ -18239,7 +18566,7 @@ function onboardCommand() {
18239
18566
  ...survey.missing.length === 0 ? {} : { missing: survey.missing, missing_hint: MISSING_HINT },
18240
18567
  // Spelled out in the machine-readable form too, because an agent
18241
18568
  // parsing this is the one that has to do the asking.
18242
- how_to_write: 'Write the body with your own file tools, then `memgineering revise <path> --action reinforce --claim "<one line>" --summary "<what recall should show>"`. This is the one place editing a brain file directly is right, and only while it is still the template.'
18569
+ how_to_write: 'Write the body with your own file tools, then `memgineering revise <path> --action reinforce --claim "<one line>" --summary "<what recall should show>"`. This is the one place editing a brain file directly is right, and only while its body has not been written yet.'
18243
18570
  },
18244
18571
  human: () => {
18245
18572
  if (survey.unfilled.length === 0 && survey.missing.length === 0) {
@@ -18253,7 +18580,7 @@ function onboardCommand() {
18253
18580
  );
18254
18581
  return;
18255
18582
  }
18256
- printHuman(`${survey.heading} (${survey.unfilled.length} of 5 still untouched)
18583
+ printHuman(`${survey.heading} (${survey.unfilled.length} of 5 still to write)
18257
18584
  `);
18258
18585
  if (survey.unfilled.length > 0) {
18259
18586
  printHuman(
@@ -18294,21 +18621,23 @@ function onboardCommand() {
18294
18621
  var MISSING_HINT = "A note that does not exist cannot be revised into \u2014 `revise` answers `ref_not_found`. `memgineering create <name>` writes the base notes into a brain that holds nothing but its own scaffolding. A brain that already holds other notes takes them from a folder: `memgineering init <folder>` then `memgineering push --brain <name>`, which skips every path already up there.";
18295
18622
  async function localSurvey(where) {
18296
18623
  const adapter = await openBrainOrExplain(where.brain.root);
18297
- const untouched = await untouchedBase(
18298
- adapter,
18299
- BASE_FILES.map((f) => f.path)
18624
+ const unwritten = /* @__PURE__ */ new Set();
18625
+ await Promise.all(
18626
+ BASE_FILES.map(async (f) => {
18627
+ const content = await adapter.read(f.path).then((r) => r.content).catch(() => null);
18628
+ if (content !== null && isUnwrittenBody(f.path, content)) unwritten.add(f.path);
18629
+ })
18300
18630
  );
18301
18631
  return {
18302
18632
  identity: { brain: where.brain.root },
18303
18633
  heading: "## onboard",
18304
- unfilled: BASE_FILES.filter((f) => untouched.has(f.path)),
18305
- // A file that is not there reads as written here, and that is the shipped
18306
- // behaviour rather than a decision taken now: `untouchedBase` cannot tell
18307
- // "somebody wrote in it" from "it is gone", because both fail the
18308
- // byte-equality test (`lib/base-files.ts`). `init` writes all five, so the
18309
- // state does not arise on a folder brain. The hosted half CAN arrive
18310
- // without them, which is why it answers the question separately.
18311
- filled: BASE_FILES.filter((f) => !untouched.has(f.path)).map((f) => f.path),
18634
+ unfilled: BASE_FILES.filter((f) => unwritten.has(f.path)),
18635
+ // A file that is not there reads as written here. That is a choice kept
18636
+ // from before, not something this side cannot tell: `init` writes all
18637
+ // five, so the state does not arise on a folder brain, and asking about a
18638
+ // file that is gone would send an agent to revise a ref that does not
18639
+ // exist. The hosted half CAN arrive without them, and says so (`missing`).
18640
+ filled: BASE_FILES.filter((f) => !unwritten.has(f.path)).map((f) => f.path),
18312
18641
  missing: []
18313
18642
  };
18314
18643
  }
@@ -18317,7 +18646,7 @@ async function hostedSurvey(where) {
18317
18646
  const state = (file) => {
18318
18647
  const content = contents.get(file.path);
18319
18648
  if (content === void 0) return "missing";
18320
- return isUntouchedBase(file.path, content) ? "unfilled" : "filled";
18649
+ return isUnwrittenBody(file.path, content) ? "unfilled" : "filled";
18321
18650
  };
18322
18651
  return {
18323
18652
  identity: { brain: where.name, brain_id: where.brainId },
@@ -22198,7 +22527,8 @@ function recallCommand() {
22198
22527
  })
22199
22528
  );
22200
22529
  isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
22201
- const capped2 = capTemplates(local.memory_cards, isScaffolding);
22530
+ const sides = new Set(local.conflicts.flatMap((conflict) => conflict.between));
22531
+ const capped2 = capTemplates(local.memory_cards, isScaffolding, (card) => sides.has(card.id));
22202
22532
  templatesWithheld = capped2.withheld.length;
22203
22533
  result = { ...local, memory_cards: capped2.kept };
22204
22534
  entries = built.entries;
@@ -22393,7 +22723,7 @@ ${unsearchedLocal.map(
22393
22723
  if (templatesWithheld > 0) {
22394
22724
  printHuman(
22395
22725
  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.
22726
+ ` ${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
22727
  `
22398
22728
  )
22399
22729
  );
@@ -23024,205 +23354,14 @@ init_setup();
23024
23354
  init_brand();
23025
23355
  init_version();
23026
23356
  init_config();
23027
- import { spawn as spawn3 } from "child_process";
23357
+ init_next_step();
23358
+ init_provenance();
23359
+ import { spawn as spawn4 } from "child_process";
23028
23360
  import { randomBytes as randomBytes2 } from "crypto";
23029
- import { mkdir as mkdir18, open, readFile as readFile27, stat as stat7, unlink as unlink4 } from "fs/promises";
23361
+ 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
23362
  import { join as join28 } from "path";
23031
23363
  import { Command as Command41 } from "commander";
23032
23364
 
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
23365
  // src/lib/update-check.ts
23227
23366
  init_atomic();
23228
23367
  init_brand();
@@ -23432,6 +23571,18 @@ async function runUpdate(opts, inherited) {
23432
23571
  return;
23433
23572
  }
23434
23573
  const held = await holdUpdateSlot(inherited);
23574
+ if (held === null && await blockedBy() === "stuck") {
23575
+ const lock = join28(brandHome(), RECLAIM_LOCK);
23576
+ 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.`;
23577
+ printDual({
23578
+ json: { ...status, provenance: provenance.kind, updated: false, reason: "update_claim_stuck", hint },
23579
+ human: () => {
23580
+ printHuman(`memgineering ${status.latest} is available, but a previous update's leftover claim could not be cleared.`);
23581
+ printHuman(c.gray(hint));
23582
+ }
23583
+ });
23584
+ return;
23585
+ }
23435
23586
  if (held === null) {
23436
23587
  const started = (await claimOwner())?.split(" ")[0];
23437
23588
  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 +23638,10 @@ async function backgroundUpdateNotice(deps = {}) {
23487
23638
  const status = await (deps.check ?? checkForUpdate)(VERSION);
23488
23639
  await reportPreviousAttempt();
23489
23640
  if (status.updateAvailable && cfg.system.autoUpdate === "on" && isSetUp(cfg) && !process.env["CI"]) {
23490
- const provenance = await (deps.provenance ?? detectProvenance)();
23641
+ const provenance = await (deps.provenance ?? (() => detectProvenance(void 0, { deadlineMs: BACKGROUND_PROBE_DEADLINE_MS })))();
23491
23642
  const token = newClaimToken();
23492
23643
  if (canSelfUpdate(provenance) && await claimUpdateSlot(token)) {
23493
- (deps.startUpdate ?? ((t) => startDetachedUpdate(spawn3, t)))(token);
23644
+ (deps.startUpdate ?? ((t) => startDetachedUpdate(spawn4, t)))(token);
23494
23645
  notice(
23495
23646
  c.gray(
23496
23647
  `memgineering ${status.latest} is available \u2014 installing it in the background (auto-update is on). The next run uses it.`
@@ -23513,7 +23664,7 @@ async function backgroundUpdateNotice(deps = {}) {
23513
23664
  } catch {
23514
23665
  }
23515
23666
  }
23516
- function startDetachedUpdate(spawnImpl = spawn3, token) {
23667
+ function startDetachedUpdate(spawnImpl = spawn4, token) {
23517
23668
  const child = spawnImpl(process.execPath, [process.argv[1] ?? "", "update"], {
23518
23669
  detached: true,
23519
23670
  stdio: "ignore",
@@ -23554,7 +23705,7 @@ async function holdUpdateSlot(inherited) {
23554
23705
  }
23555
23706
  var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
23556
23707
  var UPDATE_CLAIM_MS = 15 * 6e4;
23557
- async function claimUpdateSlot(token = newClaimToken()) {
23708
+ async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
23558
23709
  const path = join28(brandHome(), "update-running");
23559
23710
  try {
23560
23711
  await mkdir18(brandHome(), { recursive: true });
@@ -23567,17 +23718,86 @@ async function claimUpdateSlot(token = newClaimToken()) {
23567
23718
  if (err?.code !== "EEXIST") return false;
23568
23719
  }
23569
23720
  try {
23570
- if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS) await unlink4(path);
23721
+ if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS) {
23722
+ await hooks.afterObserve?.();
23723
+ await withReclaimLock(async () => {
23724
+ if (Date.now() - (await stat7(path)).mtimeMs < UPDATE_CLAIM_MS) return;
23725
+ await hooks.afterRecheck?.();
23726
+ await unlink4(path);
23727
+ });
23728
+ }
23571
23729
  } catch {
23572
23730
  }
23573
23731
  return false;
23574
23732
  }
23733
+ var RECLAIM_LOCK = "update-running.reclaim";
23734
+ var RECLAIM_LOCK_MAX_MS = 10 * 6e4;
23735
+ async function withReclaimLock(fn) {
23736
+ const home = brandHome();
23737
+ const lock = join28(home, RECLAIM_LOCK);
23738
+ const staging = join28(home, `${RECLAIM_LOCK}.${process.pid}.${randomBytes2(4).toString("hex")}`);
23739
+ await mkdir18(staging, { recursive: true });
23740
+ await writeFile15(join28(staging, "owner"), `${process.pid} ${Date.now()}
23741
+ `);
23742
+ try {
23743
+ await rename4(staging, lock);
23744
+ } catch {
23745
+ await rm8(staging, { recursive: true, force: true }).catch(() => void 0);
23746
+ await breakAbandonedLock(lock);
23747
+ return null;
23748
+ }
23749
+ try {
23750
+ return await fn();
23751
+ } finally {
23752
+ const aside = `${lock}.done.${randomBytes2(4).toString("hex")}`;
23753
+ await rename4(lock, aside).then(() => rm8(aside, { recursive: true, force: true })).catch(() => void 0);
23754
+ }
23755
+ }
23756
+ async function breakAbandonedLock(lock) {
23757
+ try {
23758
+ const seen = (await readFile27(join28(lock, "owner"), "utf8")).trim();
23759
+ if (!lockAbandoned(seen)) return;
23760
+ const aside = `${lock}.dead.${randomBytes2(4).toString("hex")}`;
23761
+ await rename4(lock, aside);
23762
+ const moved = (await readFile27(join28(aside, "owner"), "utf8").catch(() => "")).trim();
23763
+ if (moved !== seen) {
23764
+ await rename4(aside, lock).catch(() => void 0);
23765
+ return;
23766
+ }
23767
+ await rm8(aside, { recursive: true, force: true });
23768
+ } catch {
23769
+ }
23770
+ }
23771
+ function lockAbandoned(owner, now = Date.now()) {
23772
+ const [pidText, atText] = owner.split(" ");
23773
+ const pid = Number(pidText);
23774
+ const at = Number(atText);
23775
+ if (!Number.isFinite(at) || now - at > RECLAIM_LOCK_MAX_MS) return true;
23776
+ if (!Number.isInteger(pid) || pid <= 0) return true;
23777
+ try {
23778
+ process.kill(pid, 0);
23779
+ return false;
23780
+ } catch (err) {
23781
+ return err?.code !== "EPERM";
23782
+ }
23783
+ }
23784
+ async function blockedBy() {
23785
+ try {
23786
+ const old = Date.now() - (await stat7(join28(brandHome(), "update-running"))).mtimeMs >= UPDATE_CLAIM_MS;
23787
+ if (old) {
23788
+ await stat7(join28(brandHome(), RECLAIM_LOCK));
23789
+ return "stuck";
23790
+ }
23791
+ } catch {
23792
+ }
23793
+ return "in_progress";
23794
+ }
23575
23795
  async function reportPreviousAttempt() {
23576
23796
  const previous = await readUpdateState();
23577
23797
  if (!previous || previous.ok) return;
23578
23798
  notice(
23579
23799
  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."
23800
+ "\n `memgineering update` shows why. `memgineering setup --agent --auto-update off` stops\n these attempts; the hooks stay as this machine recorded them."
23581
23801
  )
23582
23802
  );
23583
23803
  }
@@ -23597,7 +23817,7 @@ function reportBelowMinimum(below) {
23597
23817
 
23598
23818
  // src/commands/undo.ts
23599
23819
  init_src();
23600
- import { readFile as readFile28, rm as rm8 } from "fs/promises";
23820
+ import { readFile as readFile28, rm as rm9 } from "fs/promises";
23601
23821
  import { join as join29 } from "path";
23602
23822
  import { Command as Command42 } from "commander";
23603
23823
  init_atomic();
@@ -23767,7 +23987,7 @@ async function reverse(brain, op, current) {
23767
23987
  "Nothing to take back. `memgineering log` shows what this brain recorded."
23768
23988
  );
23769
23989
  }
23770
- await rm8(absolute, { force: true });
23990
+ await rm9(absolute, { force: true });
23771
23991
  return null;
23772
23992
  }
23773
23993
  if (op.verb === "exclude" || op.verb === "unexclude") {
@@ -24301,6 +24521,7 @@ function whoamiCommand() {
24301
24521
 
24302
24522
  // src/program.ts
24303
24523
  init_config();
24524
+ init_next_step();
24304
24525
  init_ui();
24305
24526
  init_brand();
24306
24527
 
@@ -24491,8 +24712,14 @@ async function run3(argv) {
24491
24712
  process.stderr.isTTY === true ? "person" : cleaned[2] === void 0 ? "full" : "brief"
24492
24713
  );
24493
24714
  }
24715
+ let ran;
24716
+ program.hook("preAction", (_root, action) => {
24717
+ let command = action;
24718
+ while (command.parent && command.parent !== program) command = command.parent;
24719
+ ran = command === program ? void 0 : command.name();
24720
+ });
24494
24721
  await program.parseAsync(cleaned);
24495
- if (argv[2] !== "update") await backgroundUpdateNotice(argv[2] === void 0 ? {} : { verb: argv[2] });
24722
+ if (ran !== "update") await backgroundUpdateNotice(ran === void 0 ? {} : { verb: ran });
24496
24723
  }
24497
24724
  async function handleError(err) {
24498
24725
  const failure = adaptRegistryError(err) ?? err;