memgineering 0.25.0 → 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/dist/index.js CHANGED
@@ -1341,8 +1341,8 @@ var init_exclude = __esm({
1341
1341
  [...ALWAYS_SKIP_DIRS].map((d) => fold(d))
1342
1342
  );
1343
1343
  ALWAYS_SKIP_SEGMENTS_FOLDED = ALWAYS_SKIP_SEGMENTS.map((s) => fold(s));
1344
- ALWAYS_SKIP_FILE = (basename3) => {
1345
- const b = fold(basename3);
1344
+ ALWAYS_SKIP_FILE = (basename4) => {
1345
+ const b = fold(basename4);
1346
1346
  return b.startsWith(".env") || b === ".ds_store" || b === ".memgdeny" || b === ".memgallow" || b === ".memgignore";
1347
1347
  };
1348
1348
  EMPTY_IGNORE_SET = { rules: [], unsupported: [] };
@@ -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"() {
@@ -4030,6 +4037,7 @@ function saysNothingNew(entry, input, normalize) {
4030
4037
  if (norm(claim.text) !== norm(previousClaim(entry))) return false;
4031
4038
  if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
4032
4039
  if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
4040
+ if (claim.binding != null && claim.binding !== (entry.memory.binding === true)) return false;
4033
4041
  const declared = new Set(
4034
4042
  entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
4035
4043
  );
@@ -4904,6 +4912,7 @@ var init_src = __esm({
4904
4912
  init_planner();
4905
4913
  init_semantic();
4906
4914
  init_unfilled();
4915
+ init_setup_progress();
4907
4916
  init_query_prefix();
4908
4917
  init_base_templates();
4909
4918
  init_scope_spread();
@@ -5137,7 +5146,7 @@ async function apiRequest(args) {
5137
5146
  if (cause.retryable && attempts < MAX_CONNECT_RETRIES) {
5138
5147
  const backoff = args.backoffMs ?? CONNECT_BACKOFF_MS;
5139
5148
  const wait = backoff[Math.min(attempts, backoff.length - 1)] ?? 0;
5140
- await new Promise((resolve16) => setTimeout(resolve16, wait));
5149
+ await new Promise((resolve17) => setTimeout(resolve17, wait));
5141
5150
  return apiRequest({ ...args, connectRetries: attempts + 1 });
5142
5151
  }
5143
5152
  throw memgError(
@@ -5161,7 +5170,7 @@ async function apiRequest(args) {
5161
5170
  if (res.status === 429 && (args.retries ?? MAX_RATE_LIMIT_RETRIES) > 0) {
5162
5171
  const wait = retryAfterMs(res.headers.get("retry-after"));
5163
5172
  args.onWait?.(wait);
5164
- await new Promise((resolve16) => setTimeout(resolve16, wait));
5173
+ await new Promise((resolve17) => setTimeout(resolve17, wait));
5165
5174
  return apiRequest({ ...args, retries: (args.retries ?? MAX_RATE_LIMIT_RETRIES) - 1 });
5166
5175
  }
5167
5176
  const envelope = parsed?.error;
@@ -5348,6 +5357,25 @@ function findBrain(cfg, root) {
5348
5357
  const target = resolve(root);
5349
5358
  return cfg.brain.brains.find((b) => resolve(b.root) === target);
5350
5359
  }
5360
+ async function clearSetupFollowUps(onlyPath) {
5361
+ try {
5362
+ const cfg = await loadConfig();
5363
+ const pending = cfg.system.pendingUpload;
5364
+ const dropUpload = pending !== void 0 && (onlyPath === void 0 || resolve(pending.path) === resolve(onlyPath));
5365
+ const dropConnected = onlyPath === void 0 && cfg.system.setupConnected !== void 0;
5366
+ if (!dropUpload && !dropConnected) return;
5367
+ const { pendingUpload, setupConnected, ...rest } = cfg.system;
5368
+ await saveConfig({
5369
+ ...cfg,
5370
+ system: {
5371
+ ...rest,
5372
+ ...dropUpload ? {} : pendingUpload ? { pendingUpload } : {},
5373
+ ...dropConnected ? {} : setupConnected ? { setupConnected } : {}
5374
+ }
5375
+ });
5376
+ } catch {
5377
+ }
5378
+ }
5351
5379
  var BrainSchema, ConfigSchema, EMPTY_CONFIG;
5352
5380
  var init_config = __esm({
5353
5381
  "src/lib/config.ts"() {
@@ -5486,7 +5514,38 @@ var init_config = __esm({
5486
5514
  * Absent means "written before this field existed", which is the same
5487
5515
  * answer as "older than this CLI" and wants the same one command.
5488
5516
  */
5489
- guidanceVersion: z8.string().optional()
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),
5525
+ /**
5526
+ * Notes `setup --web` was asked to upload, not yet sent.
5527
+ *
5528
+ * The detached half of the setup never uploads on its own (security
5529
+ * audit H-3): whoever confirmed the setup decided which account receives
5530
+ * the notes, and that is checked by somebody on THIS machine first. The
5531
+ * next command's notice names the path and the receiving account; a
5532
+ * `push` of that path, or `logout`, clears it.
5533
+ */
5534
+ pendingUpload: z8.object({
5535
+ path: z8.string().min(1),
5536
+ brain: z8.string().nullable(),
5537
+ /** The notice names the brain by id only: its name came from the screen. */
5538
+ brain_id: z8.string().nullable().optional(),
5539
+ account_id: z8.string().min(1),
5540
+ account_email: z8.string().nullable(),
5541
+ at: z8.string().min(1)
5542
+ }).optional().catch(void 0),
5543
+ /**
5544
+ * The account a `setup --web` signed this machine into, when it had no
5545
+ * sign-in before — shown once by the next command, because there was no
5546
+ * earlier account to compare it with (H-3).
5547
+ */
5548
+ setupConnected: z8.object({ account_id: z8.string().min(1), email: z8.string().nullable(), at: z8.string().min(1) }).optional().catch(void 0)
5490
5549
  }).default({ autoUpdate: "on" })
5491
5550
  });
5492
5551
  EMPTY_CONFIG = ConfigSchema.parse({});
@@ -5503,6 +5562,10 @@ async function openCloudBrain(target) {
5503
5562
  apiUrl: target.apiUrl
5504
5563
  };
5505
5564
  }
5565
+ function connectorLabel(writtenBy) {
5566
+ const app = writtenBy.replace(/^mcp:/, "").replace(/[^A-Za-z0-9 ._-]/g, "").replace(/\s+/g, " ").trim().slice(0, 32).trim();
5567
+ return app === "" || app.toLowerCase() === "connector" ? "written by a chat connector" : `written by the ${app} connector`;
5568
+ }
5506
5569
  function findCloudBrain(brains, named) {
5507
5570
  return brains.find((brain) => brain.id === named) ?? brains.find((brain) => brain.name === named);
5508
5571
  }
@@ -7054,12 +7117,12 @@ var init_deadline = __esm({
7054
7117
  import { spawn } from "child_process";
7055
7118
  async function openBrowser(url) {
7056
7119
  const { file, args } = browserOpener(url);
7057
- return new Promise((resolve16) => {
7120
+ return new Promise((resolve17) => {
7058
7121
  let settled = false;
7059
7122
  const finish = (opened) => {
7060
7123
  if (settled) return;
7061
7124
  settled = true;
7062
- resolve16(opened);
7125
+ resolve17(opened);
7063
7126
  };
7064
7127
  const child = spawn(file, args, { detached: true, stdio: "ignore" });
7065
7128
  child.on("error", () => finish(false));
@@ -7089,7 +7152,7 @@ function loginCommand() {
7089
7152
  "start the sign-in, open the browser, and return immediately \u2014 THE path an agent should use on a user\u2019s behalf; the next command claims the token automatically. Prefer this over the plain form, whose wait for a human click will outlast an agent\u2019s command timeout"
7090
7153
  ).option("--no-browser", "do not try to open a browser \u2014 just print the link").option("--print-url", "print the approval link even when a browser opened").addHelpText(
7091
7154
  "after",
7092
- "\nAgents: run `login --emit-only`, then SHOW the code in your reply. The\napproval page asks the user to check it against what you showed them,\nand they cannot if you never showed it. Do not ask the user to run a\nlogin command themselves."
7155
+ "\nAgents: run `login --emit-only`, then SHOW the code in your reply. The\napproval page asks the user to type it, and they cannot if you never\nshowed it. Do not ask the user to run a login command themselves."
7093
7156
  ).action(async (opts) => {
7094
7157
  const base2 = apiUrl();
7095
7158
  const already = await currentCredentials(base2);
@@ -7140,7 +7203,11 @@ async function startPairing(base2) {
7140
7203
  const res = await apiRequest({
7141
7204
  method: "POST",
7142
7205
  path: "/v1/auth/device/code",
7143
- baseUrl: base2
7206
+ baseUrl: base2,
7207
+ // Display text for the approval screen — "requested from memgineering on
7208
+ // darwin" — so a person can tell their own terminal's request from a link
7209
+ // somebody sent them (security audit H-2). Nothing identifying.
7210
+ body: { platform: process.platform, client: `memgineering/${VERSION}` }
7144
7211
  });
7145
7212
  const { device_code, user_code, verification_url, expires_in } = res.body ?? {};
7146
7213
  if (!device_code || !user_code || !verification_url) {
@@ -7171,10 +7238,10 @@ function emit(pending, ctx) {
7171
7238
  browser_opened: ctx.opened,
7172
7239
  api_url: ctx.base,
7173
7240
  waiting: ctx.blocking,
7174
- next: ctx.blocking ? "Waiting for approval in the browser." : "SHOW user_code to the user in your reply, then run any memgineering command \u2014 the token is claimed automatically once they approve. The approval screen asks THEM to agree to the terms and the privacy policy and to confirm they are 14 or older; you cannot answer that for them, and an account is not created until they do."
7241
+ next: ctx.blocking ? "Waiting for approval in the browser." : "SHOW user_code to the user in your reply \u2014 the approval screen asks them to type it \u2014 then run any memgineering command; the token is claimed automatically once they approve. The approval screen also asks THEM to agree to the terms and the privacy policy and to confirm they are 14 or older; you cannot answer that for them, and an account is not created until they do."
7175
7242
  },
7176
7243
  human: () => {
7177
- printHuman("Show this code to whoever is signing in:\n");
7244
+ printHuman("Show this code to whoever is signing in \u2014 the approval page asks them to type it:\n");
7178
7245
  printHuman(` ${c.bold().yellow(oneLine(pending.user_code, WIDTH.handle))}
7179
7246
  `);
7180
7247
  if (ctx.opened) {
@@ -7267,9 +7334,10 @@ var init_login = __esm({
7267
7334
  init_open_browser();
7268
7335
  init_pending_login();
7269
7336
  init_ui();
7337
+ init_version();
7270
7338
  BLOCKING_TIMEOUT_MS = 10 * 60 * 1e3;
7271
- sleep = (ms) => new Promise((resolve16) => {
7272
- setTimeout(resolve16, ms).unref?.();
7339
+ sleep = (ms) => new Promise((resolve17) => {
7340
+ setTimeout(resolve17, ms).unref?.();
7273
7341
  });
7274
7342
  }
7275
7343
  });
@@ -7380,6 +7448,7 @@ If your notes are in one of those, that rule is why \u2014 nothing here can see
7380
7448
  }
7381
7449
  }
7382
7450
  const repointed = await pointConfigAt(destination);
7451
+ await clearSetupFollowUps(local.root);
7383
7452
  printDual({
7384
7453
  json: {
7385
7454
  from: local.root,
@@ -7810,13 +7879,208 @@ var init_setup_signin = __esm({
7810
7879
  }
7811
7880
  });
7812
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
+
7813
8077
  // src/lib/provenance.ts
7814
- import { execFile } from "child_process";
8078
+ import { execFile, spawn as spawn2 } from "child_process";
7815
8079
  import { realpath as realpath3 } from "fs/promises";
7816
- 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";
7817
8081
  import { fileURLToPath as fileURLToPath2 } from "url";
7818
8082
  import { promisify } from "util";
7819
- async function detectProvenance(selfPath) {
8083
+ async function detectProvenance(selfPath, opts = {}) {
7820
8084
  const entry = selfPath ?? fileURLToPath2(import.meta.url);
7821
8085
  const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
7822
8086
  if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
@@ -7828,9 +8092,11 @@ async function detectProvenance(selfPath) {
7828
8092
  reason: "this is a one-off npx run, not an installed copy"
7829
8093
  };
7830
8094
  }
7831
- for (const manager of MANAGERS) {
7832
- const prefix = await prefixOf(manager.bin, manager.prefixArgs);
7833
- 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;
7834
8100
  if (isInside2(selfDir, manager.moduleRoot(prefix))) {
7835
8101
  return {
7836
8102
  kind: manager.kind,
@@ -7841,6 +8107,15 @@ async function detectProvenance(selfPath) {
7841
8107
  };
7842
8108
  }
7843
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
+ }
7844
8119
  const local = selfDir.includes(`${"node_modules"}`);
7845
8120
  return {
7846
8121
  kind: local ? "local" : "unknown",
@@ -7853,6 +8128,62 @@ async function detectProvenance(selfPath) {
7853
8128
  function canSelfUpdate(p) {
7854
8129
  return p.kind === "npm" || p.kind === "pnpm" || p.kind === "bun";
7855
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
+ }
7856
8187
  async function prefixOf(bin, args) {
7857
8188
  try {
7858
8189
  const { stdout } = await run(bin, args, { timeout: 5e3 });
@@ -7874,7 +8205,7 @@ async function unreachableByName(selfPath) {
7874
8205
  const p = await detectProvenance(entry);
7875
8206
  return p.kind === "local" || p.kind === "npx" ? p.reason : null;
7876
8207
  }
7877
- var run, MANAGERS;
8208
+ var run, MANAGERS, BACKGROUND_PROBE_DEADLINE_MS;
7878
8209
  var init_provenance = __esm({
7879
8210
  "src/lib/provenance.ts"() {
7880
8211
  "use strict";
@@ -7884,7 +8215,7 @@ var init_provenance = __esm({
7884
8215
  kind: "npm",
7885
8216
  bin: "npm",
7886
8217
  prefixArgs: ["prefix", "-g"],
7887
- 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"),
7888
8219
  install: "npm i -g memgineering@latest"
7889
8220
  },
7890
8221
  {
@@ -7903,6 +8234,7 @@ var init_provenance = __esm({
7903
8234
  install: "bun add -g memgineering"
7904
8235
  }
7905
8236
  ];
8237
+ BACKGROUND_PROBE_DEADLINE_MS = 1500;
7906
8238
  }
7907
8239
  });
7908
8240
 
@@ -7911,28 +8243,36 @@ var setup_cloud_exports = {};
7911
8243
  __export(setup_cloud_exports, {
7912
8244
  SETUP_CHILD_ENV: () => SETUP_CHILD_ENV,
7913
8245
  applyAnswers: () => applyAnswers,
8246
+ insideFolder: () => insideFolder,
8247
+ newBrainRoot: () => newBrainRoot,
7914
8248
  readChildToken: () => readChildToken,
7915
8249
  runSetupCloud: () => runSetupCloud,
7916
8250
  runSetupCloudChild: () => runSetupCloudChild,
7917
8251
  waitAndApply: () => waitAndApply
7918
8252
  });
7919
- import { spawn as spawn2 } from "child_process";
8253
+ import { spawn as spawn3 } from "child_process";
8254
+ import { realpathSync as realpathSync2 } from "fs";
7920
8255
  import { readdir as readdir7, stat as stat6 } from "fs/promises";
7921
8256
  import { homedir as homedir5 } from "os";
7922
- import { join as join24 } from "path";
7923
- 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) {
7924
8259
  try {
7925
8260
  await apiRequest({
7926
8261
  method: "POST",
7927
8262
  path: "/v1/setup/session/report",
7928
- 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
+ }
7929
8269
  });
7930
8270
  } catch {
7931
8271
  }
7932
8272
  }
7933
8273
  async function folderCandidates() {
7934
8274
  const home = homedir5();
7935
- 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));
7936
8276
  const found = [];
7937
8277
  const countNotes = async (dir) => {
7938
8278
  let count = 0;
@@ -7941,7 +8281,7 @@ async function folderCandidates() {
7941
8281
  if (entry.name.startsWith(".")) continue;
7942
8282
  if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
7943
8283
  else if (entry.isDirectory()) {
7944
- const inner = await readdir7(join24(dir, entry.name)).catch(() => []);
8284
+ const inner = await readdir7(join25(dir, entry.name)).catch(() => []);
7945
8285
  count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
7946
8286
  }
7947
8287
  if (count > 999) break;
@@ -7963,7 +8303,7 @@ function toolsFromAnswers(answers, detected) {
7963
8303
  const picked = TOOLS.filter((t) => wanted.has(t.id));
7964
8304
  return picked.length > 0 ? picked : [...detected];
7965
8305
  }
7966
- async function applyAnswers(setupToken, claim, detected) {
8306
+ async function applyAnswers(setupToken, claim, detected, guards = { candidates: [], confirmUpload: null }) {
7967
8307
  const answers = claim.answers;
7968
8308
  if (!answers) {
7969
8309
  throw memgError(
@@ -7972,29 +8312,53 @@ async function applyAnswers(setupToken, claim, detected) {
7972
8312
  "Nothing was installed. Run `memgineering setup --web` again; if it repeats, the brain server is answering in a shape this version does not understand."
7973
8313
  );
7974
8314
  }
8315
+ const localRoot = checkLocalPath(answers, guards.candidates);
8316
+ let account = null;
7975
8317
  if (typeof claim.token === "string" && claim.token !== "") {
8318
+ account = claim.account ?? await whoIs(claim.token);
8319
+ const before = await readCredentials();
8320
+ const firstSignIn = before === null || before.api_url !== apiUrl();
8321
+ if (!firstSignIn && (account === null || before.account_id !== account.id)) {
8322
+ throw memgError(
8323
+ "conflict",
8324
+ "this machine is signed in to a different account, so the setup changed nothing",
8325
+ `Signed in here: ${before.display_name ?? before.account_id}. The setup screen was confirmed by: ${account?.email ?? account?.display_name ?? "an unknown account"}. If that second account is yours and you want to switch, run \`memgineering logout\` and then \`memgineering setup --web\` again; if you did not confirm it, someone else did \u2014 nothing was changed.`
8326
+ );
8327
+ }
7976
8328
  const registry = registryFromClaim(claim.registry);
7977
- const me = await apiRequest({
7978
- method: "GET",
7979
- path: "/v1/me",
7980
- token: claim.token
7981
- }).catch(() => null);
7982
8329
  const credentials = {
7983
8330
  api_url: apiUrl(),
7984
8331
  token: claim.token,
7985
- account_id: me?.body?.account?.id ?? "",
7986
- display_name: me?.body?.account?.display_name ?? null,
8332
+ account_id: account?.id ?? "",
8333
+ display_name: account?.display_name ?? null,
7987
8334
  signed_in_at: (/* @__PURE__ */ new Date()).toISOString(),
7988
8335
  ...registry !== void 0 ? { registry } : {}
7989
8336
  };
7990
8337
  await saveCredentials(credentials);
8338
+ if (firstSignIn && account !== null) {
8339
+ const cfg = await loadConfig();
8340
+ await saveConfig({
8341
+ ...cfg,
8342
+ system: {
8343
+ ...cfg.system,
8344
+ setupConnected: {
8345
+ account_id: account.id,
8346
+ email: account.email,
8347
+ at: (/* @__PURE__ */ new Date()).toISOString()
8348
+ }
8349
+ }
8350
+ });
8351
+ }
7991
8352
  }
7992
- 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");
7993
8354
  const choice = {
7994
8355
  tools: toolsFromAnswers(answers, detected),
7995
8356
  autoUpdate: answers.auto_update ? "on" : "off",
7996
8357
  errorTelemetry: answers.error_telemetry === null ? "undecided" : answers.error_telemetry ? "on" : "off",
7997
- 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"
7998
8362
  };
7999
8363
  await applySetup(choice, false);
8000
8364
  const parts = [];
@@ -8005,7 +8369,7 @@ async function applyAnswers(setupToken, claim, detected) {
8005
8369
  await recordLink(target.adapter, DEFAULT_OBSERVATIONS_DIR);
8006
8370
  parts.push(`linked ${answers.local_path}`);
8007
8371
  } else {
8008
- const made = await createBrain(answers.local_path, "default");
8372
+ const made = await createBrain(localRoot ?? answers.local_path, "default");
8009
8373
  parts.push(`created ${made.root}`);
8010
8374
  }
8011
8375
  }
@@ -8015,16 +8379,82 @@ async function applyAnswers(setupToken, claim, detected) {
8015
8379
  parts.push(`pointed at ${claim.brain.name}`);
8016
8380
  }
8017
8381
  if (answers.location === "cloud" && answers.upload_path) {
8018
- await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.");
8019
- const { pushCommand: pushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
8020
- await pushCommand2().parseAsync(
8021
- ["--vault", answers.upload_path, ...claim.brain?.name ? ["--brain", claim.brain.name] : []],
8022
- { from: "user" }
8023
- );
8024
- parts.push(`uploaded ${answers.upload_path}`);
8382
+ const path = answers.upload_path;
8383
+ if (!guards.candidates.includes(path)) {
8384
+ parts.push(`not uploaded: ${path} was not one of the folders this machine offered`);
8385
+ } else {
8386
+ const email = account?.email ?? null;
8387
+ const yes = guards.confirmUpload ? await guards.confirmUpload({ path, email }) : null;
8388
+ if (yes === true) {
8389
+ await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.", "uploading");
8390
+ const { pushCommand: pushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
8391
+ await pushCommand2().parseAsync(
8392
+ ["--vault", path, ...claim.brain?.id ? ["--brain", claim.brain.id] : []],
8393
+ { from: "user" }
8394
+ );
8395
+ parts.push(`uploaded ${path}`);
8396
+ } else if (yes === null && account !== null) {
8397
+ const cfg = await loadConfig();
8398
+ await saveConfig({
8399
+ ...cfg,
8400
+ system: {
8401
+ ...cfg.system,
8402
+ pendingUpload: {
8403
+ path,
8404
+ brain: claim.brain?.name ?? null,
8405
+ brain_id: claim.brain?.id ?? null,
8406
+ account_id: account.id,
8407
+ account_email: email,
8408
+ at: (/* @__PURE__ */ new Date()).toISOString()
8409
+ }
8410
+ }
8411
+ });
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
+ );
8418
+ parts.push(`upload of ${path} waits for a check on this machine`);
8419
+ } else {
8420
+ parts.push(`not uploaded: ${path}`);
8421
+ }
8422
+ }
8025
8423
  }
8424
+ if (account?.email) parts.push(`signed in as ${account.email}`);
8026
8425
  return { summary: parts.join(" \xB7 ") };
8027
8426
  }
8427
+ async function whoIs(token) {
8428
+ const me = await apiRequest({ method: "GET", path: "/v1/me", token }).catch(() => null);
8429
+ const a = me?.body?.account;
8430
+ return a?.id ? { id: a.id, email: a.email ?? null, display_name: a.display_name ?? null } : null;
8431
+ }
8432
+ function checkLocalPath(answers, candidates) {
8433
+ if (answers.location !== "local" || !answers.local_path) return null;
8434
+ const path = answers.local_path;
8435
+ if (answers.local_mode === "link") {
8436
+ if (!candidates.includes(path)) {
8437
+ throw memgError(
8438
+ "invalid_input",
8439
+ "the folder the setup screen chose to link was not one this machine offered",
8440
+ "Nothing was changed. To use an existing folder, run `memgineering link <folder>` here."
8441
+ );
8442
+ }
8443
+ return null;
8444
+ }
8445
+ return newBrainRoot(path);
8446
+ }
8447
+ function newBrainRoot(path, home = homedir5()) {
8448
+ const root = path === "~" ? home : path.startsWith("~/") ? resolve15(home, path.slice(2)) : resolve15(home, path);
8449
+ if (!insideFolder(root, home)) {
8450
+ throw memgError(
8451
+ "invalid_input",
8452
+ "the setup screen chose a place outside the home folder for the new brain",
8453
+ `Nothing was changed. Run \`memgineering init <folder>\` here to make a brain somewhere under ${home}.`
8454
+ );
8455
+ }
8456
+ return root;
8457
+ }
8028
8458
  async function pollOnce(setupToken) {
8029
8459
  const res = await apiRequest({
8030
8460
  method: "POST",
@@ -8036,6 +8466,7 @@ async function pollOnce(setupToken) {
8036
8466
  }
8037
8467
  async function waitAndApply(setupToken, opts) {
8038
8468
  let sentFolders = false;
8469
+ let offered = [];
8039
8470
  for (; ; ) {
8040
8471
  if (Date.now() > opts.deadline) return "expired";
8041
8472
  let answer = null;
@@ -8049,12 +8480,16 @@ async function waitAndApply(setupToken, opts) {
8049
8480
  if (answer) {
8050
8481
  await report2(setupToken, "applying");
8051
8482
  try {
8052
- const done = await applyAnswers(setupToken, answer, opts.detected);
8053
- await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.");
8483
+ const done = await applyAnswers(setupToken, answer, opts.detected, {
8484
+ candidates: offered,
8485
+ confirmUpload: opts.confirmUpload ?? null
8486
+ });
8487
+ await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.", "done");
8054
8488
  return done;
8055
8489
  } catch (err) {
8056
8490
  const message = err instanceof Error ? err.message : String(err);
8057
- 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);
8058
8493
  throw err;
8059
8494
  }
8060
8495
  }
@@ -8066,8 +8501,9 @@ async function waitAndApply(setupToken, opts) {
8066
8501
  }).catch(() => null);
8067
8502
  if (state?.body?.folders_requested === true) {
8068
8503
  sentFolders = true;
8069
- opts.onProgress?.("\uD3F4\uB354\uB97C \uCC3E\uC544\uBCF4\uB294 \uC911\u2026");
8504
+ opts.onProgress?.("Looking for note folders on this machine\u2026");
8070
8505
  const candidates = await folderCandidates();
8506
+ offered = candidates.map((c2) => c2.path);
8071
8507
  await apiRequest({
8072
8508
  method: "POST",
8073
8509
  path: "/v1/setup/session/folders",
@@ -8076,7 +8512,7 @@ async function waitAndApply(setupToken, opts) {
8076
8512
  });
8077
8513
  }
8078
8514
  }
8079
- await new Promise((resolve16) => setTimeout(resolve16, opts.intervalMs));
8515
+ await new Promise((resolve17) => setTimeout(resolve17, opts.intervalMs));
8080
8516
  }
8081
8517
  }
8082
8518
  async function startSession(detected) {
@@ -8096,7 +8532,7 @@ async function startSession(detected) {
8096
8532
  return body;
8097
8533
  }
8098
8534
  function detachChild(setupToken) {
8099
- const child = spawn2(process.execPath, [process.argv[1] ?? "", "setup", "--web"], {
8535
+ const child = spawn3(process.execPath, [process.argv[1] ?? "", "setup", "--web"], {
8100
8536
  detached: true,
8101
8537
  stdio: ["pipe", "ignore", "ignore"],
8102
8538
  env: { ...process.env, [SETUP_CHILD_ENV]: "1" }
@@ -8115,26 +8551,30 @@ async function runSetupCloud(opts) {
8115
8551
  const session = await startSession(opts.detected);
8116
8552
  const intervalMs = Math.max(1, session.interval ?? 5) * 1e3;
8117
8553
  printHuman(`
8118
- \uC124\uCE58 \uCF54\uB4DC: ${c.bold(session.user_code)}
8554
+ Setup code: ${c.bold(session.user_code)}
8119
8555
  `);
8120
8556
  const opened = opts.printUrl ? false : await openBrowser(session.setup_url);
8121
8557
  const foreground = opts.wait || process.stdout.isTTY === true && process.stderr.isTTY === true;
8122
8558
  printDual({
8123
8559
  json: {
8124
8560
  user_code: session.user_code,
8125
- setup_url: session.setup_url,
8561
+ // The link drives the setup screen — it carries the browser key (H-3).
8562
+ // Once a browser has it, nobody else needs it, so it is not printed.
8563
+ ...opened ? {
8564
+ hint: "The setup screen was opened in the browser. If none appeared, run `memgineering setup --web --print-url` and open the printed link yourself."
8565
+ } : { setup_url: session.setup_url },
8126
8566
  opened,
8127
8567
  waiting: foreground
8128
8568
  },
8129
8569
  human: () => {
8130
- 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.");
8131
8571
  else {
8132
- 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");
8133
8573
  printHuman(` ${session.setup_url}`);
8134
8574
  }
8135
8575
  printHuman(
8136
8576
  c.gray(
8137
- 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."
8138
8578
  )
8139
8579
  );
8140
8580
  }
@@ -8143,14 +8583,16 @@ async function runSetupCloud(opts) {
8143
8583
  detachChild(session.setup_token);
8144
8584
  return;
8145
8585
  }
8586
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !isJsonMode();
8146
8587
  const outcome = await waitAndApply(session.setup_token, {
8147
8588
  intervalMs,
8148
8589
  deadline: Date.now() + CHILD_LIFETIME_MS,
8149
8590
  detected: opts.detected,
8150
- onProgress: (line) => printHuman(c.gray(` ${line}`))
8591
+ onProgress: (line) => printHuman(c.gray(` ${line}`)),
8592
+ confirmUpload: interactive ? askBeforeUpload : null
8151
8593
  });
8152
8594
  if (outcome === "expired") {
8153
- 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.";
8154
8596
  if (isJsonMode()) notice(JSON.stringify({ setup_web: "expired" }));
8155
8597
  else printHuman(c.gray(`
8156
8598
  ${line}`));
@@ -8158,14 +8600,49 @@ ${line}`));
8158
8600
  }
8159
8601
  if (isJsonMode()) notice(JSON.stringify({ setup_web: "applied" }));
8160
8602
  else {
8161
- printHuman(c.green("\n\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694."));
8603
+ printHuman(c.green("\nSetup finished."));
8162
8604
  printHuman(
8163
8605
  c.gray(
8164
- " \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."
8165
8607
  )
8166
8608
  );
8167
8609
  }
8168
8610
  }
8611
+ function insideFolder(target, folder) {
8612
+ const real = (p) => {
8613
+ try {
8614
+ return realpathSync2(p);
8615
+ } catch {
8616
+ return p;
8617
+ }
8618
+ };
8619
+ const root = real(resolve15(folder));
8620
+ let existing = resolve15(target);
8621
+ const rest = [];
8622
+ for (; ; ) {
8623
+ try {
8624
+ realpathSync2(existing);
8625
+ break;
8626
+ } catch {
8627
+ const parent = dirname9(existing);
8628
+ if (parent === existing) break;
8629
+ rest.unshift(basename3(existing));
8630
+ existing = parent;
8631
+ }
8632
+ }
8633
+ const landed = resolve15(real(existing), ...rest);
8634
+ return landed === root || landed.startsWith(root + sep4);
8635
+ }
8636
+ async function askBeforeUpload(q) {
8637
+ const { default: prompts6 } = await import("prompts");
8638
+ const { yes } = await prompts6({
8639
+ type: "confirm",
8640
+ name: "yes",
8641
+ message: `Upload the notes in ${q.path} to the brain on ${q.email ?? "this account"}?`,
8642
+ initial: false
8643
+ });
8644
+ return yes === true;
8645
+ }
8169
8646
  async function runSetupCloudChild(detected) {
8170
8647
  const setupToken = await readChildToken();
8171
8648
  if (setupToken === "") return;
@@ -8182,6 +8659,7 @@ var init_setup_cloud = __esm({
8182
8659
  "use strict";
8183
8660
  init_api_client();
8184
8661
  init_errors();
8662
+ init_config();
8185
8663
  init_credentials();
8186
8664
  init_open_browser();
8187
8665
  init_ui();
@@ -8195,8 +8673,8 @@ var init_setup_cloud = __esm({
8195
8673
 
8196
8674
  // src/commands/setup.ts
8197
8675
  import { createHash as createHash9 } from "crypto";
8198
- import { access as access2, mkdir as mkdir15, readFile as readFile24 } from "fs/promises";
8199
- import { dirname as dirname9, 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";
8200
8678
  import { fileURLToPath as fileURLToPath3 } from "url";
8201
8679
  import { Command as Command40 } from "commander";
8202
8680
  import prompts5 from "prompts";
@@ -8221,10 +8699,11 @@ function setupCommand() {
8221
8699
  "after",
8222
8700
  [
8223
8701
  "",
8224
- "Chat apps with no shell \u2014 ChatGPT, Claude.ai, Claude Desktop, Cowork \u2014 need no",
8225
- `setup: add ${HOSTED_CONNECTOR_URL} as a custom connector in the app and`,
8226
- "sign in. It reads and writes the brain on your account (not a folder on this",
8227
- "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."
8228
8707
  ].join("\n")
8229
8708
  ).action(async (opts) => {
8230
8709
  const detected = await detectTools();
@@ -8259,16 +8738,18 @@ function setupCommand() {
8259
8738
  dryRun: opts.dryRun === true
8260
8739
  });
8261
8740
  const mode = await resolveMode(opts);
8262
- 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);
8263
8743
  const defaultsTaken = !mode.chosen ? [
8264
- ...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"] : [],
8265
8746
  // Every callback this flag arms, named — on Claude Code and Codex
8266
8747
  // that is two (note summaries at session start, the user's own
8267
8748
  // rules before an edit), and saying only the first would be
8268
8749
  // reporting half of what was decided on their behalf. On Grok it
8269
8750
  // is one, at the end of a turn, and naming the other two there
8270
8751
  // reported callbacks that were never installed.
8271
- ...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`) : []
8272
8753
  ] : [];
8273
8754
  if (choice === null) {
8274
8755
  printHuman(c.yellow("Cancelled \u2014 nothing was changed."));
@@ -8534,6 +9015,7 @@ async function applySetup(choice, dryRun) {
8534
9015
  system: {
8535
9016
  ...cfg.system,
8536
9017
  autoUpdate: choice.autoUpdate,
9018
+ ...choice.hookAnswer === void 0 ? {} : { hook: choice.hookAnswer },
8537
9019
  installed,
8538
9020
  // Stamped whether or not anything was rewritten. A run that finds every
8539
9021
  // file already current is exactly the run that must clear the notice —
@@ -8559,7 +9041,7 @@ async function applySetup(choice, dryRun) {
8559
9041
  }
8560
9042
  async function write2() {
8561
9043
  for (const tool of choice.tools) {
8562
- const hubPath = join25(agentHome(), tool.hubFile);
9044
+ const hubPath = join26(agentHome(), tool.hubFile);
8563
9045
  const written = await injectBlock(
8564
9046
  hubPath,
8565
9047
  hub + tailFor(tool, choice.hook),
@@ -8574,8 +9056,8 @@ async function applySetup(choice, dryRun) {
8574
9056
  });
8575
9057
  if (!dryRun && written.after !== null) mark(hubPath, written.after);
8576
9058
  for (const skill of BRAND.subSkills) {
8577
- const content = await loadAsset(join25(skill, "SKILL.md"));
8578
- 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");
8579
9061
  const out = await writeIfDifferent(path, content, dryRun);
8580
9062
  results.push({
8581
9063
  tool: tool.label,
@@ -8648,6 +9130,23 @@ function globalInstallHint(reason, hooks) {
8648
9130
  return `${hooks ? "The hooks and the guide call" : "The guide tells your agent to run"} \`memgineering\` by name, and ${reason} \u2014
8649
9131
  so nothing reaches it until a global install exists: \`npm i -g memgineering\`. Nothing needs re-running after that.`;
8650
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
+ }
8651
9150
  async function resolveMode(opts) {
8652
9151
  if (opts.agent) return { kind: "agent", chosen: true };
8653
9152
  if (opts.human) {
@@ -8681,11 +9180,11 @@ async function detectTools() {
8681
9180
  const home = agentHome();
8682
9181
  const found = [];
8683
9182
  for (const tool of TOOLS) {
8684
- if (await exists2(join25(home, tool.dir))) found.push(tool);
9183
+ if (await exists2(join26(home, tool.dir))) found.push(tool);
8685
9184
  }
8686
9185
  return found;
8687
9186
  }
8688
- function fromFlags(detected, opts) {
9187
+ function fromFlags(detected, opts, recorded = {}) {
8689
9188
  const tools = opts.tools ? opts.tools.split(",").map((t) => t.trim().toLowerCase()).map((id) => {
8690
9189
  const tool = detected.find((t) => t.id === id);
8691
9190
  if (tool) return tool;
@@ -8704,14 +9203,16 @@ function fromFlags(detected, opts) {
8704
9203
  `${known.label} creates ~/${known.dir} the first time it runs. If it is installed, open it once, then run setup again. ${detectedList}`
8705
9204
  );
8706
9205
  }) : detected;
9206
+ const hookAnswer = parseChoice(opts.hook, recorded.hook ?? "on", "--hook");
8707
9207
  return {
8708
9208
  tools,
8709
- autoUpdate: parseChoice(opts.autoUpdate, "on", "--auto-update"),
9209
+ autoUpdate: parseChoice(opts.autoUpdate, recorded.autoUpdate ?? "on", "--auto-update"),
8710
9210
  errorTelemetry: opts.errorTelemetry === void 0 ? "undecided" : parseChoice(opts.errorTelemetry, "off", "--error-telemetry"),
8711
9211
  // Only Claude Code can run something at session start. For the others the
8712
9212
  // skill tells the agent to call resurface itself, which is why asking for
8713
9213
  // a hook there is not an error — it simply has nowhere to go.
8714
- hook: parseChoice(opts.hook, "on", "--hook") === "on" && tools.some((t) => t.hooks)
9214
+ hook: hookAnswer === "on" && tools.some((t) => t.hooks),
9215
+ hookAnswer
8715
9216
  };
8716
9217
  }
8717
9218
  function parseChoice(value, fallback, flag2) {
@@ -8770,17 +9271,19 @@ async function askHuman(detected, opts) {
8770
9271
  autoUpdate: answers.autoUpdate === false ? "off" : parseChoice(opts.autoUpdate, "on", "--auto-update"),
8771
9272
  // An aborted prompt is not an answer — undecided, never a silent yes.
8772
9273
  errorTelemetry: answers.errorTelemetry === void 0 ? "undecided" : answers.errorTelemetry ? "on" : "off",
8773
- 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" }
8774
9277
  };
8775
9278
  }
8776
9279
  async function injectBlock(filePath, content, dryRun, weWroteThis) {
8777
9280
  const block = `${BRAND.hubMarkerStart}
8778
9281
  ${content.trimEnd()}
8779
9282
  ${BRAND.hubMarkerEnd}`;
8780
- const existing = await readFile24(filePath, "utf8").catch(() => null);
9283
+ const existing = await readFile25(filePath, "utf8").catch(() => null);
8781
9284
  if (existing === null) {
8782
9285
  if (!dryRun) {
8783
- await mkdir15(dirname9(filePath), { recursive: true });
9286
+ await mkdir16(dirname10(filePath), { recursive: true });
8784
9287
  await writeThenRename(filePath, `${block}
8785
9288
  `);
8786
9289
  }
@@ -8820,11 +9323,11 @@ function looksLikeOurGuide(text) {
8820
9323
  return text.includes("memgineering recall") && text.includes("memgineering remember");
8821
9324
  }
8822
9325
  async function writeIfDifferent(filePath, content, dryRun) {
8823
- const existing = await readFile24(filePath, "utf8").catch(() => null);
9326
+ const existing = await readFile25(filePath, "utf8").catch(() => null);
8824
9327
  const after = fingerprint(content);
8825
9328
  if (existing === content) return { outcome: "unchanged", before: existing, after };
8826
9329
  if (!dryRun) {
8827
- await mkdir15(dirname9(filePath), { recursive: true });
9330
+ await mkdir16(dirname10(filePath), { recursive: true });
8828
9331
  await writeThenRename(filePath, content);
8829
9332
  }
8830
9333
  return { outcome: existing === null ? "injected" : "updated", before: existing, after };
@@ -8835,8 +9338,8 @@ function hooksFor(tools) {
8835
9338
  );
8836
9339
  }
8837
9340
  async function installHook(spec, tool, dryRun) {
8838
- const settingsPath = join25(agentHome(), ...(tool.hooksFile ?? "").split("/"));
8839
- 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);
8840
9343
  let settings = {};
8841
9344
  if (raw !== null) {
8842
9345
  try {
@@ -8878,15 +9381,15 @@ async function installHook(spec, tool, dryRun) {
8878
9381
  }
8879
9382
  const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
8880
9383
  if (!dryRun) {
8881
- await mkdir15(dirname9(settingsPath), { recursive: true });
9384
+ await mkdir16(dirname10(settingsPath), { recursive: true });
8882
9385
  await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
8883
9386
  `);
8884
9387
  }
8885
9388
  return raw === null ? "injected" : "updated";
8886
9389
  }
8887
9390
  async function removeHook(spec, tool, dryRun) {
8888
- const settingsPath = join25(agentHome(), ...(tool.hooksFile ?? "").split("/"));
8889
- 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);
8890
9393
  if (raw === null) return "unchanged";
8891
9394
  let parsed;
8892
9395
  try {
@@ -8910,7 +9413,7 @@ async function removeHook(spec, tool, dryRun) {
8910
9413
  return "removed";
8911
9414
  }
8912
9415
  async function loadAsset(relPath) {
8913
- const raw = await readFile24(join25(await assetRoot(), relPath), "utf8");
9416
+ const raw = await readFile25(join26(await assetRoot(), relPath), "utf8");
8914
9417
  if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
8915
9418
  const end = raw.indexOf("\n---", 4);
8916
9419
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -8918,14 +9421,14 @@ async function loadAsset(relPath) {
8918
9421
  return raw;
8919
9422
  }
8920
9423
  async function assetRoot() {
8921
- const here = dirname9(fileURLToPath3(import.meta.url));
8922
- const bundled = join25(here, "..", "assets");
8923
- if (await exists2(join25(bundled, BRAND.hubFileName))) return bundled;
9424
+ const here = dirname10(fileURLToPath3(import.meta.url));
9425
+ const bundled = join26(here, "..", "assets");
9426
+ if (await exists2(join26(bundled, BRAND.hubFileName))) return bundled;
8924
9427
  let dir = here;
8925
9428
  for (; ; ) {
8926
- const candidate = join25(dir, "packages", "skills");
8927
- if (await exists2(join25(candidate, BRAND.hubFileName))) return candidate;
8928
- const parent = dirname9(dir);
9429
+ const candidate = join26(dir, "packages", "skills");
9430
+ if (await exists2(join26(candidate, BRAND.hubFileName))) return candidate;
9431
+ const parent = dirname10(dir);
8929
9432
  if (parent === dir) break;
8930
9433
  dir = parent;
8931
9434
  }
@@ -8954,6 +9457,7 @@ var init_setup = __esm({
8954
9457
  init_setup_brain();
8955
9458
  init_setup_signin();
8956
9459
  init_config();
9460
+ init_next_step();
8957
9461
  init_provenance();
8958
9462
  init_ui();
8959
9463
  init_version();
@@ -9312,7 +9816,7 @@ async function reportError(kind, err) {
9312
9816
  headers: { "content-type": "application/json" },
9313
9817
  body: JSON.stringify(payload)
9314
9818
  }),
9315
- new Promise((resolve16) => setTimeout(resolve16, REPORT_TIMEOUT_MS))
9819
+ new Promise((resolve17) => setTimeout(resolve17, REPORT_TIMEOUT_MS))
9316
9820
  ]);
9317
9821
  } catch {
9318
9822
  }
@@ -11003,7 +11507,7 @@ var KordisClient = class {
11003
11507
  `[kordis] api: ${method} ${url.pathname}${url.search} \u2192 fetch failed, retrying in 150ms (${err.message})`
11004
11508
  );
11005
11509
  }
11006
- await new Promise((resolve16) => setTimeout(resolve16, 150));
11510
+ await new Promise((resolve17) => setTimeout(resolve17, 150));
11007
11511
  continue;
11008
11512
  }
11009
11513
  }
@@ -12858,12 +13362,12 @@ var BASE_FILES = [
12858
13362
  ];
12859
13363
  var HUB_PATH = "00_HUB/HUB.md";
12860
13364
  var TEMPLATE_SLOTS = 2;
12861
- function capTemplates(cards, isTemplate) {
13365
+ function capTemplates(cards, isTemplate, inConflict = () => false) {
12862
13366
  const kept = [];
12863
13367
  const withheld = [];
12864
13368
  let templates = 0;
12865
13369
  for (const card of cards) {
12866
- if (isTemplate(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
13370
+ if (isTemplate(card) && !inConflict(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
12867
13371
  else kept.push(card);
12868
13372
  }
12869
13373
  return { kept, withheld };
@@ -14507,11 +15011,11 @@ function lines(text, marker) {
14507
15011
  return text === "" ? [] : text.split("\n").map((l) => `${marker} ${l}`);
14508
15012
  }
14509
15013
  async function readInput(source) {
14510
- const raw = source === "-" ? await new Promise((resolve16, reject) => {
15014
+ const raw = source === "-" ? await new Promise((resolve17, reject) => {
14511
15015
  let buf = "";
14512
15016
  process.stdin.setEncoding("utf8");
14513
15017
  process.stdin.on("data", (chunk) => buf += chunk);
14514
- process.stdin.on("end", () => resolve16(buf));
15018
+ process.stdin.on("end", () => resolve17(buf));
14515
15019
  process.stdin.on("error", reject);
14516
15020
  }) : await readFile16(source, "utf8").catch(() => source);
14517
15021
  try {
@@ -17174,8 +17678,14 @@ function groupHostedLines(rules, project) {
17174
17678
  const width = ruleWidthFor(rules.length);
17175
17679
  const line = (r) => {
17176
17680
  const quoted = renderRuleLine(r.text ?? r.summary ?? r.title, width);
17177
- return r.freshness === "current" || r.freshness === void 0 ? quoted : `${quoted}
17178
- (${r.freshness === "expired" ? "past its end date" : "superseded"} \u2014 worth checking)`;
17681
+ const labels = [
17682
+ ...r.freshness === "current" || r.freshness === void 0 ? [] : [`(${r.freshness === "expired" ? "past its end date" : "superseded"} \u2014 worth checking)`],
17683
+ // Whose words these are, when a chat app's (security audit M-2). The
17684
+ // person promoted it; the agent is owed knowing where it came from.
17685
+ ...r.written_by === void 0 ? [] : [`(${connectorLabel(r.written_by)})`]
17686
+ ];
17687
+ return labels.length === 0 ? quoted : `${quoted}
17688
+ ${labels.join(" ")}`;
17179
17689
  };
17180
17690
  const globals = rules.filter((r) => r.lane === "global");
17181
17691
  const scoped = rules.filter((r) => r.lane === "project");
@@ -17896,6 +18406,7 @@ init_login();
17896
18406
  init_errors();
17897
18407
  init_api_client();
17898
18408
  init_credentials();
18409
+ init_config();
17899
18410
  init_pending_login();
17900
18411
  init_ui();
17901
18412
  import { Command as Command22 } from "commander";
@@ -17907,6 +18418,7 @@ function logoutCommand() {
17907
18418
  const base2 = apiUrl();
17908
18419
  const credentials = await readCredentials();
17909
18420
  await clearPendingLogin();
18421
+ await clearSetupFollowUps();
17910
18422
  if (!credentials) {
17911
18423
  printDual({
17912
18424
  json: { signed_out: true, was_signed_in: false, api_url: base2 },
@@ -22008,7 +22520,8 @@ function recallCommand() {
22008
22520
  })
22009
22521
  );
22010
22522
  isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
22011
- 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));
22012
22525
  templatesWithheld = capped2.withheld.length;
22013
22526
  result = { ...local, memory_cards: capped2.kept };
22014
22527
  entries = built.entries;
@@ -22203,7 +22716,7 @@ ${unsearchedLocal.map(
22203
22716
  if (templatesWithheld > 0) {
22204
22717
  printHuman(
22205
22718
  c.gray(
22206
- ` ${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.
22207
22720
  `
22208
22721
  )
22209
22722
  );
@@ -22716,7 +23229,7 @@ function rulesCommand() {
22716
23229
  const lane = rule.lane === "project" ? oneLine(project2 ?? "this project", WIDTH.title) : "global";
22717
23230
  printHuman(
22718
23231
  c.gray(
22719
- ` \`open: ${oneLine(rule.handle, WIDTH.handle)}\`${rule.lane ? ` \xB7 ${lane}` : ""}`
23232
+ ` \`open: ${oneLine(rule.handle, WIDTH.handle)}\`${rule.lane ? ` \xB7 ${lane}` : ""}${rule.written_by === void 0 ? "" : ` \xB7 ${connectorLabel(rule.written_by)}`}`
22720
23233
  )
22721
23234
  );
22722
23235
  }
@@ -22735,7 +23248,7 @@ ${shownOf - rules2.length} more not shown.`));
22735
23248
  printHuman(`- ${oneLine(rule.summary ?? rule.title, WIDTH.text)}`);
22736
23249
  printHuman(
22737
23250
  c.gray(
22738
- ` \`open: ${oneLine(rule.handle, WIDTH.handle)}\` \xB7 ${oneLine(rule.scope ?? "", WIDTH.title)}`
23251
+ ` \`open: ${oneLine(rule.handle, WIDTH.handle)}\` \xB7 ${oneLine(rule.scope ?? "", WIDTH.title)}${rule.written_by === void 0 ? "" : ` \xB7 ${connectorLabel(rule.written_by)}`}`
22739
23252
  )
22740
23253
  );
22741
23254
  }
@@ -22834,151 +23347,14 @@ init_setup();
22834
23347
  init_brand();
22835
23348
  init_version();
22836
23349
  init_config();
22837
- import { spawn as spawn3 } from "child_process";
23350
+ init_next_step();
23351
+ init_provenance();
23352
+ import { spawn as spawn4 } from "child_process";
22838
23353
  import { randomBytes as randomBytes2 } from "crypto";
22839
- 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";
22840
23355
  import { join as join28 } from "path";
22841
23356
  import { Command as Command41 } from "commander";
22842
23357
 
22843
- // src/lib/next-step.ts
22844
- init_brand();
22845
- init_api_client();
22846
- init_credentials();
22847
- init_config();
22848
- init_version();
22849
- import { mkdir as mkdir16, readFile as readFile25, writeFile as writeFile13 } from "fs/promises";
22850
- import { join as join26 } from "path";
22851
- function isSetUp(cfg) {
22852
- return Object.keys(cfg.system.installed).length > 0;
22853
- }
22854
- var FINISH_SETUP = `## memgineering is installed, but not set up yet.
22855
-
22856
- Nothing is connected: there is no brain to read, and the agent tools on this
22857
- machine have no memgineering guidance yet. Setup is one command:
22858
-
22859
- \`\`\`bash
22860
- memgineering setup --web
22861
- \`\`\`
22862
-
22863
- It does NOT wait for them. It returns in a few seconds \u2014 measured \u2014 and leaves
22864
- a setup screen open in their browser at memgineering.com, where they choose
22865
- everything: signing in, where the memory lives, which notes. Nothing is
22866
- installed until they finish there, and this machine picks it up on its own.
22867
-
22868
- If they have not already asked you to set it up, ask them first \u2014 it opens a
22869
- browser window on their computer. Once you run it, tell them in their language
22870
- that the setup screen is open. It prints a short code first: show it to them,
22871
- because the screen asks them to compare it.
22872
-
22873
- No browser on this machine (a server, a container, a sandbox)? Run
22874
- \`memgineering setup --web --print-url\` and give them the link to open on the
22875
- computer they are actually sitting at.
22876
-
22877
- \`setup --agent\` answers every one of those questions on their behalf, and
22878
- \`--no-login\` answers the biggest one \u2014 no account, so the memory cannot follow
22879
- them to another machine. Use it only when they have said they would rather you
22880
- decided, or when this machine cannot reach a browser and \`--print-url\` is no
22881
- use to them either.
22882
-
22883
- Setup's output then says how to connect their brain \u2014 create one, pick one, or
22884
- move existing notes up.
22885
-
22886
- The \`memgineering-setup\` skill covers what comes after.`;
22887
- 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.";
22888
- async function claimFullSetupNotice() {
22889
- const marker = join26(brandHome(), "setup-notice-shown");
22890
- try {
22891
- await readFile25(marker, "utf8");
22892
- return false;
22893
- } catch {
22894
- }
22895
- try {
22896
- await mkdir16(brandHome(), { recursive: true });
22897
- await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
22898
- `);
22899
- } catch {
22900
- }
22901
- return true;
22902
- }
22903
- var REFRESH_GUIDANCE = `## The guidance in your agent tools is older than this memgineering.
22904
-
22905
- The hub and skills this CLI installs into your tool's home were written by an
22906
- earlier release. One command rewrites them:
22907
-
22908
- \`\`\`bash
22909
- memgineering setup
22910
- \`\`\`
22911
-
22912
- It does not change which brain this machine reads, or any note. Two things it
22913
- does change, both worth knowing before it runs:
22914
-
22915
- - A setup-owned file your user edited is overwritten; setup names it afterwards.
22916
- - Run with no flags where there is no terminal (an agent's shell), it applies
22917
- setup's defaults over whatever they chose before: auto-update on, its hooks
22918
- on, every agent tool it finds \u2014 and on a machine not signed in, it starts a
22919
- sign-in and prints a code. To keep what they chose, pass it:
22920
- \`--auto-update off\`, \`--hook off\`, \`--no-login\` for a machine they keep
22921
- local, and \`--tools\` naming every tool they use (a tool left out keeps its
22922
- old guidance, and this notice stops appearing anyway).
22923
-
22924
- If you are not sure what they chose, ask them. If you run it,
22925
- mention it to your user in one line: it is upkeep for a tool they already set
22926
- up, and they should not have to discover it later.
22927
-
22928
- Guidance files are read at the start of a session, so what changes today
22929
- arrives in the next one.`;
22930
- var CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
22931
-
22932
- A brain is a folder of markdown notes. Until one is connected there is nothing
22933
- to recall and nowhere to remember. Which folder is your user's choice; the
22934
- command is one you can run for them:
22935
-
22936
- \`\`\`bash
22937
- memgineering link <their-notes-folder> # they already keep notes
22938
- memgineering init <path> # they do not \u2014 creates one, laid out
22939
- \`\`\`
22940
-
22941
- \`link\` reports what would be stored before anything is indexed. An existing
22942
- folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
22943
-
22944
- Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
22945
- it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
22946
-
22947
- Read \`memgineering-setup\` for accounts and which of their tools this reaches.
22948
-
22949
- Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
22950
- var POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
22951
-
22952
- \`memgineering brains\` lists what is on the account.
22953
- \`memgineering use --brain <name>\` points this machine at one.
22954
- \`memgineering create <name>\` makes one and points here, when the account has none yet.
22955
- `;
22956
- function nextStepFor(cfg, signedIn = false) {
22957
- if (signedIn && cfg.brain.cloud === void 0) {
22958
- return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
22959
- }
22960
- if (isSetUp(cfg) && cfg.system.guidanceVersion !== VERSION) {
22961
- return { id: "refresh_guidance", markdown: REFRESH_GUIDANCE };
22962
- }
22963
- if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
22964
- if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
22965
- if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
22966
- return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
22967
- }
22968
- return null;
22969
- }
22970
- async function currentNextStep() {
22971
- try {
22972
- const stored = await readCredentials().catch(() => null);
22973
- return nextStepFor(await loadConfig(), stored !== null && stored.api_url === apiUrl());
22974
- } catch {
22975
- return null;
22976
- }
22977
- }
22978
-
22979
- // src/commands/update.ts
22980
- init_provenance();
22981
-
22982
23358
  // src/lib/update-check.ts
22983
23359
  init_atomic();
22984
23360
  init_brand();
@@ -23188,6 +23564,18 @@ async function runUpdate(opts, inherited) {
23188
23564
  return;
23189
23565
  }
23190
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
+ }
23191
23579
  if (held === null) {
23192
23580
  const started = (await claimOwner())?.split(" ")[0];
23193
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.";
@@ -23243,10 +23631,10 @@ async function backgroundUpdateNotice(deps = {}) {
23243
23631
  const status = await (deps.check ?? checkForUpdate)(VERSION);
23244
23632
  await reportPreviousAttempt();
23245
23633
  if (status.updateAvailable && cfg.system.autoUpdate === "on" && isSetUp(cfg) && !process.env["CI"]) {
23246
- const provenance = await (deps.provenance ?? detectProvenance)();
23634
+ const provenance = await (deps.provenance ?? (() => detectProvenance(void 0, { deadlineMs: BACKGROUND_PROBE_DEADLINE_MS })))();
23247
23635
  const token = newClaimToken();
23248
23636
  if (canSelfUpdate(provenance) && await claimUpdateSlot(token)) {
23249
- (deps.startUpdate ?? ((t) => startDetachedUpdate(spawn3, t)))(token);
23637
+ (deps.startUpdate ?? ((t) => startDetachedUpdate(spawn4, t)))(token);
23250
23638
  notice(
23251
23639
  c.gray(
23252
23640
  `memgineering ${status.latest} is available \u2014 installing it in the background (auto-update is on). The next run uses it.`
@@ -23269,7 +23657,7 @@ async function backgroundUpdateNotice(deps = {}) {
23269
23657
  } catch {
23270
23658
  }
23271
23659
  }
23272
- function startDetachedUpdate(spawnImpl = spawn3, token) {
23660
+ function startDetachedUpdate(spawnImpl = spawn4, token) {
23273
23661
  const child = spawnImpl(process.execPath, [process.argv[1] ?? "", "update"], {
23274
23662
  detached: true,
23275
23663
  stdio: "ignore",
@@ -23310,7 +23698,7 @@ async function holdUpdateSlot(inherited) {
23310
23698
  }
23311
23699
  var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
23312
23700
  var UPDATE_CLAIM_MS = 15 * 6e4;
23313
- async function claimUpdateSlot(token = newClaimToken()) {
23701
+ async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
23314
23702
  const path = join28(brandHome(), "update-running");
23315
23703
  try {
23316
23704
  await mkdir18(brandHome(), { recursive: true });
@@ -23323,17 +23711,86 @@ async function claimUpdateSlot(token = newClaimToken()) {
23323
23711
  if (err?.code !== "EEXIST") return false;
23324
23712
  }
23325
23713
  try {
23326
- 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
+ }
23327
23722
  } catch {
23328
23723
  }
23329
23724
  return false;
23330
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
+ }
23331
23788
  async function reportPreviousAttempt() {
23332
23789
  const previous = await readUpdateState();
23333
23790
  if (!previous || previous.ok) return;
23334
23791
  notice(
23335
23792
  c.yellow(`\u26A0 The last update attempt did not complete: ${previous.message}`) + c.gray(
23336
- "\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."
23337
23794
  )
23338
23795
  );
23339
23796
  }
@@ -23353,7 +23810,7 @@ function reportBelowMinimum(below) {
23353
23810
 
23354
23811
  // src/commands/undo.ts
23355
23812
  init_src();
23356
- import { readFile as readFile28, rm as rm8 } from "fs/promises";
23813
+ import { readFile as readFile28, rm as rm9 } from "fs/promises";
23357
23814
  import { join as join29 } from "path";
23358
23815
  import { Command as Command42 } from "commander";
23359
23816
  init_atomic();
@@ -23523,7 +23980,7 @@ async function reverse(brain, op, current) {
23523
23980
  "Nothing to take back. `memgineering log` shows what this brain recorded."
23524
23981
  );
23525
23982
  }
23526
- await rm8(absolute, { force: true });
23983
+ await rm9(absolute, { force: true });
23527
23984
  return null;
23528
23985
  }
23529
23986
  if (op.verb === "exclude" || op.verb === "unexclude") {
@@ -23663,7 +24120,7 @@ init_api_client();
23663
24120
  init_credentials();
23664
24121
  init_cloud_brain();
23665
24122
  init_config();
23666
- import { isAbsolute as isAbsolute5, relative as relative2, resolve as resolve15 } from "path";
24123
+ import { isAbsolute as isAbsolute5, relative as relative2, resolve as resolve16 } from "path";
23667
24124
  import { Command as Command44 } from "commander";
23668
24125
  init_vault();
23669
24126
  init_ui();
@@ -23693,7 +24150,7 @@ function useCommand() {
23693
24150
  await pointAtHosted(opts.brain, opts, brainPath);
23694
24151
  return;
23695
24152
  }
23696
- const where = await canonicalize(resolve15(opts.here ?? process.cwd()));
24153
+ const where = await canonicalize(resolve16(opts.here ?? process.cwd()));
23697
24154
  if (!brainPath) {
23698
24155
  const pointer = await readPointer(where);
23699
24156
  const resolved = await resolveBrain(void 0, where).catch((err) => err);
@@ -23839,7 +24296,7 @@ ${hint}`));
23839
24296
  });
23840
24297
  return;
23841
24298
  }
23842
- const root = await canonicalize(resolve15(brainPath));
24299
+ const root = await canonicalize(resolve16(brainPath));
23843
24300
  const cfg = await loadConfig();
23844
24301
  if (!findBrain(cfg, root)) {
23845
24302
  throw memgError(
@@ -23866,7 +24323,7 @@ A pointer to an unlinked folder would fail on every command instead of at this o
23866
24323
  return;
23867
24324
  }
23868
24325
  const value = isInside(root, where) ? relative2(where, root) || "." : root;
23869
- const file = resolve15(where, BRAND.pointerFileName);
24326
+ const file = resolve16(where, BRAND.pointerFileName);
23870
24327
  await writeThenRename(file, `${value}
23871
24328
  `);
23872
24329
  printDual({
@@ -24056,6 +24513,8 @@ function whoamiCommand() {
24056
24513
  }
24057
24514
 
24058
24515
  // src/program.ts
24516
+ init_config();
24517
+ init_next_step();
24059
24518
  init_ui();
24060
24519
  init_brand();
24061
24520
 
@@ -24178,6 +24637,10 @@ async function announceNextStep(commandName, reader) {
24178
24637
  const step = await currentNextStep();
24179
24638
  if (step === null) return;
24180
24639
  announced = true;
24640
+ if (step.id === "setup_connected") {
24641
+ const cfg = await loadConfig().catch(() => null);
24642
+ if (cfg?.system.setupConnected) await markConnectedNoticeShown(cfg.system.setupConnected.at);
24643
+ }
24181
24644
  if (step.id === "finish_setup" && reader !== "full") {
24182
24645
  const text = reader === "person" ? SHORT_SETUP_NOTICE : reader === "verb" && await claimFullSetupNotice() ? step.markdown : SETUP_NUDGE;
24183
24646
  process.stderr.write(`
@@ -24242,8 +24705,14 @@ async function run3(argv) {
24242
24705
  process.stderr.isTTY === true ? "person" : cleaned[2] === void 0 ? "full" : "brief"
24243
24706
  );
24244
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
+ });
24245
24714
  await program.parseAsync(cleaned);
24246
- if (argv[2] !== "update") await backgroundUpdateNotice(argv[2] === void 0 ? {} : { verb: argv[2] });
24715
+ if (ran !== "update") await backgroundUpdateNotice(ran === void 0 ? {} : { verb: ran });
24247
24716
  }
24248
24717
  async function handleError(err) {
24249
24718
  const failure = adaptRegistryError(err) ?? err;