memgineering 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1164,12 +1164,12 @@ function findConflicts(memories, relations, claimsById) {
1164
1164
  }
1165
1165
  function missingCounterparts(included, all, relations) {
1166
1166
  const inSet = new Set(included.map((m) => m.id));
1167
- const exists2 = new Set(all.map((m) => m.id));
1167
+ const exists3 = new Set(all.map((m) => m.id));
1168
1168
  const wanted = /* @__PURE__ */ new Set();
1169
1169
  for (const rel of relations) {
1170
1170
  if (rel.type !== "contradicts") continue;
1171
- if (inSet.has(rel.from) && !inSet.has(rel.to) && exists2.has(rel.to)) wanted.add(rel.to);
1172
- if (inSet.has(rel.to) && !inSet.has(rel.from) && exists2.has(rel.from)) wanted.add(rel.from);
1171
+ if (inSet.has(rel.from) && !inSet.has(rel.to) && exists3.has(rel.to)) wanted.add(rel.to);
1172
+ if (inSet.has(rel.to) && !inSet.has(rel.from) && exists3.has(rel.from)) wanted.add(rel.from);
1173
1173
  }
1174
1174
  return [...wanted].sort();
1175
1175
  }
@@ -1734,6 +1734,120 @@ var init_commit = __esm({
1734
1734
  }
1735
1735
  });
1736
1736
 
1737
+ // ../../packages/memory-engine/src/capture/curate.ts
1738
+ function asSingleMatch(entry, now = /* @__PURE__ */ new Date()) {
1739
+ return {
1740
+ candidate: {
1741
+ subject: entry.memory.title,
1742
+ scope: entry.memory.scope,
1743
+ kind: entry.memory.type,
1744
+ candidate_claim: entry.memory.summary ?? entry.memory.title,
1745
+ entities: [],
1746
+ importance: "normal"
1747
+ },
1748
+ matches: [
1749
+ {
1750
+ id: entry.memory.id,
1751
+ title: entry.memory.title,
1752
+ summary: entry.memory.summary,
1753
+ path: entry.path,
1754
+ revision: entry.revision,
1755
+ freshness: freshnessOf(entry.memory, entry.claims, now),
1756
+ score: 1
1757
+ }
1758
+ ],
1759
+ contradicts: [],
1760
+ scopes: entry.memory.scope ? [entry.memory.scope] : []
1761
+ };
1762
+ }
1763
+ function reviseInput(entry, intent) {
1764
+ const action = intent.action;
1765
+ return {
1766
+ action,
1767
+ memory_id: entry.memory.id,
1768
+ claim: {
1769
+ text: intent.claim,
1770
+ scope: entry.memory.scope,
1771
+ valid_from: validFrom(entry, intent),
1772
+ confidence: "stated",
1773
+ summary: action === "conflict" ? null : intent.summary ?? intent.claim,
1774
+ // Never defaulted from the claim, unlike the summary. A summary that is
1775
+ // missing makes the change invisible, so filling it in is a rescue; a
1776
+ // title that is missing simply means the author's heading still fits, and
1777
+ // overwriting it would be rewriting their classification uninvited.
1778
+ title: action === "conflict" ? null : intent.title ?? null
1779
+ },
1780
+ relations: {
1781
+ depends_on: [],
1782
+ relates_to: [],
1783
+ contradicts: [...intent.contradicts ?? []]
1784
+ },
1785
+ reason: intent.reason ?? NO_REASON_GIVEN,
1786
+ // The engine refuses to submit without an excerpt, because approving a diff
1787
+ // on an assertion alone is not consent. Nothing here is approved by a
1788
+ // person, but the requirement still earns its keep: it forces the record to
1789
+ // say what the change was based on rather than only what it did.
1790
+ source_excerpt: intent.because ?? entry.memory.summary ?? entry.memory.title,
1791
+ target_path: entry.path
1792
+ };
1793
+ }
1794
+ function lifecycleInput(entry, action, reason) {
1795
+ return {
1796
+ action,
1797
+ memory_id: entry.memory.id,
1798
+ claim: null,
1799
+ reason: reason ?? noLifecycleReason(action),
1800
+ // For a standing change the memory's own summary IS the passage the
1801
+ // decision is about: it is what the note currently says, and it is what
1802
+ // stops being current.
1803
+ source_excerpt: entry.memory.summary ?? entry.memory.title,
1804
+ target_path: entry.path
1805
+ };
1806
+ }
1807
+ function saysNothingNew(entry, input, normalize) {
1808
+ const norm = (v) => normalize(v ?? "").trim();
1809
+ const claim = input.claim;
1810
+ if (!claim || norm(claim.text) === "") return false;
1811
+ if (norm(claim.text) !== norm(previousClaim(entry))) return false;
1812
+ if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
1813
+ if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
1814
+ const declared = new Set(
1815
+ entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
1816
+ );
1817
+ return (input.relations?.contradicts ?? []).every((id) => declared.has(id));
1818
+ }
1819
+ function bodyStillAsserts(content, oldClaim, newClaim, normalize) {
1820
+ if (oldClaim === null) return null;
1821
+ const line = normalize(oldClaim.split("\n")[0] ?? "").trim();
1822
+ if (line === "") return null;
1823
+ if (line === normalize(newClaim?.split("\n")[0] ?? "").trim()) return null;
1824
+ return bodyOf(content).includes(line) ? line : null;
1825
+ }
1826
+ function bodyOf(content) {
1827
+ const stripped = content.replace(/^/, "");
1828
+ const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
1829
+ return fence ? stripped.slice(fence[0].length) : stripped;
1830
+ }
1831
+ function previousClaim(entry) {
1832
+ const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
1833
+ return current?.text ?? null;
1834
+ }
1835
+ function validFrom(entry, intent) {
1836
+ if (intent.validFrom) return intent.validFrom;
1837
+ if (intent.action === "supersede") return (intent.now ?? /* @__PURE__ */ new Date()).toISOString();
1838
+ const declared = entry.claims.find((c2) => c2.valid_from)?.valid_from;
1839
+ return declared ?? null;
1840
+ }
1841
+ var NO_REASON_GIVEN, noLifecycleReason;
1842
+ var init_curate = __esm({
1843
+ "../../packages/memory-engine/src/capture/curate.ts"() {
1844
+ "use strict";
1845
+ init_planner();
1846
+ NO_REASON_GIVEN = "revised directly";
1847
+ noLifecycleReason = (action) => `${action}d directly`;
1848
+ }
1849
+ });
1850
+
1737
1851
  // ../../packages/memory-engine/src/proposal/patch.ts
1738
1852
  import { parseDocument, parse as parseYaml2, stringify as stringifyYaml } from "yaml";
1739
1853
  function inlineValueOf(keyLine) {
@@ -2069,6 +2183,109 @@ var init_apply = __esm({
2069
2183
  }
2070
2184
  });
2071
2185
 
2186
+ // ../../packages/memory-engine/src/resurface/rank.ts
2187
+ function rank(entries, events, opts) {
2188
+ const recalls = events.filter((e) => e.verb !== "resurface");
2189
+ const here = opts.here ? recalls.filter((e) => opts.here(e)) : [];
2190
+ const countIn = (source) => {
2191
+ const counts = /* @__PURE__ */ new Map();
2192
+ for (const event of source) {
2193
+ for (const id of event.returned_ids) counts.set(id, (counts.get(id) ?? 0) + 1);
2194
+ if (event.opened_id) counts.set(event.opened_id, (counts.get(event.opened_id) ?? 0) + 1);
2195
+ }
2196
+ return counts;
2197
+ };
2198
+ const hereCounts = countIn(here);
2199
+ const anyCounts = countIn(recalls);
2200
+ const lastTouched = /* @__PURE__ */ new Map();
2201
+ for (const event of recalls) {
2202
+ const t = Date.parse(event.ts);
2203
+ if (!Number.isFinite(t)) continue;
2204
+ for (const id of [...event.returned_ids, event.opened_id].filter(Boolean)) {
2205
+ lastTouched.set(id, Math.max(lastTouched.get(id) ?? 0, t));
2206
+ }
2207
+ }
2208
+ const hasBase = entries.some((e) => e.path.startsWith("01_BASE/"));
2209
+ const scored = [];
2210
+ for (const entry of entries) {
2211
+ const id = entry.memory.id;
2212
+ const freshness = freshnessOf(entry.memory, entry.claims, opts.now);
2213
+ if (freshness === "retired") continue;
2214
+ const why = [];
2215
+ let score = 0;
2216
+ const hereCount = hereCounts.get(id) ?? 0;
2217
+ if (hereCount > 0) {
2218
+ score += WEIGHT.contextRecall * Math.log2(1 + hereCount);
2219
+ why.push(`recalled ${hereCount}\xD7 in this folder`);
2220
+ }
2221
+ const anyCount = (anyCounts.get(id) ?? 0) - hereCount;
2222
+ if (anyCount > 0) {
2223
+ score += WEIGHT.anyRecall * Math.log2(1 + anyCount);
2224
+ if (hereCount === 0) why.push(`recalled ${anyCount}\xD7 elsewhere`);
2225
+ }
2226
+ const touched = lastTouched.get(id);
2227
+ if (touched !== void 0) {
2228
+ const days = (opts.now.getTime() - touched) / 864e5;
2229
+ score += WEIGHT.recency * Math.pow(0.5, days / 7);
2230
+ }
2231
+ if (hasBase && entry.path.startsWith("01_BASE/")) {
2232
+ const days = touched === void 0 ? Number.POSITIVE_INFINITY : (opts.now.getTime() - touched) / 864e5;
2233
+ if (days > STALE_BASE_DAYS) {
2234
+ score += WEIGHT.staleBase;
2235
+ why.push(
2236
+ touched === void 0 ? "never read this session" : `not read in ${Math.floor(days)} days`
2237
+ );
2238
+ }
2239
+ }
2240
+ if (score === 0 && !entry.path.startsWith("01_BASE/")) {
2241
+ score += WEIGHT.neverReached;
2242
+ why.push("never surfaced yet");
2243
+ }
2244
+ if (freshness !== "current") why.push(freshness);
2245
+ if (score > 0) scored.push({ entry, score, why });
2246
+ }
2247
+ return scored.sort(
2248
+ (a, b) => b.score - a.score || a.entry.memory.id.localeCompare(b.entry.memory.id)
2249
+ );
2250
+ }
2251
+ var WEIGHT, STALE_BASE_DAYS;
2252
+ var init_rank = __esm({
2253
+ "../../packages/memory-engine/src/resurface/rank.ts"() {
2254
+ "use strict";
2255
+ init_planner();
2256
+ WEIGHT = {
2257
+ contextRecall: 3,
2258
+ anyRecall: 1,
2259
+ recency: 2,
2260
+ staleBase: 4,
2261
+ /**
2262
+ * A note nobody has reached for yet.
2263
+ *
2264
+ * Everything above scores a note for having been USED — recalled here,
2265
+ * recalled anywhere, touched lately. A note that has only ever been written
2266
+ * scores none of them, and the filter at the end drops anything at zero. So
2267
+ * on a brain whose notes have not been recalled yet, `resurface` could return
2268
+ * nothing but `01_BASE/` templates: the two real notes in a three-note brain
2269
+ * were invisible at `--limit 8`, while `recall` found them immediately.
2270
+ *
2271
+ * That is a loop with no way in. `resurface` runs at session start and is the
2272
+ * answer to "what should I already know here" — a question asked precisely
2273
+ * when nobody knows what to search for. If a note has to be recalled before
2274
+ * it can be surfaced, and surfacing is how you learn it exists, it never
2275
+ * surfaces. Measured: an agent given such a brain saw five "not filled in
2276
+ * yet" placeholders, concluded there was nothing in it, and never called
2277
+ * memgineering again in that session.
2278
+ *
2279
+ * Smallest weight on the board on purpose — anything with real evidence
2280
+ * behind it still outranks this, and this only decides what fills the rest of
2281
+ * the page instead of leaving it blank.
2282
+ */
2283
+ neverReached: 0.5
2284
+ };
2285
+ STALE_BASE_DAYS = 30;
2286
+ }
2287
+ });
2288
+
2072
2289
  // ../../packages/memory-engine/src/index.ts
2073
2290
  var init_src = __esm({
2074
2291
  "../../packages/memory-engine/src/index.ts"() {
@@ -2086,8 +2303,10 @@ var init_src = __esm({
2086
2303
  init_store();
2087
2304
  init_prepare();
2088
2305
  init_commit();
2306
+ init_curate();
2089
2307
  init_patch();
2090
2308
  init_apply();
2309
+ init_rank();
2091
2310
  }
2092
2311
  });
2093
2312
 
@@ -2672,6 +2891,24 @@ var init_config = __esm({
2672
2891
  ConfigSchema = z6.object({
2673
2892
  brain: z6.object({
2674
2893
  brains: z6.array(BrainSchema).default([]),
2894
+ /**
2895
+ * The hosted brain this machine is pointed at, when there is one.
2896
+ *
2897
+ * Its presence is what makes a command look at the server instead of the
2898
+ * disk, so it is set deliberately — by `push`, or by picking one — and
2899
+ * never inferred from being signed in. Somebody with an account and no
2900
+ * hosted brain keeps working exactly as they did.
2901
+ *
2902
+ * `api_url` is stored beside the id for the same reason the token is:
2903
+ * a brain id means nothing to a different server, and pointing
2904
+ * `MEMGINEERING_API_URL` somewhere else must read as "no hosted brain
2905
+ * here" rather than as a request for a stranger's brain.
2906
+ */
2907
+ cloud: z6.object({
2908
+ id: z6.string().min(1),
2909
+ name: z6.string().min(1),
2910
+ api_url: z6.string().min(1)
2911
+ }).optional(),
2675
2912
  /**
2676
2913
  * The brain to use when nothing else says which — root path, or absent.
2677
2914
  *
@@ -2850,9 +3087,237 @@ var init_index_build = __esm({
2850
3087
  }
2851
3088
  });
2852
3089
 
3090
+ // src/lib/credentials.ts
3091
+ import { mkdir as mkdir7, readFile as readFile12, unlink, writeFile as writeFile6 } from "fs/promises";
3092
+ import { join as join11 } from "path";
3093
+ import { z as z8 } from "zod";
3094
+ function credentialsPath() {
3095
+ return join11(brandHome(), "credentials.json");
3096
+ }
3097
+ async function readCredentials() {
3098
+ try {
3099
+ const parsed = CredentialsSchema.safeParse(
3100
+ JSON.parse(await readFile12(credentialsPath(), "utf8"))
3101
+ );
3102
+ return parsed.success ? parsed.data : null;
3103
+ } catch {
3104
+ return null;
3105
+ }
3106
+ }
3107
+ async function saveCredentials(credentials) {
3108
+ await mkdir7(brandHome(), { recursive: true });
3109
+ await unlink(credentialsPath()).catch(() => void 0);
3110
+ await writeFile6(credentialsPath(), JSON.stringify(credentials, null, 2) + "\n", {
3111
+ encoding: "utf8",
3112
+ mode: 384
3113
+ });
3114
+ }
3115
+ async function clearCredentials() {
3116
+ await unlink(credentialsPath()).catch(() => void 0);
3117
+ }
3118
+ var CredentialsSchema;
3119
+ var init_credentials = __esm({
3120
+ "src/lib/credentials.ts"() {
3121
+ "use strict";
3122
+ init_brand();
3123
+ CredentialsSchema = z8.object({
3124
+ /** The server that issued this token. Compared before the token is ever sent. */
3125
+ api_url: z8.string().min(1),
3126
+ token: z8.string().min(1),
3127
+ account_id: z8.string().min(1),
3128
+ display_name: z8.string().nullable().default(null),
3129
+ signed_in_at: z8.string().min(1)
3130
+ });
3131
+ }
3132
+ });
3133
+
3134
+ // src/lib/pending-login.ts
3135
+ import { mkdir as mkdir8, readFile as readFile13, unlink as unlink2, writeFile as writeFile7 } from "fs/promises";
3136
+ import { join as join12 } from "path";
3137
+ import { z as z9 } from "zod";
3138
+ function pendingLoginPath() {
3139
+ return join12(brandHome(), "pending-login.json");
3140
+ }
3141
+ async function readPendingLogin() {
3142
+ try {
3143
+ const parsed = PendingLoginSchema.safeParse(
3144
+ JSON.parse(await readFile13(pendingLoginPath(), "utf8"))
3145
+ );
3146
+ return parsed.success ? parsed.data : null;
3147
+ } catch {
3148
+ return null;
3149
+ }
3150
+ }
3151
+ async function savePendingLogin(pending) {
3152
+ await mkdir8(brandHome(), { recursive: true });
3153
+ await unlink2(pendingLoginPath()).catch(() => void 0);
3154
+ await writeFile7(pendingLoginPath(), JSON.stringify(pending, null, 2) + "\n", {
3155
+ encoding: "utf8",
3156
+ mode: 384
3157
+ });
3158
+ }
3159
+ async function clearPendingLogin() {
3160
+ await unlink2(pendingLoginPath()).catch(() => void 0);
3161
+ }
3162
+ function pendingExpired(pending, now = Date.now()) {
3163
+ return Math.floor(now / 1e3) >= pending.expires_at_epoch;
3164
+ }
3165
+ var PendingLoginSchema;
3166
+ var init_pending_login = __esm({
3167
+ "src/lib/pending-login.ts"() {
3168
+ "use strict";
3169
+ init_brand();
3170
+ PendingLoginSchema = z9.object({
3171
+ /** Which server this pairing belongs to. */
3172
+ api_url: z9.string().min(1),
3173
+ device_code: z9.string().min(1),
3174
+ /** Shown again if the agent needs to re-display it. */
3175
+ user_code: z9.string().min(1),
3176
+ verification_url: z9.string().min(1),
3177
+ /** Unix seconds. Past it, the pairing is gone server-side too. */
3178
+ expires_at_epoch: z9.number().int().positive()
3179
+ });
3180
+ }
3181
+ });
3182
+
3183
+ // src/lib/api-client.ts
3184
+ function apiUrl(env = process.env) {
3185
+ const raw = env["MEMGINEERING_API_URL"]?.trim();
3186
+ return (raw && raw !== "" ? raw : DEFAULT_API_URL).replace(/\/+$/, "");
3187
+ }
3188
+ async function apiRequest(args) {
3189
+ const base = args.baseUrl ?? apiUrl();
3190
+ const url = `${base}${args.path}`;
3191
+ let res;
3192
+ try {
3193
+ res = await fetch(url, {
3194
+ method: args.method,
3195
+ headers: {
3196
+ accept: "application/json",
3197
+ ...args.body === void 0 ? {} : { "content-type": "application/json" },
3198
+ ...args.token ? { authorization: `Bearer ${args.token}` } : {}
3199
+ },
3200
+ ...args.body === void 0 ? {} : { body: JSON.stringify(args.body) },
3201
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
3202
+ });
3203
+ } catch (err) {
3204
+ throw memgError(
3205
+ "server_unreachable",
3206
+ `could not reach ${base}`,
3207
+ `Nothing was sent or changed. Check the connection, or set MEMGINEERING_API_URL if the brain server is somewhere else. Underlying cause: ${err instanceof Error ? err.message : String(err)}`
3208
+ );
3209
+ }
3210
+ let parsed = null;
3211
+ const text = await res.text().catch(() => "");
3212
+ if (text !== "") {
3213
+ try {
3214
+ parsed = JSON.parse(text);
3215
+ } catch {
3216
+ parsed = null;
3217
+ }
3218
+ }
3219
+ if (res.ok || args.expect?.includes(res.status)) {
3220
+ return { status: res.status, body: parsed };
3221
+ }
3222
+ const envelope = parsed?.error;
3223
+ if (envelope && typeof envelope.message === "string" && typeof envelope.hint === "string") {
3224
+ throw new MemgError({
3225
+ code: typeof envelope.code === "string" ? envelope.code : "server_refused",
3226
+ message: envelope.message,
3227
+ hint: envelope.hint
3228
+ });
3229
+ }
3230
+ throw memgError(
3231
+ "server_refused",
3232
+ `${base} answered ${res.status} with no readable error`,
3233
+ `This is not a shape this CLI knows. If MEMGINEERING_API_URL points at something other than a memgineering brain server, that would explain it. First 200 characters of the response: ${text.slice(0, 200)}`
3234
+ );
3235
+ }
3236
+ async function claimPendingLogin(baseUrl = apiUrl()) {
3237
+ const pending = await readPendingLogin();
3238
+ if (!pending) return null;
3239
+ if (pending.api_url !== baseUrl) return null;
3240
+ if (pendingExpired(pending)) {
3241
+ await clearPendingLogin();
3242
+ return null;
3243
+ }
3244
+ return claimOnce(pending, baseUrl);
3245
+ }
3246
+ async function claimOnce(pending, baseUrl = apiUrl()) {
3247
+ let res;
3248
+ try {
3249
+ res = await apiRequest({
3250
+ method: "POST",
3251
+ path: "/v1/auth/device/token",
3252
+ body: { device_code: pending.device_code },
3253
+ baseUrl,
3254
+ expect: [425]
3255
+ });
3256
+ } catch (err) {
3257
+ if (err instanceof MemgError && err.code !== "server_unreachable") {
3258
+ await clearPendingLogin();
3259
+ }
3260
+ throw err;
3261
+ }
3262
+ if (res.status === 425) return null;
3263
+ const token = res.body?.token;
3264
+ if (typeof token !== "string" || token === "") {
3265
+ await clearPendingLogin();
3266
+ throw memgError(
3267
+ "server_refused",
3268
+ "the sign-in completed but no token came back",
3269
+ "Nothing is stored. Run `memgineering login` again; if it repeats, the brain server is answering in a shape this version does not understand."
3270
+ );
3271
+ }
3272
+ const me = await apiRequest({ method: "GET", path: "/v1/me", token, baseUrl });
3273
+ const credentials = {
3274
+ api_url: baseUrl,
3275
+ token,
3276
+ account_id: me.body?.account?.id ?? "",
3277
+ display_name: me.body?.account?.display_name ?? null,
3278
+ signed_in_at: (/* @__PURE__ */ new Date()).toISOString()
3279
+ };
3280
+ await saveCredentials(credentials);
3281
+ await clearPendingLogin();
3282
+ return credentials;
3283
+ }
3284
+ async function currentCredentials(baseUrl = apiUrl()) {
3285
+ const stored = await readCredentials();
3286
+ if (stored && stored.api_url === baseUrl) return stored;
3287
+ return claimPendingLogin(baseUrl);
3288
+ }
3289
+ async function requireCredentials(baseUrl = apiUrl()) {
3290
+ const credentials = await currentCredentials(baseUrl);
3291
+ if (credentials) return credentials;
3292
+ const pending = await readPendingLogin();
3293
+ if (pending && !pendingExpired(pending)) {
3294
+ throw memgError(
3295
+ "login_pending",
3296
+ "a sign-in has been started but nobody has approved it yet",
3297
+ `Show the user this code \u2014 ${pending.user_code} \u2014 and this link: ${pending.verification_url}. They approve there, then run this again and the token is collected automatically.`
3298
+ );
3299
+ }
3300
+ throw memgError(
3301
+ "not_signed_in",
3302
+ `no memgineering account is signed in for ${baseUrl}`,
3303
+ "An account is optional. Everything works on the brain on this machine \u2014 add `--local` to run this against it, and nothing else is needed.\nONLY if the user has asked to connect an account: you lead it \u2014 run `memgineering login --emit-only`, show the code it prints in your reply, and run this again to collect the token.\nDo NOT start one they have not asked for. It opens a Google consent page in their browser, and a sign-in nobody requested is the thing that makes people close the tab."
3304
+ );
3305
+ }
3306
+ var DEFAULT_API_URL, REQUEST_TIMEOUT_MS;
3307
+ var init_api_client = __esm({
3308
+ "src/lib/api-client.ts"() {
3309
+ "use strict";
3310
+ init_errors();
3311
+ init_credentials();
3312
+ init_pending_login();
3313
+ DEFAULT_API_URL = "https://api.memgineering.com";
3314
+ REQUEST_TIMEOUT_MS = 1e4;
3315
+ }
3316
+ });
3317
+
2853
3318
  // src/commands/init.ts
2854
- import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
2855
- import { join as join12, resolve as resolve5 } from "path";
3319
+ import { mkdir as mkdir10, readdir as readdir5, writeFile as writeFile8 } from "fs/promises";
3320
+ import { join as join14, resolve as resolve5 } from "path";
2856
3321
  import { Command as Command3 } from "commander";
2857
3322
  function initCommand() {
2858
3323
  return new Command3("init").description("create a new brain, laid out and ready to write into").argument("<path>", "where the brain should live").option("--name <name>", "what to call it in the hub", "My brain").action(async (path, opts) => {
@@ -2896,10 +3361,10 @@ async function createBrain(path, name) {
2896
3361
  memgineering link ${path}`
2897
3362
  );
2898
3363
  }
2899
- await mkdir8(root, { recursive: true });
2900
- for (const dir of FOLDERS) await mkdir8(join12(root, dir), { recursive: true });
3364
+ await mkdir10(root, { recursive: true });
3365
+ for (const dir of FOLDERS) await mkdir10(join14(root, dir), { recursive: true });
2901
3366
  for (const [file, content] of Object.entries(files(name))) {
2902
- await writeFile6(join12(root, file), content, "utf8");
3367
+ await writeFile8(join14(root, file), content, "utf8");
2903
3368
  }
2904
3369
  const canonicalRoot2 = await canonicalize(root);
2905
3370
  const adapter = await LocalFileReadAdapter.openVault(canonicalRoot2);
@@ -3046,8 +3511,8 @@ var init_init = __esm({
3046
3511
  });
3047
3512
 
3048
3513
  // src/commands/link.ts
3049
- import { readFile as readFile13 } from "fs/promises";
3050
- import { join as join13 } from "path";
3514
+ import { readFile as readFile15 } from "fs/promises";
3515
+ import { join as join15 } from "path";
3051
3516
  import { Command as Command5 } from "commander";
3052
3517
  import prompts from "prompts";
3053
3518
  function sameBasename(all, entry) {
@@ -3247,7 +3712,7 @@ async function recordLink(adapter, observationsDir) {
3247
3712
  }
3248
3713
  async function refuseSilentRules(path, denied, excluded, unsupported) {
3249
3714
  const denyEntries = (await LocalFileReadAdapter.readDenyFile(path, ".memgdeny")).entries;
3250
- const rawDenyLines = await readFile13(join13(path, ".memgdeny"), "utf8").then(
3715
+ const rawDenyLines = await readFile15(join15(path, ".memgdeny"), "utf8").then(
3251
3716
  (t) => t.split("\n").map((l) => l.trim()).filter((l) => l !== "" && !l.startsWith("#"))
3252
3717
  ).catch(() => []);
3253
3718
  const asWritten = (folded) => rawDenyLines.find(
@@ -3417,29 +3882,371 @@ var init_link = __esm({
3417
3882
  }
3418
3883
  });
3419
3884
 
3420
- // src/commands/setup-web-page.ts
3421
- function renderSetupPage() {
3422
- return `<!doctype html>
3423
- <html lang="ko">
3424
- <head>
3425
- <meta charset="utf-8" />
3426
- <meta name="viewport" content="width=device-width, initial-scale=1" />
3427
- <meta name="referrer" content="no-referrer" />
3428
- <title>memgineering \u2014 \uC124\uCE58</title>
3429
- <style>
3430
- :root {
3431
- --bg: #ffffff;
3432
- --fg: #101114;
3433
- --muted: #6b7280;
3434
- --line: #e6e8ec;
3435
- --accent: #2f6bff;
3436
- --accent-fg: #ffffff;
3437
- --card: #ffffff;
3438
- --soft: #f5f6f8;
3439
- --warn: #b45309;
3440
- --ok: #157347;
3441
- }
3442
- @media (prefers-color-scheme: dark) {
3885
+ // src/lib/open-browser.ts
3886
+ import { spawn } from "child_process";
3887
+ async function openBrowser(url) {
3888
+ const { file, args } = browserOpener(url);
3889
+ return new Promise((resolve11) => {
3890
+ let settled = false;
3891
+ const finish = (opened) => {
3892
+ if (settled) return;
3893
+ settled = true;
3894
+ resolve11(opened);
3895
+ };
3896
+ const child = spawn(file, args, { detached: true, stdio: "ignore" });
3897
+ child.on("error", () => finish(false));
3898
+ child.on("exit", (code) => {
3899
+ if (code !== null && code !== 0) finish(false);
3900
+ });
3901
+ child.unref();
3902
+ setTimeout(() => finish(true), 300);
3903
+ });
3904
+ }
3905
+ function browserOpener(url) {
3906
+ if (process.platform === "darwin") return { file: "open", args: [url] };
3907
+ if (process.platform === "win32") return { file: "cmd", args: ["/c", "start", "", url] };
3908
+ return { file: "xdg-open", args: [url] };
3909
+ }
3910
+ var init_open_browser = __esm({
3911
+ "src/lib/open-browser.ts"() {
3912
+ "use strict";
3913
+ }
3914
+ });
3915
+
3916
+ // src/commands/login.ts
3917
+ import { Command as Command9 } from "commander";
3918
+ function loginCommand() {
3919
+ return new Command9("login").description("sign in to a hosted brain").option(
3920
+ "--emit-only",
3921
+ "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"
3922
+ ).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(
3923
+ "after",
3924
+ "\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."
3925
+ ).action(async (opts) => {
3926
+ const base = apiUrl();
3927
+ const already = await currentCredentials(base);
3928
+ if (already) {
3929
+ printDual({
3930
+ json: {
3931
+ already_signed_in: true,
3932
+ account: { id: already.account_id, display_name: already.display_name },
3933
+ api_url: base
3934
+ },
3935
+ human: () => {
3936
+ printHuman(
3937
+ `Already signed in${already.display_name ? ` as ${c.bold(already.display_name)}` : ""}.`
3938
+ );
3939
+ printHuman(c.gray(" `memgineering logout` first if you want a different account."));
3940
+ }
3941
+ });
3942
+ return;
3943
+ }
3944
+ const { pending, opened } = await beginSignIn({ browser: opts.browser !== false });
3945
+ const showUrl = !opened || opts.printUrl === true;
3946
+ if (opts.emitOnly) {
3947
+ emit(pending, { base, opened, showUrl, blocking: false });
3948
+ return;
3949
+ }
3950
+ emit(pending, { base, opened, showUrl, blocking: true });
3951
+ await waitForApproval(pending, base);
3952
+ });
3953
+ }
3954
+ async function beginSignIn(opts) {
3955
+ const pending = await startPairing(apiUrl());
3956
+ const opened = opts.browser ? await openBrowser(pending.verification_url) : false;
3957
+ await savePendingLogin(pending);
3958
+ return { pending, opened };
3959
+ }
3960
+ async function signInSummary() {
3961
+ const base = apiUrl();
3962
+ const stored = await readCredentials();
3963
+ const pending = await pendingOrNull();
3964
+ return {
3965
+ signedIn: stored !== null && stored.api_url === base,
3966
+ displayName: stored?.api_url === base ? stored?.display_name ?? null : null,
3967
+ pendingUserCode: pending?.api_url === base ? pending.user_code : null,
3968
+ apiUrl: base
3969
+ };
3970
+ }
3971
+ async function startPairing(base) {
3972
+ const res = await apiRequest({
3973
+ method: "POST",
3974
+ path: "/v1/auth/device/code",
3975
+ baseUrl: base
3976
+ });
3977
+ const { device_code, user_code, verification_url, expires_in } = res.body ?? {};
3978
+ if (!device_code || !user_code || !verification_url) {
3979
+ throw memgError(
3980
+ "server_refused",
3981
+ "the brain server did not return a usable sign-in code",
3982
+ `Nothing is stored. Check that MEMGINEERING_API_URL points at a memgineering brain server \u2014 it is currently ${base}.`
3983
+ );
3984
+ }
3985
+ return {
3986
+ api_url: base,
3987
+ device_code,
3988
+ user_code,
3989
+ verification_url,
3990
+ expires_at_epoch: Math.floor(Date.now() / 1e3) + (expires_in ?? 600)
3991
+ };
3992
+ }
3993
+ function emit(pending, ctx) {
3994
+ printDual({
3995
+ json: {
3996
+ user_code: pending.user_code,
3997
+ // Always present in JSON, unlike `setup --web`: this link authorizes
3998
+ // nothing on its own. It names a pairing whose only power is to be
3999
+ // approved by somebody who signs in with Google, and an agent that cannot
4000
+ // re-show it leaves a user stuck at a page with no way back to it.
4001
+ verification_url: pending.verification_url,
4002
+ expires_at_epoch: pending.expires_at_epoch,
4003
+ browser_opened: ctx.opened,
4004
+ api_url: ctx.base,
4005
+ waiting: ctx.blocking,
4006
+ 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."
4007
+ },
4008
+ human: () => {
4009
+ printHuman("Show this code to whoever is signing in:\n");
4010
+ printHuman(` ${c.bold().yellow(pending.user_code)}
4011
+ `);
4012
+ if (ctx.opened) {
4013
+ printHuman("Opened the approval page in your browser.");
4014
+ } else {
4015
+ printHuman("Open this to approve:\n");
4016
+ printHuman(` ${c.cyan(pending.verification_url)}`);
4017
+ }
4018
+ if (ctx.opened && ctx.showUrl) {
4019
+ printHuman(c.gray(` ${pending.verification_url}`));
4020
+ }
4021
+ printHuman(
4022
+ c.gray(
4023
+ "\n The page asks whether the code above is the one you were shown.\n If you did not start this, close it \u2014 nothing has been created."
4024
+ )
4025
+ );
4026
+ if (!ctx.blocking) {
4027
+ printHuman(c.gray("\n Not waiting. The next memgineering command collects the token."));
4028
+ }
4029
+ }
4030
+ });
4031
+ }
4032
+ async function waitForApproval(pending, base) {
4033
+ const deadline = Date.now() + BLOCKING_TIMEOUT_MS;
4034
+ progress("Waiting for approval");
4035
+ for (; ; ) {
4036
+ if (Date.now() >= deadline || pendingExpired(pending)) {
4037
+ throw memgError(
4038
+ "login_expired",
4039
+ "nobody approved the sign-in in time",
4040
+ "Run `memgineering login` again for a fresh code. Nothing was created."
4041
+ );
4042
+ }
4043
+ await sleep(5e3);
4044
+ let claimed;
4045
+ try {
4046
+ claimed = await claimOnce(pending, base);
4047
+ } catch (err) {
4048
+ if (err instanceof MemgError && err.code === "server_unreachable") continue;
4049
+ throw err;
4050
+ }
4051
+ if (claimed) {
4052
+ printDual({
4053
+ json: {
4054
+ signed_in: true,
4055
+ account: { id: claimed.account_id, display_name: claimed.display_name },
4056
+ api_url: base
4057
+ },
4058
+ human: () => {
4059
+ printHuman(
4060
+ `
4061
+ Signed in${claimed.display_name ? ` as ${c.bold(claimed.display_name)}` : ""}.`
4062
+ );
4063
+ printHuman(c.gray(" The token is stored on this machine only, readable by you alone."));
4064
+ }
4065
+ });
4066
+ return;
4067
+ }
4068
+ }
4069
+ }
4070
+ async function pendingOrNull() {
4071
+ const pending = await readPendingLogin();
4072
+ if (!pending) return null;
4073
+ if (pendingExpired(pending)) {
4074
+ await clearPendingLogin();
4075
+ return null;
4076
+ }
4077
+ return pending;
4078
+ }
4079
+ var BLOCKING_TIMEOUT_MS, sleep;
4080
+ var init_login = __esm({
4081
+ "src/commands/login.ts"() {
4082
+ "use strict";
4083
+ init_errors();
4084
+ init_api_client();
4085
+ init_credentials();
4086
+ init_open_browser();
4087
+ init_pending_login();
4088
+ init_ui();
4089
+ BLOCKING_TIMEOUT_MS = 10 * 60 * 1e3;
4090
+ sleep = (ms) => new Promise((resolve11) => {
4091
+ setTimeout(resolve11, ms).unref?.();
4092
+ });
4093
+ }
4094
+ });
4095
+
4096
+ // src/commands/setup-signin.ts
4097
+ import prompts2 from "prompts";
4098
+ async function runSignInStep(opts) {
4099
+ const summary = await signInSummary();
4100
+ if (summary.signedIn) return { status: "signed-in", displayName: summary.displayName };
4101
+ if (summary.pendingUserCode) return { status: "pending", userCode: summary.pendingUserCode };
4102
+ if (opts.login === false) return { status: "skipped" };
4103
+ if (opts.dryRun) {
4104
+ const wouldAsk = opts.interactive || opts.login === true;
4105
+ return wouldAsk ? { status: "would-start" } : { status: "skipped" };
4106
+ }
4107
+ if (!opts.interactive && opts.login !== true) return { status: "skipped" };
4108
+ if (opts.interactive) {
4109
+ printHuman(c.bold("\nmemgineering \u2014 setup\n"));
4110
+ const { wants } = await prompts2({
4111
+ type: "toggle",
4112
+ name: "wants",
4113
+ message: "Sign in to a memgineering account first?",
4114
+ initial: false,
4115
+ active: "sign in",
4116
+ inactive: "later"
4117
+ });
4118
+ if (wants !== true) {
4119
+ printHuman(
4120
+ c.gray(" Skipped. Everything below works on files on this machine, with no account.\n")
4121
+ );
4122
+ return { status: "skipped" };
4123
+ }
4124
+ }
4125
+ try {
4126
+ const { pending, opened } = await beginSignIn({ browser: opts.browser });
4127
+ return {
4128
+ status: "started",
4129
+ userCode: pending.user_code,
4130
+ verificationUrl: pending.verification_url,
4131
+ opened
4132
+ };
4133
+ } catch (err) {
4134
+ return {
4135
+ status: "unavailable",
4136
+ because: err instanceof MemgError ? err.message : String(err)
4137
+ };
4138
+ }
4139
+ }
4140
+ function printSignInStep(step) {
4141
+ switch (step.status) {
4142
+ case "signed-in":
4143
+ printHuman(c.gray(` signed in${step.displayName ? ` as ${step.displayName}` : ""}
4144
+ `));
4145
+ return;
4146
+ case "started":
4147
+ printHuman("Show this code to whoever is signing in:\n");
4148
+ printHuman(` ${c.bold().yellow(step.userCode)}
4149
+ `);
4150
+ printHuman(
4151
+ step.opened ? c.gray(" The approval page is open in the browser.") : `Open this to approve:
4152
+ ${c.cyan(step.verificationUrl)}`
4153
+ );
4154
+ printHuman(
4155
+ c.gray(" Not waiting \u2014 setup carries on, and the next command collects the token.\n")
4156
+ );
4157
+ return;
4158
+ case "pending":
4159
+ printHuman(c.gray(` a sign-in is waiting for approval \u2014 code ${step.userCode}
4160
+ `));
4161
+ return;
4162
+ case "unavailable":
4163
+ printHuman(c.yellow(` Could not start a sign-in: ${step.because}`));
4164
+ printHuman(c.gray(" Setup continues \u2014 an account is not needed for any of it.\n"));
4165
+ return;
4166
+ case "would-start":
4167
+ printHuman(c.gray(" would start a sign-in (nothing was asked for \u2014 this is a dry run)\n"));
4168
+ return;
4169
+ case "skipped":
4170
+ printHuman(
4171
+ c.gray(" no account \u2014 this brain lives on this machine only.") + c.gray("\n To sync it across your devices and tools later: `memgineering login`\n")
4172
+ );
4173
+ return;
4174
+ }
4175
+ }
4176
+ function signInStepJson(step) {
4177
+ switch (step.status) {
4178
+ case "signed-in":
4179
+ return { account: { signed_in: true, display_name: step.displayName } };
4180
+ case "started":
4181
+ return {
4182
+ account: {
4183
+ signed_in: false,
4184
+ sign_in_started: true,
4185
+ user_code: step.userCode,
4186
+ verification_url: step.verificationUrl,
4187
+ next: "SHOW user_code to the user and have them approve at verification_url. The token is claimed by the next memgineering command."
4188
+ }
4189
+ };
4190
+ case "pending":
4191
+ return {
4192
+ account: {
4193
+ signed_in: false,
4194
+ sign_in_pending: true,
4195
+ user_code: step.userCode,
4196
+ next: "A sign-in is already waiting. SHOW user_code to the user; the token is claimed once they approve."
4197
+ }
4198
+ };
4199
+ case "unavailable":
4200
+ return { account: { signed_in: false, sign_in_unavailable: step.because } };
4201
+ case "would-start":
4202
+ return {
4203
+ account: {
4204
+ signed_in: false,
4205
+ would_start_sign_in: true,
4206
+ next: "A dry run asks nothing of the server. Run without --dry-run to start the sign-in."
4207
+ }
4208
+ };
4209
+ case "skipped":
4210
+ return {
4211
+ account: {
4212
+ signed_in: false,
4213
+ next: "Optional: `memgineering login --emit-only` connects a hosted account. Nothing installed here needs one \u2014 brains are folders on this machine."
4214
+ }
4215
+ };
4216
+ }
4217
+ }
4218
+ var init_setup_signin = __esm({
4219
+ "src/commands/setup-signin.ts"() {
4220
+ "use strict";
4221
+ init_errors();
4222
+ init_login();
4223
+ init_ui();
4224
+ }
4225
+ });
4226
+
4227
+ // src/commands/setup-web-page.ts
4228
+ function renderSetupPage() {
4229
+ return `<!doctype html>
4230
+ <html lang="ko">
4231
+ <head>
4232
+ <meta charset="utf-8" />
4233
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4234
+ <meta name="referrer" content="no-referrer" />
4235
+ <title>memgineering \u2014 \uC124\uCE58</title>
4236
+ <style>
4237
+ :root {
4238
+ --bg: #ffffff;
4239
+ --fg: #101114;
4240
+ --muted: #6b7280;
4241
+ --line: #e6e8ec;
4242
+ --accent: #2f6bff;
4243
+ --accent-fg: #ffffff;
4244
+ --card: #ffffff;
4245
+ --soft: #f5f6f8;
4246
+ --warn: #b45309;
4247
+ --ok: #157347;
4248
+ }
4249
+ @media (prefers-color-scheme: dark) {
3443
4250
  :root {
3444
4251
  --bg: #0e0f12;
3445
4252
  --fg: #f2f3f5;
@@ -3514,6 +4321,18 @@ function renderSetupPage() {
3514
4321
  .err b { display: block; margin-bottom: 0.35rem; }
3515
4322
  .err span { color: var(--muted); }
3516
4323
  .spinner { color: var(--muted); font-size: 0.9rem; margin-top: 1.2rem; }
4324
+ table.cmp { width: 100%; border-collapse: collapse; margin: 1.4rem 0 0.4rem; font-size: 0.95rem; }
4325
+ table.cmp th, table.cmp td { padding: 0.62rem 0.5rem; border-bottom: 1px solid var(--line); text-align: left; }
4326
+ table.cmp thead th { color: var(--muted); font-weight: 600; font-size: 0.85rem; }
4327
+ table.cmp th:first-child { width: 52%; font-weight: 400; }
4328
+ table.cmp td { text-align: center; width: 24%; }
4329
+ table.cmp td.yes { color: var(--accent); font-weight: 600; }
4330
+ table.cmp td.no { color: var(--muted); }
4331
+ table.cmp td small { display: block; color: var(--muted); font-weight: 400; font-size: 0.78rem; }
4332
+ .lede { font-size: 1.05rem; line-height: 1.65; margin: 0 0 0.2rem; }
4333
+ .terms-body { font-size: 0.9rem; line-height: 1.75; color: var(--muted); }
4334
+ .terms-body h2 { font-size: 1rem; color: var(--fg); margin: 1.6rem 0 0.4rem; }
4335
+ .draft { border-left: 3px solid var(--warn); padding: 0.2rem 0 0.2rem 0.9rem; margin: 0 0 1.4rem; font-size: 0.9rem; }
3517
4336
  </style>
3518
4337
  </head>
3519
4338
  <body>
@@ -3521,6 +4340,72 @@ function renderSetupPage() {
3521
4340
  <div class="brand">memgineering</div>
3522
4341
  <div id="error" class="err" hidden><b id="error-msg"></b><span id="error-hint"></span></div>
3523
4342
 
4343
+ <section id="step-account" hidden>
4344
+ <div class="steps" id="count-account"></div>
4345
+ <h1>\uAE30\uC5B5\uC744 \uC5B4\uB514\uC5D0 \uB458\uAE4C\uC694?</h1>
4346
+ <p class="lede">memgineering\uC740 <b>\uB2F9\uC2E0\uC774 \uC4F0\uB294 AI\uB4E4\uC774 \uD568\uAED8 \uBCF4\uB294 \uAE30\uC5B5</b>\uC785\uB2C8\uB2E4. \uD55C \uBC88 \uB9D0\uD55C \uAC83\uC744 \uB2E4\uC74C \uB300\uD654\uC5D0\uC11C, \uB2E4\uB978 AI\uC5D0\uC11C, \uB0B4\uC77C\uB3C4 \uADF8\uB300\uB85C \uC501\uB2C8\uB2E4. \uB9E4\uBC88 \uCC98\uC74C\uBD80\uD130 \uC124\uBA85\uD558\uC9C0 \uC54A\uC544\uB3C4 \uB429\uB2C8\uB2E4.</p>
4347
+ <p class="sub">\uBE44\uC5B4 \uC788\uB294 \uCC44\uB85C \uC2DC\uC791\uD569\uB2C8\uB2E4. \uC4F0\uB294 \uB3D9\uC548 \uCC44\uC6CC\uC9D1\uB2C8\uB2E4.</p>
4348
+
4349
+ <table class="cmp">
4350
+ <thead>
4351
+ <tr><th></th><th>\uC774 \uCEF4\uD4E8\uD130\uC5D0\uB9CC</th><th>\uACC4\uC815\uC5D0 \uB450\uAE30</th></tr>
4352
+ </thead>
4353
+ <tbody>
4354
+ <tr><th>\uC9C0\uAE08 \uC4F0\uB294 AI\uAC00 \uC77D\uACE0 \uC501\uB2C8\uB2E4</th><td class="yes">\u2713</td><td class="yes">\u2713</td></tr>
4355
+ <tr><th>\uBAA8\uB4E0 \uBCC0\uACBD\uC774 \uAE30\uB85D\uB418\uACE0 \uB418\uB3CC\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4</th><td class="yes">\u2713</td><td class="yes">\u2713</td></tr>
4356
+ <tr><th>\uAC00\uC785\uD558\uC9C0 \uC54A\uC544\uB3C4 \uC804\uBD80 \uB3D9\uC791\uD569\uB2C8\uB2E4</th><td class="yes">\u2713</td><td class="no">\u2014</td></tr>
4357
+ <tr><th>\uB2E4\uB978 \uCEF4\uD4E8\uD130\uC5D0\uC11C\uB3C4 \uAC19\uC740 \uAE30\uC5B5</th><td class="no">\u2014</td><td class="yes">\u2713</td></tr>
4358
+ <tr><th>\uC774 \uCEF4\uD4E8\uD130\uAC00 \uC5C6\uC5B4\uC838\uB3C4 \uAE30\uC5B5\uC740 \uB0A8\uC2B5\uB2C8\uB2E4</th><td class="no">\u2014</td><td class="yes">\u2713</td></tr>
4359
+ <tr><th>\uC5B8\uC81C\uB4E0 \uD3F4\uB354\uB85C \uB0B4\uB824\uBC1B\uAE30</th><td class="no"><small>\uC6D0\uB798 \uD3F4\uB354\uC785\uB2C8\uB2E4</small></td><td class="yes">\u2713</td></tr>
4360
+ <tr><th>\uBE44\uC6A9</th><td class="no"><small>\uBB34\uB8CC</small></td><td class="yes">\uBB34\uB8CC<small>\uBE0C\uB808\uC778 1\uAC1C</small></td></tr>
4361
+ </tbody>
4362
+ </table>
4363
+
4364
+ <button class="card" id="account-cloud" aria-checked="false"><span class="tick"></span><span><b>\uACC4\uC815\uC5D0 \uB450\uAE30<span class="badge">\uAD8C\uC7A5</span></b><small>\uAD6C\uAE00 \uACC4\uC815\uC73C\uB85C \uB85C\uADF8\uC778\uD569\uB2C8\uB2E4. \uAE30\uC5B5\uC774 \uAE30\uAE30\uC640 \uB3C4\uAD6C\uB97C \uB530\uB77C\uB2E4\uB2D9\uB2C8\uB2E4.</small></span></button>
4365
+ <button class="card" id="account-local" aria-checked="false"><span class="tick"></span><span><b>\uC774 \uCEF4\uD4E8\uD130\uC5D0\uB9CC \uB450\uAE30</b><small>\uAC00\uC785 \uC5C6\uC774 \uC9C0\uAE08 \uBC14\uB85C. \uB098\uC911\uC5D0 \uACC4\uC815\uC73C\uB85C \uC62E\uAE38 \uC218 \uC788\uC2B5\uB2C8\uB2E4.</small></span></button>
4366
+
4367
+ <div class="row">
4368
+ <button class="primary" id="account-next" disabled>\uB2E4\uC74C</button>
4369
+ <button class="ghost" id="account-terms">\uC57D\uAD00 \uBCF4\uAE30</button>
4370
+ </div>
4371
+ <p class="note">\uACC4\uC815\uC744 \uB9CC\uB4E4\uBA74 \uB178\uD2B8\uAC00 memgineering \uC11C\uBC84\uC5D0 \uC800\uC7A5\uB429\uB2C8\uB2E4. \uC5B4\uB290 \uCABD\uC744 \uACE0\uB974\uB4E0 \uC6D0\uBCF8\uC740 \uB9C8\uD06C\uB2E4\uC6B4 \uD30C\uC77C\uC774\uACE0, \uC5B8\uC81C\uB4E0 \uD3F4\uB354\uB85C \uAEBC\uB0BC \uC218 \uC788\uC2B5\uB2C8\uB2E4.</p>
4372
+ </section>
4373
+
4374
+ <section id="step-terms" hidden>
4375
+ <h1>\uC57D\uAD00</h1>
4376
+ <div class="draft">
4377
+ <b>\uCD08\uC548\uC785\uB2C8\uB2E4.</b> \uC544\uC9C1 \uBC95\uB960 \uAC80\uD1A0\uB97C \uAC70\uCE58\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC815\uC2DD \uC57D\uAD00\uC774 \uC900\uBE44\uB418\uBA74 \uC774 \uD654\uBA74\uC774 \uAD50\uCCB4\uB418\uACE0, \uADF8 \uC804\uAE4C\uC9C0\uB294 \uC774 \uB0B4\uC6A9\uC744 \uACC4\uC57D\uC73C\uB85C \uC0BC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
4378
+ </div>
4379
+ <div class="terms-body">
4380
+ <h2>\uC774\uC6A9\uC57D\uAD00 (\uCD08\uC548)</h2>
4381
+ <p><b>1. \uC774 \uC11C\uBE44\uC2A4\uAC00 \uD558\uB294 \uC77C.</b> memgineering\uC740 \uC0AC\uC6A9\uC790\uAC00 \uC791\uC131\uD55C \uB178\uD2B8\uB97C \uC800\uC7A5\uD558\uACE0, \uC0AC\uC6A9\uC790\uAC00 \uC5F0\uACB0\uD55C AI \uB3C4\uAD6C\uAC00 \uADF8 \uB178\uD2B8\uB97C \uC77D\uACE0 \uC4F8 \uC218 \uC788\uAC8C \uD569\uB2C8\uB2E4.</p>
4382
+ <p><b>2. \uB178\uD2B8\uC758 \uC18C\uC720\uAD8C.</b> \uC0AC\uC6A9\uC790\uAC00 \uC4F4 \uAC83\uC740 \uC0AC\uC6A9\uC790\uC758 \uAC83\uC785\uB2C8\uB2E4. \uC6B4\uC601\uC790\uB294 \uB178\uD2B8\uC758 \uC18C\uC720\uAD8C\uC744 \uC8FC\uC7A5\uD558\uC9C0 \uC54A\uC73C\uBA70, \uB178\uD2B8\uB97C \uD559\uC2B5\uC5D0 \uC0AC\uC6A9\uD558\uAC70\uB098 \uC81C3\uC790\uC5D0\uAC8C \uD310\uB9E4\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.</p>
4383
+ <p><b>3. \uC5B8\uC81C\uB4E0 \uAC00\uC838\uAC08 \uC218 \uC788\uC2B5\uB2C8\uB2E4.</b> \uC800\uC7A5\uB41C \uB178\uD2B8\uB294 \uC6D0\uB798 \uACBD\uB85C \uADF8\uB300\uB85C \uB9C8\uD06C\uB2E4\uC6B4 \uD30C\uC77C\uB85C \uB0B4\uB824\uBC1B\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC774 \uAD8C\uB9AC\uB97C \uC81C\uD55C\uD558\uB294 \uC870\uAC74\uC740 \uB450\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.</p>
4384
+ <p><b>4. \uC0AD\uC81C.</b> \uBE0C\uB808\uC778\uC744 \uC9C0\uC6B0\uBA74 \uADF8 \uB178\uD2B8\xB7\uAE30\uB85D\xB7\uC77D\uC740 \uC774\uB825\uC774 \uD568\uAED8 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4. \uACC4\uC815\uC744 \uC9C0\uC6B0\uBA74 \uACC4\uC815\uC5D0 \uC18D\uD55C \uBAA8\uB4E0 \uBE0C\uB808\uC778\uC774 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4.</p>
4385
+ <p><b>5. \uBB34\uB8CC \uBC94\uC704.</b> \uBE0C\uB808\uC778 1\uAC1C\uB294 \uACC4\uC18D \uBB34\uB8CC\uC785\uB2C8\uB2E4. \uC720\uB8CC \uD56D\uBAA9\uC774 \uC0DD\uAE30\uBA74 \uBBF8\uB9AC \uC54C\uB9AC\uACE0, \uC774\uBBF8 \uC800\uC7A5\uB41C \uB178\uD2B8\uB97C \uC778\uC9C8\uB85C \uC0BC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.</p>
4386
+ <p><b>6. \uBCF4\uC99D\uD558\uC9C0 \uC54A\uB294 \uAC83.</b> \uC774 \uC11C\uBE44\uC2A4\uB294 \uD604\uC7AC \uC0C1\uD0DC \uADF8\uB300\uB85C \uC81C\uACF5\uB429\uB2C8\uB2E4. \uB370\uC774\uD130\uAC00 \uC5B4\uB5A4 \uC0C1\uD669\uC5D0\uC11C\uB3C4 \uC0AC\uB77C\uC9C0\uC9C0 \uC54A\uB294\uB2E4\uACE0 \uBCF4\uC99D\uD558\uC9C0 \uC54A\uC73C\uBBC0\uB85C, \uC911\uC694\uD55C \uB178\uD2B8\uB294 \uC0AC\uC6A9\uC790 \uCABD\uC5D0\uB3C4 \uC0AC\uBCF8\uC744 \uB450\uC2DC\uAE30\uB97C \uAD8C\uD569\uB2C8\uB2E4.</p>
4387
+ <p><b>7. \uAE08\uC9C0.</b> \uD0C0\uC778\uC758 \uBE44\uBC00\xB7\uC790\uACA9\uC99D\uBA85\uC744 \uBB34\uB2E8\uC73C\uB85C \uC800\uC7A5\uD558\uAC70\uB098, \uC11C\uBE44\uC2A4\uB97C \uACF5\uACA9\uD558\uAC70\uB098, \uBC95\uC744 \uC5B4\uAE30\uB294 \uB370 \uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.</p>
4388
+
4389
+ <h2>\uAC1C\uC778\uC815\uBCF4 \uCC98\uB9AC\uBC29\uCE68 (\uCD08\uC548)</h2>
4390
+ <p><b>1. \uBC1B\uB294 \uAC83.</b> \uAD6C\uAE00 \uB85C\uADF8\uC778\uC73C\uB85C \uBC1B\uB294 \uC774\uBA54\uC77C \uC8FC\uC18C\uC640 \uD45C\uC2DC \uC774\uB984, \uC0AC\uC6A9\uC790\uAC00 \uC800\uC7A5\uD55C \uB178\uD2B8, \uADF8\uB9AC\uACE0 \uBB34\uC5C7\uC744 \uC5B8\uC81C \uD68C\uC0C1\uD588\uB294\uC9C0\uC758 \uAE30\uB85D.</p>
4391
+ <p><b>2. \uD68C\uC0C1 \uAE30\uB85D\uC744 \uB0A8\uAE30\uB294 \uC774\uC720.</b> \uC5B4\uB5A4 \uAE30\uC5B5\uC774 \uC2E4\uC81C\uB85C \uB3C4\uC6C0\uC774 \uB410\uB294\uC9C0\uB97C \uC7AC\uB294 \uC720\uC77C\uD55C \uBC29\uBC95\uC774\uACE0, \uADF8\uAC83\uC73C\uB85C \uB2E4\uC74C \uD68C\uC0C1\uC758 \uC21C\uC11C\uB97C \uC815\uD569\uB2C8\uB2E4. \uC774 \uAE30\uB85D\uC740 \uC0AC\uC6A9\uC790 \uBCF8\uC778\uC758 \uBE0C\uB808\uC778 \uC548\uC5D0\uB9CC \uB0A8\uC2B5\uB2C8\uB2E4.</p>
4392
+ <p><b>3. \uD558\uC9C0 \uC54A\uB294 \uAC83.</b> \uB178\uD2B8\uB97C \uBAA8\uB378 \uD559\uC2B5\uC5D0 \uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB178\uD2B8\uB97C \uAD11\uACE0\uC5D0 \uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB178\uD2B8\uB97C \uC81C3\uC790\uC5D0\uAC8C \uD314\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.</p>
4393
+ <p><b>4. \uC5B4\uB5BB\uAC8C \uC800\uC7A5\uB418\uB294\uAC00.</b> \uACC4\uC815\uC5D0 \uC62C\uB9B0 \uB178\uD2B8\uB294 \uC11C\uBC84\uC5D0 <b>\uD3C9\uBB38\uC73C\uB85C</b> \uC800\uC7A5\uB429\uB2C8\uB2E4. \uC885\uB2E8\uAC04 \uC554\uD638\uD654\uAC00 \uC544\uB2C8\uBA70, \uC11C\uBC84\uB97C \uC6B4\uC601\uD558\uB294 \uC0AC\uB78C\uC740 \uAE30\uC220\uC801\uC73C\uB85C \uB178\uD2B8\uB97C \uC77D\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC77D\uC9C0 \uC54A\uB294 \uAC83\uC740 \uC57D\uC18D\uC774\uC9C0 \uBD88\uAC00\uB2A5\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uADF8\uAC83\uC73C\uB85C \uCDA9\uBD84\uD558\uC9C0 \uC54A\uC740 \uB178\uD2B8\uB294 \uACC4\uC815\uC5D0 \uC62C\uB9AC\uC9C0 \uB9C8\uC2DC\uACE0, \uC774 \uCEF4\uD4E8\uD130\uC5D0\uB9CC \uB450\uC2DC\uAC70\uB098 <code>.memgignore</code>\uB85C \uBE7C\uB450\uC2ED\uC2DC\uC624.</p>
4394
+ <p><b>5. \uC790\uACA9\uC99D\uBA85 \uCC28\uB2E8.</b> API \uD0A4\xB7\uBE44\uBC00\uBC88\uD638\uCC98\uB7FC \uBCF4\uC774\uB294 \uB0B4\uC6A9\uC740 \uC800\uC7A5 \uC804\uC5D0 \uAC70\uBD80\uB429\uB2C8\uB2E4. \uC644\uBCBD\uD55C \uCC28\uB2E8\uC744 \uBCF4\uC99D\uD558\uC9C0\uB294 \uC54A\uC2B5\uB2C8\uB2E4.</p>
4395
+ <p><b>6. \uBCF4\uAD00 \uAE30\uAC04.</b> \uC0AC\uC6A9\uC790\uAC00 \uC9C0\uC6B8 \uB54C\uAE4C\uC9C0 \uBCF4\uAD00\uD569\uB2C8\uB2E4. \uACC4\uC815\uC744 \uC9C0\uC6B0\uBA74 \uD568\uAED8 \uC9C0\uC6C1\uB2C8\uB2E4.</p>
4396
+ <p><b>7. \uBB38\uC758.</b> memgineering@gmail.com</p>
4397
+
4398
+ <h2>\uC544\uC9C1 \uC815\uD558\uC9C0 \uBABB\uD55C \uAC83</h2>
4399
+ <p>\uCD08\uC548\uC5D0 \uC5C6\uB294 \uD56D\uBAA9\uC744 \uC5C6\uB294 \uCC44\uB85C \uB450\uC9C0 \uC54A\uACE0 \uC801\uC5B4 \uB461\uB2C8\uB2E4. \uC544\uB798\uB294 \uC544\uC9C1 \uC815\uD574\uC9C0\uC9C0 \uC54A\uC558\uACE0, \uC815\uC2DD \uC57D\uAD00\uC5D0\uC11C \uB2F5\uD569\uB2C8\uB2E4. \uC9C0\uAE08 \uC774 \uC11C\uBE44\uC2A4\uB97C \uC4F0\uAE30\uB85C \uD558\uC2E0\uB2E4\uBA74 \uC774\uAC83\uB4E4\uC774 \uBBF8\uC815\uC778 \uC0C1\uD0DC\uB85C \uC4F0\uC2DC\uB294 \uAC83\uC785\uB2C8\uB2E4.</p>
4400
+ <p>\xB7 \uC11C\uBE44\uC2A4\uB97C \uC6B4\uC601\uD558\uB294 \uBC95\uC801 \uC8FC\uCCB4\uC640 \uBD84\uC7C1 \uC2DC \uAD00\uD560<br />
4401
+ \xB7 \uC11C\uBC84\uC640 \uB370\uC774\uD130\uAC00 \uC704\uCE58\uD55C \uAD6D\uAC00, \uC678\uBD80 \uCC98\uB9AC\uC5C5\uCCB4<br />
4402
+ \xB7 \uC0AD\uC81C \uC694\uCCAD \uD6C4 \uBC31\uC5C5\uC5D0\uC11C \uC2E4\uC81C\uB85C \uC9C0\uC6CC\uC9C0\uAE30\uAE4C\uC9C0 \uAC78\uB9AC\uB294 \uC2DC\uAC04<br />
4403
+ \xB7 \uBCF4\uC548 \uC0AC\uACE0\uAC00 \uB0AC\uC744 \uB54C \uC54C\uB9AC\uB294 \uBC29\uC2DD\uACFC \uC2DC\uD55C<br />
4404
+ \xB7 \uACC4\uC815 \uC804\uCCB4\uB97C \uC9C0\uC6B0\uB294 \uD654\uBA74 (\uC9C0\uAE08\uC740 \uC694\uCCAD\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4)</p>
4405
+ </div>
4406
+ <div class="row"><button class="primary" id="terms-back">\uB3CC\uC544\uAC00\uAE30</button></div>
4407
+ </section>
4408
+
3524
4409
  <section id="step-tools" hidden>
3525
4410
  <div class="steps" id="count-tools"></div>
3526
4411
  <h1>\uC5B4\uB5A4 AI\uAC00 \uC774 \uAE30\uC5B5\uC744 \uC4F0\uAC8C \uD560\uAE4C\uC694?</h1>
@@ -3537,26 +4422,26 @@ function renderSetupPage() {
3537
4422
  <h1>\uC790\uB3D9\uC73C\uB85C \uCD5C\uC2E0 \uC0C1\uD0DC\uB97C \uC720\uC9C0\uD560\uAE4C\uC694?</h1>
3538
4423
  <p class="sub">\uD558\uB8E8\uC5D0 \uD55C \uBC88 \uC0C8 \uBC84\uC804\uC774 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uACE0, \uC788\uC73C\uBA74 \uC870\uC6A9\uD788 \uC124\uCE58\uD569\uB2C8\uB2E4.</p>
3539
4424
  <button class="card" id="update-on" aria-checked="false"><span class="tick"></span><span><b>\uB124, \uC54C\uC544\uC11C \uCD5C\uC2E0\uC73C\uB85C<span class="badge">\uAD8C\uC7A5</span></b><small>\uB2E4\uC2DC \uC2E0\uACBD \uC4F0\uC9C0 \uC54A\uC544\uB3C4 \uB429\uB2C8\uB2E4.</small></span></button>
3540
- <button class="card" id="update-off" aria-checked="false"><span class="tick"></span><span><b>\uC544\uB2C8\uC694, \uC81C\uAC00 \uC9C1\uC811 \uD558\uACA0\uC2B5\uB2C8\uB2E4</b><small>\uC6D0\uD560 \uB54C <code>memgineering update</code> \uB97C \uC2E4\uD589\uD558\uBA74 \uB429\uB2C8\uB2E4.</small></span></button>
4425
+ <button class="card" id="update-off" aria-checked="false"><span class="tick"></span><span><b>\uC544\uB2C8\uC694, \uC81C\uAC00 \uC9C1\uC811 \uD558\uACA0\uC2B5\uB2C8\uB2E4</b><small>\uC0C8 \uBC84\uC804\uC774 \uB098\uC640\uB3C4 \uADF8\uB300\uB85C \uB461\uB2C8\uB2E4. \uB098\uC911\uC5D0 \uC62C\uB9AC\uACE0 \uC2F6\uC73C\uBA74 \uC4F0\uC2DC\uB294 AI\uC5D0\uAC8C \u201Cmemgineering \uCD5C\uC2E0\uC73C\uB85C \uC62C\uB824\uC918\u201D\uB77C\uACE0 \uD558\uC138\uC694.</small></span></button>
3541
4426
 
3542
4427
  <div id="hook-block" hidden>
3543
4428
  <h2>\uB300\uD654\uB97C \uC0C8\uB85C \uC5F4 \uB54C\uB9C8\uB2E4 \uB178\uD2B8 \uC694\uC57D\uC744 \uBA3C\uC800 \uBCF4\uC5EC\uC904\uAE4C\uC694?</h2>
3544
4429
  <p class="sub">Claude Code\uC5D0\uC11C\uB9CC \uC791\uB3D9\uD569\uB2C8\uB2E4. \uC0C8 \uB300\uD654\uB97C \uC2DC\uC791\uD560 \uB54C, \uCD5C\uADFC \uB178\uD2B8 \uC694\uC57D \uBA87 \uAC1C\uB97C \uBB3B\uC9C0 \uC54A\uC544\uB3C4 AI\uC5D0\uAC8C \uBA3C\uC800 \uAC74\uB135\uB2C8\uB2E4.</p>
3545
- <button class="card" id="hook-on" aria-checked="false"><span class="tick"></span><span><b>\uB124, \uB9E4\uBC88 \uBA3C\uC800 \uAC74\uB124\uC8FC\uC138\uC694<span class="badge">\uAE30\uBCF8</span></b><small>\uC5B4\uB514\uAE4C\uC9C0 \uD588\uB294\uC9C0 AI\uAC00 \uC54C\uACE0 \uC2DC\uC791\uD569\uB2C8\uB2E4.</small></span></button>
4430
+ <button class="card" id="hook-on" aria-checked="false"><span class="tick"></span><span><b>\uB124, \uB9E4\uBC88 \uBA3C\uC800 \uAC74\uB124\uC8FC\uC138\uC694</b><small>\uC5B4\uB514\uAE4C\uC9C0 \uD588\uB294\uC9C0 AI\uAC00 \uC54C\uACE0 \uC2DC\uC791\uD569\uB2C8\uB2E4.</small></span></button>
3546
4431
  <button class="card" id="hook-off" aria-checked="false"><span class="tick"></span><span><b>\uC544\uB2C8\uC694, \uBB3C\uC5B4\uBCFC \uB54C\uB9CC</b><small>\uC81C\uAC00 \uCC3E\uC544\uB2EC\uB77C\uACE0 \uD560 \uB54C\uB9CC \uAE30\uC5B5\uC744 \uAEBC\uB0C5\uB2C8\uB2E4.</small></span></button>
3547
4432
  <div class="warnbox">
3548
4433
  \uC9C1\uC811 \uC4F4 \uAC1C\uC778\uC801\uC778 \uB178\uD2B8\uC758 \uC694\uC57D\uB3C4 \uC5EC\uAE30 \uD3EC\uD568\uB429\uB2C8\uB2E4. \uBB3C\uC5B4\uBCF4\uC9C0 \uC54A\uC544\uB3C4 AI \uC55E\uC5D0 \uB193\uC778\uB2E4\uB294 \uB73B\uC785\uB2C8\uB2E4.
3549
- \uBCF4\uC774\uACE0 \uC2F6\uC9C0 \uC54A\uC740 \uB178\uD2B8\uB294 <code>memgineering exclude &lt;path&gt;</code> \uB85C \uBE7C\uB458 \uC218 \uC788\uACE0,
3550
- \uC774 \uC124\uC815\uC740 \uB098\uC911\uC5D0 \uB2E4\uC2DC \uB04C \uC218 \uC788\uC2B5\uB2C8\uB2E4.
4434
+ \uBCF4\uC774\uACE0 \uC2F6\uC9C0 \uC54A\uC740 \uB178\uD2B8\uAC00 \uC788\uC73C\uBA74 \uC4F0\uC2DC\uB294 AI\uC5D0\uAC8C \u201C\uC774 \uB178\uD2B8\uB294 \uBE7C\uC918\u201D\uB77C\uACE0 \uD558\uBA74 \uB429\uB2C8\uB2E4.
4435
+ \uC774 \uC124\uC815\uB3C4 \uB098\uC911\uC5D0 \uB2E4\uC2DC \uB04C \uC218 \uC788\uC2B5\uB2C8\uB2E4.
3551
4436
  </div>
3552
4437
  </div>
3553
4438
 
3554
- <div class="row"><button class="primary" id="options-next">\uB2E4\uC74C</button><button class="ghost" id="options-back">\uB4A4\uB85C</button></div>
4439
+ <div class="row"><button class="primary" id="options-next" disabled>\uB2E4\uC74C</button><button class="ghost" id="options-back">\uB4A4\uB85C</button></div>
3555
4440
  </section>
3556
4441
 
3557
4442
  <section id="step-brain" hidden>
3558
4443
  <div class="steps" id="count-brain"></div>
3559
- <h1>\uAE30\uC5B5\uC744 \uC5B4\uB514\uC5D0 \uB2F4\uC744\uAE4C\uC694?</h1>
4444
+ <h1>\uC5B4\uB290 \uD3F4\uB354\uB97C \uC4F8\uAE4C\uC694?</h1>
3560
4445
  <p class="sub">\uC774\uBBF8 \uC4F0\uACE0 \uC788\uB294 \uB178\uD2B8 \uD3F4\uB354\uB97C \uADF8\uB300\uB85C \uC77D\uC2B5\uB2C8\uB2E4. \uD3F4\uB354 \uAD6C\uC870\uB294 \uC190\uB300\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.</p>
3561
4446
 
3562
4447
  <div id="candidate-list"></div>
@@ -3593,12 +4478,13 @@ function renderSetupPage() {
3593
4478
  <h1 id="done-title">\uC124\uCE58\uB97C \uB9C8\uCCE4\uC2B5\uB2C8\uB2E4.</h1>
3594
4479
  <p class="sub"><b>\uC4F0\uACE0 \uC788\uB358 AI \uB300\uD654\uB97C \uAED0\uB2E4\uAC00 \uB2E4\uC2DC \uC2DC\uC791\uD574\uC57C \uC801\uC6A9\uB429\uB2C8\uB2E4.</b> \uC548\uB0B4\uBB38\uC740 \uB300\uD654\uAC00 \uC2DC\uC791\uB420 \uB54C \uD55C \uBC88 \uC77D\uD788\uAE30 \uB54C\uBB38\uC785\uB2C8\uB2E4.</p>
3595
4480
  <ul class="plain" id="done-list"></ul>
4481
+ <div class="facts" id="done-account" hidden></div>
3596
4482
  <div class="facts" id="done-brain" hidden></div>
3597
4483
  <div id="done-nobrain" hidden>
3598
- <p class="note"><b>\uC544\uC9C1 \uC5F0\uACB0\uB41C \uB178\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</b> \uD130\uBBF8\uB110\uC5D0\uC11C \uC774\uB807\uAC8C \uC5F0\uACB0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.</p>
4484
+ <p class="note"><b>\uC544\uC9C1 \uC5F0\uACB0\uB41C \uB178\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</b> \uC4F0\uC2DC\uB294 AI\uC5D0\uAC8C \uC774\uB807\uAC8C \uB9D0\uD558\uBA74 \uB429\uB2C8\uB2E4.</p>
3599
4485
  <ul class="plain">
3600
- <li><code>memgineering link &lt;\uB178\uD2B8-\uD3F4\uB354&gt;</code><br /><small>\uBB34\uC5C7\uC774 \uC800\uC7A5\uB418\uB294\uC9C0 \uBA3C\uC800 \uBCF4\uC5EC\uC8FC\uACE0, \uADF8\uB2E4\uC74C \uBB3C\uC5B4\uBD05\uB2C8\uB2E4</small></li>
3601
- <li><code>memgineering init &lt;\uACBD\uB85C&gt;</code><br /><small>\uC544\uC9C1 \uB178\uD2B8\uAC00 \uC5C6\uB2E4\uBA74 \u2014 \uBF08\uB300\uAE4C\uC9C0 \uAC16\uCD98 \uC0C8 \uAE30\uC5B5\uC744 \uB9CC\uB4ED\uB2C8\uB2E4</small></li>
4486
+ <li>\u201C\uB0B4 \uB178\uD2B8 \uD3F4\uB354 \uC5F0\uACB0\uD574\uC918\u201D<br /><small>\uBB34\uC5C7\uC774 \uC800\uC7A5\uB418\uB294\uC9C0 \uBA3C\uC800 \uBCF4\uC5EC\uC8FC\uACE0, \uADF8\uB2E4\uC74C \uBB3C\uC5B4\uBD05\uB2C8\uB2E4</small></li>
4487
+ <li>\u201C\uC0C8 \uAE30\uC5B5 \uD558\uB098 \uB9CC\uB4E4\uC5B4\uC918\u201D<br /><small>\uC544\uC9C1 \uB178\uD2B8\uAC00 \uC5C6\uB2E4\uBA74 \u2014 \uBF08\uB300\uAE4C\uC9C0 \uAC16\uCD98 \uC0C8 \uAE30\uC5B5\uC744 \uB9CC\uB4ED\uB2C8\uB2E4</small></li>
3602
4488
  </ul>
3603
4489
  </div>
3604
4490
  <p class="note">\uC774 \uD0ED\uC740 \uB2EB\uC73C\uC154\uB3C4 \uB429\uB2C8\uB2E4. \uC124\uCE58 \uD654\uBA74\uC740 \uC774\uBBF8 \uC2A4\uC2A4\uB85C \uC885\uB8CC\uD588\uC2B5\uB2C8\uB2E4.</p>
@@ -3606,7 +4492,7 @@ function renderSetupPage() {
3606
4492
 
3607
4493
  <section id="step-closed" hidden>
3608
4494
  <h1>\uB2EB\uC558\uC2B5\uB2C8\uB2E4.</h1>
3609
- <p class="sub">\uC544\uBB34\uAC83\uB3C4 \uBC14\uB00C\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC5B8\uC81C\uB4E0 <code>memgineering setup --web</code> \uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uB429\uB2C8\uB2E4.</p>
4495
+ <p class="sub">\uC544\uBB34\uAC83\uB3C4 \uBC14\uB00C\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uD558\uACE0 \uC2F6\uC73C\uBA74 \uC4F0\uC2DC\uB294 AI\uC5D0\uAC8C \u201Cmemgineering \uC124\uC815 \uD654\uBA74 \uB2E4\uC2DC \uC5F4\uC5B4\uC918\u201D\uB77C\uACE0 \uD558\uC138\uC694.</p>
3610
4496
  </section>
3611
4497
  </main>
3612
4498
 
@@ -3620,8 +4506,9 @@ ${pageScript()}
3620
4506
  function pageScript() {
3621
4507
  return `(function () {
3622
4508
  var token = new URLSearchParams(location.search).get('t') || '';
3623
- var STEPS = ['step-tools', 'step-options', 'step-brain', 'step-review'];
4509
+ var STEPS = ['step-account', 'step-tools', 'step-options', 'step-brain', 'step-review'];
3624
4510
  var state = {
4511
+ account: null,
3625
4512
  tools: [],
3626
4513
  selected: [],
3627
4514
  autoUpdate: 'on',
@@ -3635,30 +4522,59 @@ function pageScript() {
3635
4522
  function el(id) { return document.getElementById(id); }
3636
4523
  function num(n) { return Number(n || 0).toLocaleString('ko-KR'); }
3637
4524
 
4525
+ /*
4526
+ * The server behind this page is a process on this machine, and it stops:
4527
+ * when the setup is applied, when it has been idle a while, when somebody
4528
+ * presses Ctrl-C in the terminal it was launched from. The page stays on
4529
+ * screen after that, and its next request has nothing to reach.
4530
+ *
4531
+ * The browser calls that "Failed to fetch", and that is what the screen used
4532
+ * to show \u2014 a sentence that reads like a bug in the tool and gives no way
4533
+ * forward. It is not a bug and there is a way forward, so it says so.
4534
+ */
3638
4535
  function api(path, options) {
3639
- return fetch(path + '?t=' + encodeURIComponent(token), options).then(function (res) {
3640
- return res.text().then(function (text) {
3641
- var body = {};
3642
- try { body = text ? JSON.parse(text) : {}; } catch (e) { body = {}; }
3643
- if (!res.ok) {
3644
- throw body.error || { message: '\uC124\uCE58 \uD654\uBA74\uC774 \uADF8 \uC694\uCCAD\uC744 \uAC70\uC808\uD588\uC2B5\uB2C8\uB2E4 (' + res.status + ').', hint: '' };
3645
- }
3646
- return body;
3647
- });
3648
- });
4536
+ return fetch(path + '?t=' + encodeURIComponent(token), options).then(
4537
+ function (res) {
4538
+ return res.text().then(function (text) {
4539
+ var body = {};
4540
+ try { body = text ? JSON.parse(text) : {}; } catch (e) { body = {}; }
4541
+ if (!res.ok) {
4542
+ throw body.error || { message: '\uC124\uCE58 \uD654\uBA74\uC774 \uADF8 \uC694\uCCAD\uC744 \uAC70\uC808\uD588\uC2B5\uB2C8\uB2E4 (' + res.status + ').', hint: '' };
4543
+ }
4544
+ return body;
4545
+ });
4546
+ },
4547
+ function () {
4548
+ gone = true;
4549
+ throw {
4550
+ message: '\uC774 \uC124\uCE58 \uD654\uBA74\uC740 \uC774\uBBF8 \uB2EB\uD614\uC2B5\uB2C8\uB2E4.',
4551
+ hint: '\uC4F0\uC2DC\uB294 AI\uC5D0\uAC8C \u201Cmemgineering \uC124\uC815 \uD654\uBA74 \uB2E4\uC2DC \uC5F4\uC5B4\uC918\u201D\uB77C\uACE0 \uD558\uBA74 \uC0C8 \uD654\uBA74\uC774 \uC5F4\uB9BD\uB2C8\uB2E4. \uC5EC\uAE30\uC11C \uACE0\uB978 \uAC83\uB4E4\uC740 \uC544\uC9C1 \uC544\uBB34\uAC83\uB3C4 \uC801\uC6A9\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.',
4552
+ };
4553
+ },
4554
+ );
3649
4555
  }
3650
4556
 
4557
+ /* Once the server is gone it does not come back \u2014 nothing here can retry. */
4558
+ var gone = false;
4559
+
3651
4560
  function fail(err) {
3652
4561
  el('error').hidden = false;
3653
4562
  el('error-msg').textContent = (err && err.message) ? err.message : '\uBB38\uC81C\uAC00 \uC0DD\uACBC\uC2B5\uB2C8\uB2E4.';
3654
4563
  el('error-hint').textContent = (err && err.hint) ? err.hint : '';
4564
+ // A dead server cannot be retried into life, so the buttons stop pretending.
4565
+ // Leaving them live invites a second click, a second identical failure, and
4566
+ // the conclusion that the tool is broken rather than finished.
4567
+ if (gone) {
4568
+ var buttons = document.querySelectorAll('button');
4569
+ for (var i = 0; i < buttons.length; i++) buttons[i].disabled = true;
4570
+ }
3655
4571
  window.scrollTo(0, 0);
3656
4572
  }
3657
4573
 
3658
4574
  function clearError() { el('error').hidden = true; }
3659
4575
 
3660
4576
  function show(id) {
3661
- var all = STEPS.concat(['step-done', 'step-closed']);
4577
+ var all = STEPS.concat(['step-terms', 'step-done', 'step-closed']);
3662
4578
  for (var i = 0; i < all.length; i++) el(all[i]).hidden = all[i] !== id;
3663
4579
  }
3664
4580
 
@@ -3719,11 +4635,33 @@ function pageScript() {
3719
4635
 
3720
4636
  /* ---- step 2: updates, and the session hook ---- */
3721
4637
 
4638
+ /*
4639
+ * Nothing here starts selected, and that is the whole point of the screen.
4640
+ *
4641
+ * Both questions used to arrive pre-answered \u2014 the hook ON whenever a
4642
+ * hook-capable tool was found \u2014 so the screen looked like a question, was
4643
+ * already answered, and got clicked past. The CLI at least says "nobody was
4644
+ * asked about: session-start hook: on" when there is no terminal to ask in.
4645
+ * Here a person is looking straight at it, which makes an unnoticed default
4646
+ * worse rather than better.
4647
+ *
4648
+ * The hook is the one that earns this. It puts note summaries in front of an
4649
+ * agent at the start of every session, and a tester who left it on found a
4650
+ * line from their own journal arriving as card five of every conversation.
4651
+ * Nothing had lied to them; nobody had asked them either.
4652
+ */
3722
4653
  function refreshHookBlock() {
3723
4654
  state.hookCapable = state.tools.some(function (t) {
3724
4655
  return t.hooks && t.detected && state.selected.indexOf(t.id) !== -1;
3725
4656
  });
3726
4657
  el('hook-block').hidden = !state.hookCapable;
4658
+ refreshOptionsNext();
4659
+ }
4660
+
4661
+ /* The next button opens only once both questions here have an answer. */
4662
+ function refreshOptionsNext() {
4663
+ var answered = state.autoUpdateChosen && (!state.hookCapable || state.hookChosen);
4664
+ el('options-next').disabled = !answered;
3727
4665
  }
3728
4666
 
3729
4667
  /* ---- step 3: where the memory lives ---- */
@@ -3763,7 +4701,12 @@ function pageScript() {
3763
4701
  var name = document.createElement('b');
3764
4702
  name.textContent = candidate.label;
3765
4703
  var note = document.createElement('small');
3766
- note.textContent = candidate.path + (candidate.linked ? ' \xB7 \uC774\uBBF8 \uC5F0\uACB0\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4' : '');
4704
+ /* \uAC1C\uC218\uB97C \uBA3C\uC800 \uB9D0\uD55C\uB2E4. \uC774 \uC904\uC774 \uC65C \uBAA9\uB85D\uC5D0 \uC788\uB294\uC9C0\uAC00 \uADF8 \uB2F5\uC774\uAE30 \uB54C\uBB38\uC774\uB2E4 \u2014
4705
+ \uB178\uD2B8 1\uAC1C\uC9DC\uB9AC \uC0AC\uC9C4 \uD3F4\uB354\uC640 434\uAC1C\uC9DC\uB9AC \uB178\uD2B8 \uD3F4\uB354\uAC00 \uAC19\uC740 \uBAA8\uC591\uC73C\uB85C \uB098\uB780\uD788
4706
+ \uC788\uC73C\uBA74, \uACE0\uB974\uB77C\uB294 \uC694\uAD6C\uAC00 \uB2F5\uD560 \uC218 \uC5C6\uB294 \uC694\uAD6C\uAC00 \uB41C\uB2E4. */
4707
+ var count = candidate.notes >= 40 ? '\uB178\uD2B8 40\uAC1C \uC774\uC0C1' : '\uB178\uD2B8 ' + candidate.notes + '\uAC1C';
4708
+ note.textContent = count + ' \xB7 ' + candidate.path +
4709
+ (candidate.linked ? ' \xB7 \uC774\uBBF8 \uC5F0\uACB0\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4' : '');
3767
4710
  text.appendChild(name);
3768
4711
  text.appendChild(note);
3769
4712
  card.appendChild(tick);
@@ -3867,7 +4810,13 @@ function pageScript() {
3867
4810
 
3868
4811
  if (data.already_linked) addLine(box, '\uC774\uBBF8 \uC5F0\uACB0\uB41C \uD3F4\uB354\uC785\uB2C8\uB2E4. \uB2E4\uC2DC \uC77D\uC5B4 \uCD5C\uC2E0 \uC0C1\uD0DC\uB85C \uB9DE\uCDA5\uB2C8\uB2E4.');
3869
4812
 
3870
- addLine(box, '\uC800\uC7A5\uB418\uB294 \uAC83: \uAC01 \uB178\uD2B8\uC758 \uC81C\uBAA9\uACFC \uCCAB \uBB38\uB2E8\uC774 \uC4F0\uC778 \uADF8\uB300\uB85C \uC800\uC7A5\uB429\uB2C8\uB2E4. \uB178\uD2B8 \uC804\uCCB4\uAC00 \uC544\uB2C8\uB77C \uCCAB \uBB38\uB2E8\uC774\uC9C0\uB9CC, \uAC1C\uC778\uC801\uC778 \uB178\uD2B8\uC5D0\uC11C\uB294 \uCCAB \uC904\uC774 \uAC00\uC7A5 \uBBFC\uAC10\uD55C \uC904\uC778 \uACBD\uC6B0\uAC00 \uB9CE\uC2B5\uB2C8\uB2E4.');
4813
+ addLine(box, '\uC774 \uCEF4\uD4E8\uD130\uC758 \uAC80\uC0C9 \uBAA9\uB85D\uC5D0 \uC800\uC7A5\uB418\uB294 \uAC83: \uAC01 \uB178\uD2B8\uC758 \uC81C\uBAA9\uACFC \uCCAB \uBB38\uB2E8\uC774 \uC4F0\uC778 \uADF8\uB300\uB85C \uC800\uC7A5\uB429\uB2C8\uB2E4. \uB178\uD2B8 \uC804\uCCB4\uAC00 \uC544\uB2C8\uB77C \uCCAB \uBB38\uB2E8\uC774\uC9C0\uB9CC, \uAC1C\uC778\uC801\uC778 \uB178\uD2B8\uC5D0\uC11C\uB294 \uCCAB \uC904\uC774 \uAC00\uC7A5 \uBBFC\uAC10\uD55C \uC904\uC778 \uACBD\uC6B0\uAC00 \uB9CE\uC2B5\uB2C8\uB2E4.');
4814
+ /* \uACC4\uC815\uC744 \uACE0\uB978 \uC0AC\uB78C\uC5D0\uAC8C\uB294 \uC704 \uBB38\uC7A5\uC774 "\uC11C\uBC84\uC5D0 \uAC00\uB294 \uAC83\uB3C4 \uCCAB \uBB38\uB2E8\uBFD0"\uC73C\uB85C
4815
+ \uC77D\uD78C\uB2E4. \uC0AC\uC2E4\uC774 \uC544\uB2C8\uB2E4 \u2014 \uACC4\uC815\uC5D0 \uC62C\uB9AC\uBA74 \uD30C\uC77C \uC804\uCCB4\uAC00 \uAC04\uB2E4. \uB450 \uAC00\uC9C0\uB97C
4816
+ \uD55C \uD654\uBA74\uC5D0\uC11C \uAC08\uB77C \uB193\uC9C0 \uC54A\uC73C\uBA74 \uADF8\uAC74 \uC624\uD574\uAC00 \uC544\uB2C8\uB77C \uC774 \uD654\uBA74\uC774 \uB9CC\uB4E0 \uAC70\uC9D3\uB9D0\uC774\uB2E4. */
4817
+ if (state.account === 'cloud') {
4818
+ addLine(box, '\uACC4\uC815\uC5D0 \uC62C\uB9B4 \uB54C \uC800\uC7A5\uB418\uB294 \uAC83: \uB178\uD2B8 \uD30C\uC77C \uC804\uCCB4\uC785\uB2C8\uB2E4. \uCCAB \uBB38\uB2E8\uB9CC\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uC62C\uB9AC\uB294 \uAC83\uC740 \uBCC4\uB3C4 \uB2E8\uACC4\uC774\uACE0, \uC9C0\uAE08 \uC774 \uD654\uBA74\uC740 \uC62C\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.');
4819
+ }
3871
4820
 
3872
4821
  if (data.excluded > 0) addLine(box, '\uADDC\uCE59\uC5D0 \uB530\uB77C \uAC74\uB108\uB6F4 \uACBD\uB85C ' + num(data.excluded) + '\uAC1C');
3873
4822
 
@@ -3927,6 +4876,10 @@ function pageScript() {
3927
4876
  list.textContent = '';
3928
4877
  var rows = [];
3929
4878
 
4879
+ rows.push(state.account === 'cloud'
4880
+ ? '\uAE30\uC5B5 \uC800\uC7A5: \uACC4\uC815\uC5D0 \uB450\uAE30 \u2014 \uC801\uC6A9 \uB4A4 \uAD6C\uAE00 \uB85C\uADF8\uC778\uC774 \uC5F4\uB9BD\uB2C8\uB2E4'
4881
+ : '\uAE30\uC5B5 \uC800\uC7A5: \uC774 \uCEF4\uD4E8\uD130\uC5D0\uB9CC \u2014 \uACC4\uC815\uC744 \uB9CC\uB4E4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4');
4882
+
3930
4883
  state.tools.forEach(function (tool) {
3931
4884
  if (state.selected.indexOf(tool.id) === -1) return;
3932
4885
  rows.push(tool.label + ' \u2014 \uAE30\uC5B5 \uC548\uB0B4\uBB38\uACFC \uC2A4\uD0AC 2\uAC1C\uB97C \uC124\uCE58\uD569\uB2C8\uB2E4');
@@ -3975,6 +4928,7 @@ function pageScript() {
3975
4928
  method: 'POST',
3976
4929
  headers: { 'content-type': 'application/json' },
3977
4930
  body: JSON.stringify({
4931
+ account: state.account,
3978
4932
  tools: state.selected,
3979
4933
  auto_update: state.autoUpdate,
3980
4934
  hook: state.hook,
@@ -4009,6 +4963,51 @@ function pageScript() {
4009
4963
  list.appendChild(row);
4010
4964
  });
4011
4965
 
4966
+ var account = el('done-account');
4967
+ account.textContent = '';
4968
+ account.hidden = true;
4969
+ if (result.account === 'cloud' && result.account_info) {
4970
+ var info = result.account_info;
4971
+ account.hidden = false;
4972
+ var title = document.createElement('div');
4973
+ title.className = 'headline';
4974
+ if (info.signed_in) {
4975
+ title.textContent = '\uACC4\uC815\uC5D0 \uC5F0\uACB0\uB410\uC2B5\uB2C8\uB2E4' + (info.display_name ? ' \u2014 ' + info.display_name : '');
4976
+ account.appendChild(title);
4977
+ } else if (info.user_code) {
4978
+ title.textContent = '\uB85C\uADF8\uC778\uC744 \uB9C8\uCE58\uB824\uBA74 \uC774 \uCF54\uB4DC\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694';
4979
+ account.appendChild(title);
4980
+ var code = document.createElement('div');
4981
+ code.style.cssText = 'font:600 1.8rem/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:0.08em;margin:0.5rem 0';
4982
+ code.textContent = info.user_code;
4983
+ account.appendChild(code);
4984
+ addLine(account, '\uBE0C\uB77C\uC6B0\uC800\uC5D0 \uC5F4\uB9B0 \uC2B9\uC778 \uD654\uBA74\uC5D0\uC11C \uC774 \uCF54\uB4DC\uAC00 \uB9DE\uB294\uC9C0 \uD655\uC778\uD558\uACE0 \uC2B9\uC778\uD558\uC138\uC694.');
4985
+ addLine(account, '\uAE30\uB2E4\uB9AC\uC9C0 \uC54A\uC544\uB3C4 \uB429\uB2C8\uB2E4 \u2014 \uC2B9\uC778\uD558\uBA74 \uB2E4\uC74C \uBA85\uB839\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uC774\uC5B4\uC9D1\uB2C8\uB2E4.');
4986
+ } else {
4987
+ title.textContent = '\uACC4\uC815 \uC5F0\uACB0\uC744 \uC2DC\uC791\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4';
4988
+ account.appendChild(title);
4989
+ addLine(account, info.sign_in_unavailable || '', 'refused');
4990
+ addLine(account, '\uC124\uCE58\uB294 \uB05D\uB0AC\uC2B5\uB2C8\uB2E4. \uACC4\uC815 \uC5C6\uC774\uB3C4 \uC804\uBD80 \uB3D9\uC791\uD569\uB2C8\uB2E4.');
4991
+ }
4992
+
4993
+ /*
4994
+ * \uC9C0\uD0A4\uC9C0 \uBABB\uD55C \uC57D\uC18D\uC744 \uC5EC\uAE30\uC11C \uB9D0\uD55C\uB2E4.
4995
+ *
4996
+ * \uCCAB \uD654\uBA74\uC758 \uBE44\uAD50\uD45C\uB294 \u300C\uACC4\uC815\uC5D0 \uB450\uAE30\u300D \uCE78\uC5D0 "\uB2E4\uB978 \uCEF4\uD4E8\uD130\uC5D0\uC11C\uB3C4 \uAC19\uC740 \uAE30\uC5B5"\uC744
4997
+ * \uCCB4\uD06C\uD574 \uB450\uC5C8\uB2E4. \uADF8\uB7F0\uB370 \uC774 \uD750\uB984\uC774 \uD558\uB294 \uC77C\uC740 \uB85C\uADF8\uC778\uBFD0\uC774\uB2E4 \u2014 \uACC4\uC815\uC5D0
4998
+ * \uBE0C\uB808\uC778\uC744 \uB9CC\uB4E4\uC9C0\uB3C4, \uB178\uD2B8\uB97C \uC62C\uB9AC\uC9C0\uB3C4 \uC54A\uB294\uB2E4. \uADF8 \uC0C1\uD0DC\uB85C \uD654\uBA74\uC744 \uB2EB\uC73C\uBA74
4999
+ * \uC0AC\uC6A9\uC790\uB294 \uC790\uAE30 \uAE30\uC5B5\uC774 \uACC4\uC815\uC5D0 \uC788\uB2E4\uACE0 \uBBFF\uACE0 \uB2E4\uB978 \uCEF4\uD4E8\uD130\uB97C \uCF20\uB2E4.
5000
+ *
5001
+ * \uC62C\uB9AC\uAE30\uB97C \uC5EC\uAE30\uC11C \uC790\uB3D9\uC73C\uB85C \uD558\uC9C0 \uC54A\uB294 \uC774\uC720\uB294 \uB530\uB85C \uC788\uB2E4: \uB0A8\uC758 \uB178\uD2B8\uB97C
5002
+ * \uC11C\uBC84\uB85C \uBCF4\uB0B4\uB294 \uAC83\uC740 \uBB3C\uC5B4\uBCF4\uACE0 \uD560 \uC77C\uC774\uC9C0 \uC124\uCE58\uC758 \uBD80\uC0B0\uBB3C\uB85C \uD560 \uC77C\uC774 \uC544\uB2C8\uB2E4.
5003
+ * \uADF8\uB798\uC11C \uD558\uC9C0 \uC54A\uB418, \uD558\uC9C0 \uC54A\uC558\uB2E4\uACE0 \uB9D0\uD55C\uB2E4.
5004
+ */
5005
+ if (result.brains_linked > 0) {
5006
+ addLine(account, '\uC544\uC9C1 \uC774 \uCEF4\uD4E8\uD130\uC5D0\uB9CC \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uB85C\uADF8\uC778\uC740 \uB410\uC9C0\uB9CC \uB178\uD2B8\uB294 \uC544\uC9C1 \uACC4\uC815\uC5D0 \uC62C\uB77C\uAC00\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.');
5007
+ addLine(account, '\uB2E4\uB978 \uAE30\uAE30\uC5D0\uC11C\uB3C4 \uAC19\uC740 \uAE30\uC5B5\uC744 \uC4F0\uB824\uBA74, \uC4F0\uC2DC\uB294 AI\uC5D0\uAC8C "\uB0B4 \uAE30\uC5B5 \uACC4\uC815\uC5D0 \uC62C\uB824\uC918"\uB77C\uACE0 \uD558\uC138\uC694.');
5008
+ }
5009
+ }
5010
+
4012
5011
  var brain = result.brain;
4013
5012
  var box = el('done-brain');
4014
5013
  box.textContent = '';
@@ -4058,16 +5057,36 @@ function pageScript() {
4058
5057
 
4059
5058
  /* ---- wiring ---- */
4060
5059
 
5060
+ /* ---- step 1: where the memory lives ---- */
5061
+
5062
+ function chooseAccount(which) {
5063
+ state.account = which;
5064
+ pick('account-cloud', 'account-local', which === 'cloud');
5065
+ el('account-next').disabled = false;
5066
+ }
5067
+
5068
+ el('account-cloud').addEventListener('click', function () { chooseAccount('cloud'); });
5069
+ el('account-local').addEventListener('click', function () { chooseAccount('local'); });
5070
+ el('account-next').addEventListener('click', function () { goto(at + 1); });
5071
+ /*
5072
+ * The terms are a screen, not a new tab. A setup flow that sends somebody to
5073
+ * a website mid-install is a setup flow they can lose their place in, and
5074
+ * this page is served by a process that stops \u2014 a tab reopened later would
5075
+ * find nothing there.
5076
+ */
5077
+ el('account-terms').addEventListener('click', function () { clearError(); show('step-terms'); window.scrollTo(0, 0); });
5078
+ el('terms-back').addEventListener('click', function () { goto(at); });
5079
+
4061
5080
  el('tools-next').addEventListener('click', function () { refreshHookBlock(); goto(at + 1); });
4062
5081
  el('tools-cancel').addEventListener('click', function () {
4063
5082
  api('/api/cancel', { method: 'POST' }).catch(function () {});
4064
5083
  show('step-closed');
4065
5084
  });
4066
5085
 
4067
- el('update-on').addEventListener('click', function () { state.autoUpdate = 'on'; pick('update-on', 'update-off', true); });
4068
- el('update-off').addEventListener('click', function () { state.autoUpdate = 'off'; pick('update-on', 'update-off', false); });
4069
- el('hook-on').addEventListener('click', function () { state.hook = true; pick('hook-on', 'hook-off', true); });
4070
- el('hook-off').addEventListener('click', function () { state.hook = false; pick('hook-on', 'hook-off', false); });
5086
+ el('update-on').addEventListener('click', function () { state.autoUpdate = 'on'; state.autoUpdateChosen = true; pick('update-on', 'update-off', true); refreshOptionsNext(); });
5087
+ el('update-off').addEventListener('click', function () { state.autoUpdate = 'off'; state.autoUpdateChosen = true; pick('update-on', 'update-off', false); refreshOptionsNext(); });
5088
+ el('hook-on').addEventListener('click', function () { state.hook = true; state.hookChosen = true; pick('hook-on', 'hook-off', true); refreshOptionsNext(); });
5089
+ el('hook-off').addEventListener('click', function () { state.hook = false; state.hookChosen = true; pick('hook-on', 'hook-off', false); refreshOptionsNext(); });
4071
5090
  el('options-next').addEventListener('click', function () { goto(at + 1); });
4072
5091
  el('options-back').addEventListener('click', function () { goto(at - 1); });
4073
5092
 
@@ -4100,15 +5119,27 @@ function pageScript() {
4100
5119
  state.selected = state.tools.filter(function (t) { return t.detected; }).map(function (t) { return t.id; });
4101
5120
  state.autoUpdate = (data.defaults && data.defaults.auto_update) || 'on';
4102
5121
  state.hook = !!(data.defaults && data.defaults.hook);
5122
+ /* Already connected: the question is answered, not asked again. */
5123
+ if (data.signed_in) {
5124
+ chooseAccount('cloud');
5125
+ el('account-cloud').querySelector('small').textContent = '\uC774\uBBF8 \uB85C\uADF8\uC778\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.';
5126
+ } else if (data.preselect_account) {
5127
+ /* A passed --login/--no-login is what they already said. Selected, not
5128
+ submitted \u2014 they can still change it before pressing \uB2E4\uC74C. */
5129
+ chooseAccount(data.preselect_account);
5130
+ }
4103
5131
  renderTools();
5132
+ // Deliberately NOT drawn as selected here. The values above are what the
5133
+ // installer would use, not what anybody has said \u2014 and drawing them as
5134
+ // selected is what turned two questions into two screens people click
5135
+ // through. refreshHookBlock leaves the next button shut until both are
5136
+ // answered.
4104
5137
  refreshHookBlock();
4105
- pick('update-on', 'update-off', state.autoUpdate === 'on');
4106
- pick('hook-on', 'hook-off', state.hook);
4107
5138
  goto(0);
4108
5139
  })
4109
5140
  .catch(function (err) {
4110
5141
  fail(err);
4111
- show('step-tools');
5142
+ show('step-account');
4112
5143
  });
4113
5144
  })();`;
4114
5145
  }
@@ -4122,15 +5153,16 @@ var init_setup_web_page = __esm({
4122
5153
  var setup_web_exports = {};
4123
5154
  __export(setup_web_exports, {
4124
5155
  browserOpener: () => browserOpener,
5156
+ disclosure: () => disclosure,
4125
5157
  runSetupWeb: () => runSetupWeb,
4126
5158
  startSetupWebServer: () => startSetupWebServer
4127
5159
  });
4128
- import { spawn } from "child_process";
5160
+ import "child_process";
4129
5161
  import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
4130
5162
  import { readdir as readdir6, stat as stat3 } from "fs/promises";
4131
5163
  import { createServer } from "http";
4132
5164
  import { homedir as homedir3 } from "os";
4133
- import { basename, isAbsolute as isAbsolute2, join as join14, relative as relative2, resolve as resolve7 } from "path";
5165
+ import { basename, isAbsolute as isAbsolute3, join as join17, relative as relative2, resolve as resolve8 } from "path";
4134
5166
  async function startSetupWebServer(opts) {
4135
5167
  const token = randomBytes2(32).toString("hex");
4136
5168
  const idleMs = opts.idleMs ?? IDLE_TIMEOUT_MS;
@@ -4142,11 +5174,11 @@ async function startSetupWebServer(opts) {
4142
5174
  sockets.add(socket);
4143
5175
  socket.on("close", () => sockets.delete(socket));
4144
5176
  });
4145
- await new Promise((resolve10, reject) => {
5177
+ await new Promise((resolve11, reject) => {
4146
5178
  server.once("error", reject);
4147
5179
  server.listen(0, "127.0.0.1", () => {
4148
5180
  server.off("error", reject);
4149
- resolve10();
5181
+ resolve11();
4150
5182
  });
4151
5183
  });
4152
5184
  const address = server.address();
@@ -4154,8 +5186,8 @@ async function startSetupWebServer(opts) {
4154
5186
  const origin = `http://127.0.0.1:${port}`;
4155
5187
  const ownOrigins = /* @__PURE__ */ new Set([origin, `http://localhost:${port}`]);
4156
5188
  let resolveDone;
4157
- const done = new Promise((resolve10) => {
4158
- resolveDone = resolve10;
5189
+ const done = new Promise((resolve11) => {
5190
+ resolveDone = resolve11;
4159
5191
  });
4160
5192
  let stopped = false;
4161
5193
  let idleTimer;
@@ -4165,8 +5197,8 @@ async function startSetupWebServer(opts) {
4165
5197
  if (idleTimer) clearTimeout(idleTimer);
4166
5198
  clearTimeout(maxTimer);
4167
5199
  process.off("SIGINT", onInterrupt);
4168
- await new Promise((resolve10) => {
4169
- server.close(() => resolve10());
5200
+ await new Promise((resolve11) => {
5201
+ server.close(() => resolve11());
4170
5202
  for (const socket of sockets) socket.destroy();
4171
5203
  });
4172
5204
  resolveDone(reason);
@@ -4183,7 +5215,13 @@ async function startSetupWebServer(opts) {
4183
5215
  void stop("interrupted");
4184
5216
  }
4185
5217
  process.once("SIGINT", onInterrupt);
4186
- const state = { detected: opts.detected, token, ownOrigins, applied: false };
5218
+ const state = {
5219
+ detected: opts.detected,
5220
+ token,
5221
+ ownOrigins,
5222
+ applied: false,
5223
+ ...opts.preselectAccount === void 0 ? {} : { preselectAccount: opts.preselectAccount }
5224
+ };
4187
5225
  server.on("request", (req, res) => {
4188
5226
  void handle(req, res, state, origin, touch).then((outcome) => {
4189
5227
  if (outcome === "applied") setTimeout(() => void stop("applied"), lingerMs).unref();
@@ -4209,21 +5247,40 @@ async function startSetupWebServer(opts) {
4209
5247
  };
4210
5248
  }
4211
5249
  async function runSetupWeb(opts) {
4212
- const server = await startSetupWebServer({ detected: opts.detected });
5250
+ const server = await startSetupWebServer({
5251
+ detected: opts.detected,
5252
+ ...opts.preselectAccount === void 0 ? {} : { preselectAccount: opts.preselectAccount }
5253
+ });
4213
5254
  const opened = opts.printUrl ? false : await openBrowser(server.url);
4214
5255
  printDual({
4215
- json: { url: server.url, port: server.port, opened },
5256
+ // The URL carries the key that authorizes reading folders and writing into
5257
+ // `~/.claude`, and it was printed on every run — in `--json` too, so it
5258
+ // landed in the transcript of the agent that ran the command, which may be
5259
+ // stored, synced or sent somewhere. That is a different exposure from the
5260
+ // one everybody thinks of with a local token: not a process on this machine
5261
+ // that could read those files anyway, but a copy of the key leaving it.
5262
+ //
5263
+ // So it is emitted only when it is the way in — the browser did not open,
5264
+ // or `--print-url` asked for it. When the browser DID open, the person is
5265
+ // already looking at the screen and nobody needs the string.
5266
+ json: {
5267
+ ...disclosure(server.url, server.port, opened)
5268
+ },
4216
5269
  human: () => {
4217
5270
  if (opened) {
4218
5271
  printHuman("Opened setup in your browser.\n");
4219
- printHuman(c.gray(` If nothing appeared: ${server.url}`));
5272
+ printHuman(
5273
+ c.gray(
5274
+ " Nothing appeared? Run `memgineering setup --web --print-url` and open\n the link yourself \u2014 it is not printed here on purpose, because it\n carries the key that authorizes the install."
5275
+ )
5276
+ );
4220
5277
  } else {
4221
5278
  printHuman("Open this in a browser to finish setting up:\n");
4222
5279
  printHuman(` ${server.url}`);
4223
5280
  }
4224
5281
  printHuman(
4225
5282
  c.gray(
4226
- "\n The link only works on this machine and only once \u2014 it carries a\n one-time key, and this screen closes when you are done.\n Ctrl-C to stop waiting."
5283
+ "\n The link works only on this machine, and only until this screen\n closes \u2014 applying the setup, closing the tab, or an hour, whichever\n comes first. Ctrl-C to stop waiting."
4227
5284
  )
4228
5285
  );
4229
5286
  }
@@ -4232,7 +5289,7 @@ async function runSetupWeb(opts) {
4232
5289
  const ending = {
4233
5290
  applied: "Setup applied. Restart your agent session for it to take effect.",
4234
5291
  cancelled: "Closed without applying \u2014 nothing was changed.",
4235
- idle: "Nothing happened for ten minutes, so the setup screen was closed. Run `memgineering setup --web` again when you are ready.",
5292
+ idle: "Nothing happened for half an hour, so the setup screen was closed. Nothing was changed \u2014 run `memgineering setup --web` again when you are ready.",
4236
5293
  interrupted: "Stopped \u2014 nothing was changed.",
4237
5294
  closed: "Setup screen closed."
4238
5295
  };
@@ -4240,6 +5297,9 @@ async function runSetupWeb(opts) {
4240
5297
  else printHuman(`
4241
5298
  ${reason === "applied" ? c.green(ending[reason]) : c.gray(ending[reason])}`);
4242
5299
  }
5300
+ function disclosure(url, port, opened) {
5301
+ return opened ? { port, opened } : { port, opened, url };
5302
+ }
4243
5303
  async function handle(req, res, state, origin, touch = () => {
4244
5304
  }) {
4245
5305
  const url = new URL(req.url ?? "/", origin);
@@ -4330,7 +5390,11 @@ async function stateDocument(state) {
4330
5390
  hook: state.detected.some((t) => t.hooks)
4331
5391
  },
4332
5392
  brains: cfg.brain.brains.map((b) => b.root),
4333
- applied: state.applied
5393
+ applied: state.applied,
5394
+ /** Null unless a flag already answered it. The screen selects it, never submits it. */
5395
+ preselect_account: state.preselectAccount ?? null,
5396
+ /** Already connected, so the screen can say so instead of offering a second sign-in. */
5397
+ signed_in: (await signInSummary()).signedIn
4334
5398
  };
4335
5399
  }
4336
5400
  async function candidateFolders(linked) {
@@ -4341,16 +5405,17 @@ async function candidateFolders(linked) {
4341
5405
  if (out.length >= MAX_CANDIDATES || seen.has(path)) return;
4342
5406
  seen.add(path);
4343
5407
  if (!await isDirectory(path)) return;
4344
- if (!await holdsNotes(path)) return;
4345
- out.push({ path, label: basename(path), linked: linked.includes(path) });
5408
+ const notes = await countNotes(path);
5409
+ if (notes === 0) return;
5410
+ out.push({ path, label: basename(path), linked: linked.includes(path), notes });
4346
5411
  };
4347
- for (const entry of await entriesOf(join14(home, "Documents"))) {
4348
- await consider(join14(home, "Documents", entry));
5412
+ for (const entry of await entriesOf(join17(home, "Documents"))) {
5413
+ await consider(join17(home, "Documents", entry));
4349
5414
  }
4350
5415
  for (const entry of await entriesOf(home)) {
4351
- if (/^brain/i.test(entry)) await consider(join14(home, entry));
5416
+ if (/^brain/i.test(entry)) await consider(join17(home, entry));
4352
5417
  }
4353
- for (const name of ["notes", "Notes"]) await consider(join14(home, name));
5418
+ for (const name of ["notes", "Notes"]) await consider(join17(home, name));
4354
5419
  return out;
4355
5420
  }
4356
5421
  async function entriesOf(dir) {
@@ -4359,21 +5424,24 @@ async function entriesOf(dir) {
4359
5424
  async function isDirectory(path) {
4360
5425
  return stat3(path).then((s) => s.isDirectory()).catch(() => false);
4361
5426
  }
4362
- async function holdsNotes(dir) {
5427
+ async function countNotes(dir) {
5428
+ const isNote = (n) => /\.(md|markdown)$/i.test(n);
4363
5429
  const entries = await entriesOf(dir);
4364
- if (entries.some((n) => /\.(md|markdown)$/i.test(n))) return true;
5430
+ let total = entries.filter(isNote).length;
5431
+ if (total >= COUNT_CAP) return COUNT_CAP;
4365
5432
  for (const entry of entries.slice(0, 40)) {
4366
- const child = join14(dir, entry);
5433
+ const child = join17(dir, entry);
4367
5434
  if (!await isDirectory(child)) continue;
4368
- if ((await entriesOf(child)).some((n) => /\.(md|markdown)$/i.test(n))) return true;
5435
+ total += (await entriesOf(child)).filter(isNote).length;
5436
+ if (total >= COUNT_CAP) return COUNT_CAP;
4369
5437
  }
4370
- return false;
5438
+ return total;
4371
5439
  }
4372
5440
  function isSystemPath(path) {
4373
5441
  return SYSTEM_ROOTS.some((root) => {
4374
5442
  if (root === "/") return path === "/";
4375
5443
  const rel = relative2(root, path);
4376
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
5444
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
4377
5445
  });
4378
5446
  }
4379
5447
  async function inspectFromScreen(req, res, state) {
@@ -4400,7 +5468,7 @@ async function inspectFromScreen(req, res, state) {
4400
5468
  });
4401
5469
  return "served";
4402
5470
  }
4403
- const path = resolve7(expandHome(asked.trim()));
5471
+ const path = resolve8(expandHome(asked.trim()));
4404
5472
  if (isSystemPath(path)) {
4405
5473
  sendJson(res, 400, {
4406
5474
  error: {
@@ -4492,14 +5560,41 @@ async function applyFromScreen(req, res, state) {
4492
5560
  const changes = await applySetup(choice.value, false);
4493
5561
  const brain = await connectBrain(choice.brain);
4494
5562
  const cfg = await loadConfig();
5563
+ const signIn = choice.account === "cloud" ? await runSignInStep({ login: true, interactive: false, browser: true, dryRun: false }) : void 0;
5564
+ if (signIn) printSignInStep(signIn);
4495
5565
  sendJson(res, 200, {
4496
5566
  applied: true,
5567
+ /**
5568
+ * The answer, and the sign-in it started, under DIFFERENT keys.
5569
+ *
5570
+ * `signInStepJson` returns `{ account: … }`, so spreading it beside a field
5571
+ * also called `account` silently replaced the string with an object and the
5572
+ * screen's `result.account === 'cloud'` stopped being true. Named apart so
5573
+ * the collision cannot come back.
5574
+ */
5575
+ account: choice.account,
5576
+ ...signIn ? { account_info: signInStepJson(signIn)["account"] } : {},
4497
5577
  tools: choice.value.tools.map((t) => t.id),
4498
5578
  auto_update: choice.value.autoUpdate,
4499
5579
  hook: choice.value.hook,
4500
5580
  changes,
4501
5581
  brain,
4502
5582
  brains_linked: cfg.brain.brains.length,
5583
+ /*
5584
+ * The half the screen cannot do, addressed to the thing that can.
5585
+ *
5586
+ * Choosing "keep it in an account" signs in and nothing more — no hosted
5587
+ * brain is created and no note is uploaded. The comparison table on the
5588
+ * first screen ticks "the same memory on another machine" for that column,
5589
+ * so leaving this unsaid ships a promise the flow does not keep.
5590
+ *
5591
+ * Not done automatically here on purpose: sending somebody's notes to a
5592
+ * server is a thing to ask about, not a side effect of an installer. The
5593
+ * agent is who asks, so the agent is who gets told.
5594
+ */
5595
+ ...choice.account === "cloud" && cfg.brain.brains.length > 0 ? {
5596
+ next: "Signed in, but the notes are still only on this machine \u2014 nothing was uploaded. `memgineering push` carries this brain up to the account, which is what makes it readable from another machine. ASK the user before running it: it sends their notes to a server."
5597
+ } : {},
4503
5598
  ...cfg.brain.brains.length === 0 ? {
4504
5599
  // Same sentence the CLI ends on. Which folder holds someone's notes
4505
5600
  // is the one decision neither surface makes for them, and `link` is
@@ -4539,6 +5634,15 @@ function readChoice(parsed, detected) {
4539
5634
  const body = parsed ?? {};
4540
5635
  const ids = Array.isArray(body.tools) ? body.tools.map(String) : [];
4541
5636
  const known = detected.map((t) => t.id).join(", ");
5637
+ if (body.account !== "cloud" && body.account !== "local") {
5638
+ return {
5639
+ error: {
5640
+ code: "invalid_input",
5641
+ message: "account must be 'cloud' or 'local'",
5642
+ hint: `Received: ${JSON.stringify(body.account)}. This is the first question on the screen \u2014 whether the memory follows you across machines, or stays on this one.`
5643
+ }
5644
+ };
5645
+ }
4542
5646
  if (ids.length === 0) {
4543
5647
  return {
4544
5648
  error: {
@@ -4582,7 +5686,8 @@ function readChoice(parsed, detected) {
4582
5686
  // is not an error, it simply has nowhere to go — same rule as `--hook`.
4583
5687
  hook: body.hook === true && tools.some((t) => t.hooks)
4584
5688
  },
4585
- brain: brain.value
5689
+ brain: brain.value,
5690
+ account: body.account
4586
5691
  };
4587
5692
  }
4588
5693
  function readBrainChoice(raw) {
@@ -4619,8 +5724,8 @@ function readBrainChoice(raw) {
4619
5724
  }
4620
5725
  function expandHome(input) {
4621
5726
  if (input === "~") return homedir3();
4622
- if (input.startsWith("~/")) return join14(homedir3(), input.slice(2));
4623
- return isAbsolute2(input) ? input : resolve7(input);
5727
+ if (input.startsWith("~/")) return join17(homedir3(), input.slice(2));
5728
+ return isAbsolute3(input) ? input : resolve8(input);
4624
5729
  }
4625
5730
  function errorEnvelope(err) {
4626
5731
  if (err instanceof MemgError) return { code: err.code, message: err.message, hint: err.hint };
@@ -4657,7 +5762,7 @@ async function readJsonBody(req, res) {
4657
5762
  }
4658
5763
  }
4659
5764
  async function readBody(req) {
4660
- return new Promise((resolve10, reject) => {
5765
+ return new Promise((resolve11, reject) => {
4661
5766
  let size = 0;
4662
5767
  let refused = false;
4663
5768
  let chunks = [];
@@ -4667,14 +5772,14 @@ async function readBody(req) {
4667
5772
  if (!refused) {
4668
5773
  refused = true;
4669
5774
  chunks = [];
4670
- resolve10(null);
5775
+ resolve11(null);
4671
5776
  }
4672
5777
  return;
4673
5778
  }
4674
5779
  chunks.push(chunk);
4675
5780
  });
4676
5781
  req.once("end", () => {
4677
- if (!refused) resolve10(Buffer.concat(chunks).toString("utf8"));
5782
+ if (!refused) resolve11(Buffer.concat(chunks).toString("utf8"));
4678
5783
  });
4679
5784
  req.once("error", reject);
4680
5785
  });
@@ -4719,45 +5824,26 @@ function sendHtml(res, html) {
4719
5824
  });
4720
5825
  res.end(html);
4721
5826
  }
4722
- async function openBrowser(url) {
4723
- const { file, args } = browserOpener(url);
4724
- return new Promise((resolve10) => {
4725
- let settled = false;
4726
- const finish = (opened) => {
4727
- if (settled) return;
4728
- settled = true;
4729
- resolve10(opened);
4730
- };
4731
- const child = spawn(file, args, { detached: true, stdio: "ignore" });
4732
- child.on("error", () => finish(false));
4733
- child.on("exit", (code) => {
4734
- if (code !== null && code !== 0) finish(false);
4735
- });
4736
- child.unref();
4737
- setTimeout(() => finish(true), 300).unref();
4738
- });
4739
- }
4740
- function browserOpener(url) {
4741
- if (process.platform === "darwin") return { file: "open", args: [url] };
4742
- if (process.platform === "win32") return { file: "cmd", args: ["/c", "start", "", url] };
4743
- return { file: "xdg-open", args: [url] };
4744
- }
4745
- var IDLE_TIMEOUT_MS, MAX_LIFETIME_MS, APPLY_LINGER_MS, MAX_BODY_BYTES, MAX_CANDIDATES, PREVIEW_CAP, SYSTEM_ROOTS;
5827
+ var IDLE_TIMEOUT_MS, MAX_LIFETIME_MS, APPLY_LINGER_MS, MAX_BODY_BYTES, MAX_CANDIDATES, COUNT_CAP, PREVIEW_CAP, SYSTEM_ROOTS;
4746
5828
  var init_setup_web = __esm({
4747
5829
  "src/commands/setup-web.ts"() {
4748
5830
  "use strict";
4749
5831
  init_errors();
5832
+ init_open_browser();
5833
+ init_setup_signin();
5834
+ init_login();
4750
5835
  init_config();
4751
5836
  init_ui();
4752
5837
  init_init();
4753
5838
  init_link();
4754
5839
  init_setup();
4755
5840
  init_setup_web_page();
4756
- IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
5841
+ IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
4757
5842
  MAX_LIFETIME_MS = 60 * 60 * 1e3;
4758
5843
  APPLY_LINGER_MS = 1e3;
4759
5844
  MAX_BODY_BYTES = 64 * 1024;
4760
5845
  MAX_CANDIDATES = 8;
5846
+ COUNT_CAP = 40;
4761
5847
  PREVIEW_CAP = 5e3;
4762
5848
  SYSTEM_ROOTS = [
4763
5849
  "/",
@@ -4776,14 +5862,17 @@ var init_setup_web = __esm({
4776
5862
  });
4777
5863
 
4778
5864
  // src/commands/setup.ts
4779
- import { access, mkdir as mkdir9, readFile as readFile15 } from "fs/promises";
5865
+ import { access, mkdir as mkdir12, readFile as readFile18 } from "fs/promises";
4780
5866
  import { homedir as homedir4 } from "os";
4781
- import { dirname as dirname3, join as join15 } from "path";
5867
+ import { dirname as dirname4, join as join18 } from "path";
4782
5868
  import { fileURLToPath } from "url";
4783
- import { Command as Command14 } from "commander";
4784
- import prompts2 from "prompts";
5869
+ import { Command as Command18 } from "commander";
5870
+ import prompts3 from "prompts";
4785
5871
  function setupCommand() {
4786
- return new Command14("setup").description("install into your agents, and point at a brain").option("--agent", "non-interactive: take every answer from flags").option("--human", "interactive: choose from a list").option("--web", "open a screen in the browser and answer it there").option("--print-url", "with --web: print the URL instead of opening a browser").option("--tools <list>", "comma-separated: claude, codex, gemini (default: all detected)").option("--auto-update <choice>", "on | off (default: on, recommended)").option("--hook <choice>", "on | off \u2014 SessionStart resurface, Claude Code only").option("--dry-run", "report what would change, write nothing").option("--silent", "only speak if something changed").action(async (opts) => {
5872
+ return new Command18("setup").description("install into your agents, and point at a brain").option("--agent", "non-interactive: take every answer from flags").option("--human", "interactive: choose from a list").option("--web", "open a screen in the browser and answer it there").option("--print-url", "with --web: print the URL instead of opening a browser").option("--tools <list>", "comma-separated: claude, codex, gemini (default: all detected)").option("--auto-update <choice>", "on | off (default: on, recommended)").option("--hook <choice>", "on | off \u2014 SessionStart resurface, Claude Code only").option("--login", "start a hosted-account sign-in as the first step").option("--no-browser", "with --login: print the approval link instead of opening a browser").option(
5873
+ "--no-login",
5874
+ "skip the sign-in step \u2014 everything installed here works without an account"
5875
+ ).option("--dry-run", "report what would change, write nothing").option("--silent", "only speak if something changed").action(async (opts) => {
4787
5876
  const detected = await detectTools();
4788
5877
  if (detected.length === 0) {
4789
5878
  throw memgError(
@@ -4794,9 +5883,23 @@ function setupCommand() {
4794
5883
  }
4795
5884
  if (opts.web) {
4796
5885
  const { runSetupWeb: runSetupWeb2 } = await Promise.resolve().then(() => (init_setup_web(), setup_web_exports));
4797
- await runSetupWeb2({ detected, printUrl: opts.printUrl === true });
5886
+ await runSetupWeb2({
5887
+ detected,
5888
+ printUrl: opts.printUrl === true,
5889
+ ...opts.login === void 0 ? {} : { preselectAccount: opts.login ? "cloud" : "local" }
5890
+ });
4798
5891
  return;
4799
5892
  }
5893
+ const signIn = await runSignInStep({
5894
+ ...opts.login === void 0 ? {} : { login: opts.login },
5895
+ // A real terminal, not the `--human` flag. Asking for `--human` says
5896
+ // which mode you want; it does not conjure a screen to draw a prompt
5897
+ // on, and prompting without one hangs until something kills it. The
5898
+ // refusal for that combination lives in `resolveMode`, below.
5899
+ interactive: isInteractive() && opts.agent !== true,
5900
+ browser: opts.browser !== false,
5901
+ dryRun: opts.dryRun === true
5902
+ });
4800
5903
  const mode = await resolveMode(opts);
4801
5904
  const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts);
4802
5905
  const defaultsTaken = !mode.chosen ? [
@@ -4813,6 +5916,7 @@ function setupCommand() {
4813
5916
  printDual({
4814
5917
  json: {
4815
5918
  mode: mode.kind,
5919
+ ...signInStepJson(signIn),
4816
5920
  defaults_used: defaultsTaken,
4817
5921
  dry_run: opts.dryRun === true,
4818
5922
  tools: choice.tools.map((t) => t.id),
@@ -4827,11 +5931,13 @@ function setupCommand() {
4827
5931
  human: () => {
4828
5932
  if (opts.silent && changed.length === 0) return;
4829
5933
  if (opts.dryRun) {
5934
+ printSignInStep(signIn);
4830
5935
  printHuman(`Would change ${changed.length} file(s):
4831
5936
  `);
4832
5937
  for (const r of changed) printHuman(c.gray(` ${r.tool} \xB7 ${r.what}`));
4833
5938
  return;
4834
5939
  }
5940
+ printSignInStep(signIn);
4835
5941
  printHuman(`Registered with ${choice.tools.map((t) => t.label).join(", ")}.
4836
5942
  `);
4837
5943
  printHuman(c.gray(` auto-update: ${choice.autoUpdate}`));
@@ -4867,7 +5973,7 @@ function setupCommand() {
4867
5973
  }
4868
5974
  });
4869
5975
  if (mode.kind === "human" && brains.length === 0 && !opts.dryRun && isInteractive()) {
4870
- const { path } = await prompts2({
5976
+ const { path } = await prompts3({
4871
5977
  type: "text",
4872
5978
  name: "path",
4873
5979
  message: "Path to your notes folder (blank to skip)"
@@ -4887,15 +5993,15 @@ async function applySetup(choice, dryRun) {
4887
5993
  results.push({
4888
5994
  tool: tool.label,
4889
5995
  what: "guide",
4890
- result: await injectBlock(join15(homedir4(), tool.hubFile), hub, dryRun)
5996
+ result: await injectBlock(join18(homedir4(), tool.hubFile), hub, dryRun)
4891
5997
  });
4892
5998
  for (const skill of BRAND.subSkills) {
4893
- const content = await loadAsset(join15(skill, "SKILL.md"));
5999
+ const content = await loadAsset(join18(skill, "SKILL.md"));
4894
6000
  results.push({
4895
6001
  tool: tool.label,
4896
6002
  what: skill,
4897
6003
  result: await writeIfDifferent(
4898
- join15(homedir4(), tool.skillsDir, skill, "SKILL.md"),
6004
+ join18(homedir4(), tool.skillsDir, skill, "SKILL.md"),
4899
6005
  content,
4900
6006
  dryRun
4901
6007
  )
@@ -4931,7 +6037,7 @@ async function resolveMode(opts) {
4931
6037
  return { kind: "human", chosen: true };
4932
6038
  }
4933
6039
  if (!isInteractive() || isJsonMode()) return { kind: "agent", chosen: false };
4934
- const { mode } = await prompts2({
6040
+ const { mode } = await prompts3({
4935
6041
  type: "select",
4936
6042
  name: "mode",
4937
6043
  message: "How would you like to set this up?",
@@ -4951,7 +6057,7 @@ async function detectTools() {
4951
6057
  const home = homedir4();
4952
6058
  const found = [];
4953
6059
  for (const tool of TOOLS) {
4954
- if (await exists(join15(home, tool.dir))) found.push(tool);
6060
+ if (await exists2(join18(home, tool.dir))) found.push(tool);
4955
6061
  }
4956
6062
  return found;
4957
6063
  }
@@ -4988,7 +6094,7 @@ async function askHuman(detected, opts) {
4988
6094
  printHuman(c.bold("\nmemgineering \u2014 setup\n"));
4989
6095
  printHuman(c.gray(" Found: ") + detected.map((t) => t.label).join(", "));
4990
6096
  printHuman("");
4991
- const answers = await prompts2([
6097
+ const answers = await prompts3([
4992
6098
  {
4993
6099
  type: "multiselect",
4994
6100
  name: "tools",
@@ -5028,10 +6134,10 @@ async function injectBlock(filePath, content, dryRun) {
5028
6134
  const block = `${BRAND.hubMarkerStart}
5029
6135
  ${content.trimEnd()}
5030
6136
  ${BRAND.hubMarkerEnd}`;
5031
- const existing = await readFile15(filePath, "utf8").catch(() => null);
6137
+ const existing = await readFile18(filePath, "utf8").catch(() => null);
5032
6138
  if (existing === null) {
5033
6139
  if (!dryRun) {
5034
- await mkdir9(dirname3(filePath), { recursive: true });
6140
+ await mkdir12(dirname4(filePath), { recursive: true });
5035
6141
  await writeThenRename(filePath, `${block}
5036
6142
  `);
5037
6143
  }
@@ -5056,17 +6162,17 @@ ${block}
5056
6162
  return "injected";
5057
6163
  }
5058
6164
  async function writeIfDifferent(filePath, content, dryRun) {
5059
- const existing = await readFile15(filePath, "utf8").catch(() => null);
6165
+ const existing = await readFile18(filePath, "utf8").catch(() => null);
5060
6166
  if (existing === content) return "unchanged";
5061
6167
  if (!dryRun) {
5062
- await mkdir9(dirname3(filePath), { recursive: true });
6168
+ await mkdir12(dirname4(filePath), { recursive: true });
5063
6169
  await writeThenRename(filePath, content);
5064
6170
  }
5065
6171
  return existing === null ? "injected" : "updated";
5066
6172
  }
5067
6173
  async function installHook(dryRun) {
5068
- const settingsPath = join15(homedir4(), ".claude", "settings.json");
5069
- const raw = await readFile15(settingsPath, "utf8").catch(() => null);
6174
+ const settingsPath = join18(homedir4(), ".claude", "settings.json");
6175
+ const raw = await readFile18(settingsPath, "utf8").catch(() => null);
5070
6176
  let settings = {};
5071
6177
  if (raw !== null) {
5072
6178
  try {
@@ -5089,14 +6195,14 @@ async function installHook(dryRun) {
5089
6195
  sessionStart.push({ hooks: [{ type: "command", command }] });
5090
6196
  const next = { ...settings, hooks: { ...hooks, SessionStart: sessionStart } };
5091
6197
  if (!dryRun) {
5092
- await mkdir9(dirname3(settingsPath), { recursive: true });
6198
+ await mkdir12(dirname4(settingsPath), { recursive: true });
5093
6199
  await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
5094
6200
  `);
5095
6201
  }
5096
6202
  return raw === null ? "injected" : "updated";
5097
6203
  }
5098
6204
  async function loadAsset(relPath) {
5099
- const raw = await readFile15(join15(await assetRoot(), relPath), "utf8");
6205
+ const raw = await readFile18(join18(await assetRoot(), relPath), "utf8");
5100
6206
  if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
5101
6207
  const end = raw.indexOf("\n---", 4);
5102
6208
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -5104,14 +6210,14 @@ async function loadAsset(relPath) {
5104
6210
  return raw;
5105
6211
  }
5106
6212
  async function assetRoot() {
5107
- const here = dirname3(fileURLToPath(import.meta.url));
5108
- const bundled = join15(here, "..", "assets");
5109
- if (await exists(join15(bundled, BRAND.hubFileName))) return bundled;
6213
+ const here = dirname4(fileURLToPath(import.meta.url));
6214
+ const bundled = join18(here, "..", "assets");
6215
+ if (await exists2(join18(bundled, BRAND.hubFileName))) return bundled;
5110
6216
  let dir = here;
5111
6217
  for (; ; ) {
5112
- const candidate = join15(dir, "packages", "skills");
5113
- if (await exists(join15(candidate, BRAND.hubFileName))) return candidate;
5114
- const parent = dirname3(dir);
6218
+ const candidate = join18(dir, "packages", "skills");
6219
+ if (await exists2(join18(candidate, BRAND.hubFileName))) return candidate;
6220
+ const parent = dirname4(dir);
5115
6221
  if (parent === dir) break;
5116
6222
  dir = parent;
5117
6223
  }
@@ -5121,7 +6227,7 @@ async function assetRoot() {
5121
6227
  "Reinstall with `npm i -g memgineering`. If you are running from a checkout, build first: `pnpm -F memgineering build`."
5122
6228
  );
5123
6229
  }
5124
- async function exists(path) {
6230
+ async function exists2(path) {
5125
6231
  try {
5126
6232
  await access(path);
5127
6233
  return true;
@@ -5136,6 +6242,7 @@ var init_setup = __esm({
5136
6242
  init_atomic();
5137
6243
  init_brand();
5138
6244
  init_errors();
6245
+ init_setup_signin();
5139
6246
  init_config();
5140
6247
  init_ui();
5141
6248
  TOOLS = [
@@ -5168,7 +6275,7 @@ var init_setup = __esm({
5168
6275
  });
5169
6276
 
5170
6277
  // src/program.ts
5171
- import { Command as Command18 } from "commander";
6278
+ import { Command as Command23 } from "commander";
5172
6279
 
5173
6280
  // src/help.ts
5174
6281
  init_ui();
@@ -5189,6 +6296,11 @@ var COMMAND_GROUPS = [
5189
6296
  blurb: "occasional \u2014 correct what is remembered and what is read",
5190
6297
  commands: ["retire", "unretire", "exclude", "unexclude"]
5191
6298
  },
6299
+ {
6300
+ title: "ACCOUNT",
6301
+ blurb: "optional \u2014 connect a hosted brain; everything above works without it",
6302
+ commands: ["login", "logout", "whoami", "push", "pull"]
6303
+ },
5192
6304
  {
5193
6305
  title: "SYSTEM",
5194
6306
  blurb: "install into your agents, and stay current",
@@ -5199,7 +6311,8 @@ var defaultFormatHelp = Help.prototype.formatHelp;
5199
6311
  var ENV_ROWS = [
5200
6312
  ["MEMGINEERING_HOME=<dir>", "state + derived index (default: ~/.memgineering)"],
5201
6313
  ["MEMGINEERING_JSON=1", "force --json framing without the flag"],
5202
- ["MEMGINEERING_NO_UPDATE=1", "skip the update check for this run"]
6314
+ ["MEMGINEERING_NO_UPDATE=1", "skip the update check for this run"],
6315
+ ["MEMGINEERING_API_URL=<url>", "hosted brain server (default: https://api.memgineering.com)"]
5203
6316
  ];
5204
6317
  function commandLabel(sub) {
5205
6318
  const args = sub.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
@@ -5329,7 +6442,16 @@ var OpVerbSchema = z7.enum([
5329
6442
  "retire",
5330
6443
  "unretire",
5331
6444
  "exclude",
5332
- "unexclude"
6445
+ "unexclude",
6446
+ /**
6447
+ * Written only by the hosted brain, when a note is uploaded at a chosen path.
6448
+ *
6449
+ * This CLI never produces one. It is here so that a ledger which came from a
6450
+ * hosted brain PARSES — a downloaded history containing a verb this schema
6451
+ * rejects would fail to load at all, taking the readable ops down with the
6452
+ * one it did not recognise.
6453
+ */
6454
+ "import"
5333
6455
  ]);
5334
6456
  var OpRecordSchema = z7.object({
5335
6457
  op_id: z7.string().min(1),
@@ -5678,18 +6800,18 @@ async function commit2(root, base, next) {
5678
6800
  function excludesPath(content, relPath) {
5679
6801
  return isExcluded(normalizeName(relPath), false, parseIgnoreFile(content));
5680
6802
  }
5681
- async function addIgnoreEntry(root, relPath, base) {
6803
+ function planAddIgnoreEntry(content, relPath) {
5682
6804
  const entry = normalizeName(relPath);
5683
- if (excludesPath(base.content, relPath)) {
5684
- return { ok: true, content: base.content, changed: false };
6805
+ if (excludesPath(content, relPath)) {
6806
+ return { content, changed: false };
5685
6807
  }
5686
- const needsNewline = base.content !== "" && !base.content.endsWith("\n");
5687
- const next = `${base.content}${needsNewline ? "\n" : ""}${entry}
6808
+ const needsNewline = content !== "" && !content.endsWith("\n");
6809
+ const next = `${content}${needsNewline ? "\n" : ""}${entry}
5688
6810
  `;
5689
6811
  const parsed = parseIgnoreFile(next);
5690
6812
  if (parsed.unsupported.includes(entry)) {
5691
6813
  throw Object.assign(new Error(`${FILE} cannot express a rule for this path: ${relPath}`), {
5692
- hint: "Paths containing `[` or a backslash are outside what this ignore parser translates. Rename the file, or add the rule by hand and check `kordis memory recall` no longer returns it. Nothing was written."
6814
+ hint: "Paths containing `[` or a backslash are outside what this ignore parser translates. Rename the file, or add the rule by hand and check `memgineering recall` no longer returns it. Nothing was written."
5693
6815
  });
5694
6816
  }
5695
6817
  if (!excludesPath(next, relPath)) {
@@ -5697,24 +6819,32 @@ async function addIgnoreEntry(root, relPath, base) {
5697
6819
  hint: `This is a defect in the editor, not in your ${FILE}. Nothing was written.`
5698
6820
  });
5699
6821
  }
5700
- assertNothingLost(base.content, next, "excluding");
5701
- return commit2(root, base, next);
6822
+ assertNothingLost(content, next, "excluding");
6823
+ return { content: next, changed: true };
5702
6824
  }
5703
- async function removeIgnoreEntry(root, relPath, base) {
6825
+ function planRemoveIgnoreEntry(content, relPath) {
5704
6826
  const entry = normalizeName(relPath);
5705
- const lines2 = base.content.split("\n");
6827
+ const lines2 = content.split("\n");
5706
6828
  const kept = lines2.filter((line) => normalizeName(line.trim()) !== entry);
5707
- if (kept.length === lines2.length) {
5708
- return { ok: true, content: base.content, changed: false };
5709
- }
6829
+ if (kept.length === lines2.length) return { content, changed: false };
5710
6830
  const next = kept.join("\n");
5711
6831
  if (excludesPath(next, relPath)) {
5712
6832
  throw Object.assign(new Error(`${relPath} is still excluded after removing its line`), {
5713
6833
  hint: `Another rule in ${FILE} or .gitignore also covers this path. Remove that one by hand \u2014 this command will not delete a rule you did not name. Nothing was written.`
5714
6834
  });
5715
6835
  }
5716
- assertNothingLost(next, base.content, "un-excluding");
5717
- return commit2(root, base, next);
6836
+ assertNothingLost(next, content, "un-excluding");
6837
+ return { content: next, changed: true };
6838
+ }
6839
+ async function addIgnoreEntry(root, relPath, base) {
6840
+ const edit = planAddIgnoreEntry(base.content, relPath);
6841
+ if (!edit.changed) return { ok: true, content: base.content, changed: false };
6842
+ return commit2(root, base, edit.content);
6843
+ }
6844
+ async function removeIgnoreEntry(root, relPath, base) {
6845
+ const edit = planRemoveIgnoreEntry(base.content, relPath);
6846
+ if (!edit.changed) return { ok: true, content: base.content, changed: false };
6847
+ return commit2(root, base, edit.content);
5718
6848
  }
5719
6849
 
5720
6850
  // ../../packages/storage-adapter/src/local-file/cas-adapter.ts
@@ -6073,6 +7203,124 @@ async function readLock(path) {
6073
7203
 
6074
7204
  // src/commands/curation.ts
6075
7205
  init_index_build();
7206
+
7207
+ // src/lib/cloud-brain.ts
7208
+ init_api_client();
7209
+ async function openCloudBrain(target) {
7210
+ const credentials = await requireCredentials(target.apiUrl);
7211
+ return {
7212
+ brainId: target.brainId,
7213
+ name: target.name,
7214
+ token: credentials.token,
7215
+ apiUrl: target.apiUrl
7216
+ };
7217
+ }
7218
+ var call = async (brain, method, path, body) => (await apiRequest({
7219
+ method,
7220
+ path: `/v1/brains/${brain.brainId}${path}`,
7221
+ token: brain.token,
7222
+ baseUrl: brain.apiUrl,
7223
+ ...body === void 0 ? {} : { body }
7224
+ })).body;
7225
+ var cloudRecall = (brain, args) => call(brain, "POST", "/recall", {
7226
+ query: args.query,
7227
+ ...args.limit === void 0 ? {} : { limit: args.limit },
7228
+ ...args.scope === void 0 ? {} : { scope: args.scope }
7229
+ });
7230
+ var cloudOpen = (brain, ref) => call(brain, "POST", "/open", { ref });
7231
+ var cloudEvidence = (brain, ref) => call(brain, "POST", "/evidence", { ref });
7232
+ var cloudRemember = (brain, args) => call(brain, "POST", "/remember", {
7233
+ text: args.text,
7234
+ ...args.scope === void 0 ? {} : { scope: args.scope },
7235
+ ...args.reason === void 0 ? {} : { reason: args.reason }
7236
+ });
7237
+ var cloudUndo = (brain, args) => call(brain, "POST", "/undo", {
7238
+ ...args.opId === void 0 ? {} : { op_id: args.opId },
7239
+ ...args.reason === void 0 ? {} : { reason: args.reason }
7240
+ });
7241
+ var cloudLog = (brain, args = {}) => {
7242
+ const query = new URLSearchParams();
7243
+ if (args.limit !== void 0) query.set("limit", String(args.limit));
7244
+ if (args.verbose === true) query.set("verbose", "true");
7245
+ const suffix = query.toString();
7246
+ return call(brain, "GET", `/log${suffix ? `?${suffix}` : ""}`);
7247
+ };
7248
+ var cloudPutNote = (brain, args) => call(brain, "POST", "/notes", args);
7249
+ var cloudListNotes = (brain, args = {}) => {
7250
+ const query = new URLSearchParams();
7251
+ if (args.cursor !== void 0) query.set("cursor", args.cursor);
7252
+ if (args.limit !== void 0) query.set("limit", String(args.limit));
7253
+ if (args.content === true) query.set("content", "1");
7254
+ const suffix = query.toString();
7255
+ return call(brain, "GET", `/notes${suffix ? `?${suffix}` : ""}`);
7256
+ };
7257
+ var cloudRevise = (brain, args) => call(brain, "POST", "/revise", {
7258
+ ref: args.ref,
7259
+ claim: args.claim,
7260
+ ...args.summary === void 0 ? {} : { summary: args.summary },
7261
+ ...args.title === void 0 ? {} : { title: args.title },
7262
+ ...args.validFrom === void 0 ? {} : { valid_from: args.validFrom },
7263
+ ...args.action === void 0 ? {} : { action: args.action },
7264
+ ...args.contradicts === void 0 ? {} : { contradicts: [...args.contradicts] },
7265
+ ...args.reason === void 0 ? {} : { reason: args.reason },
7266
+ ...args.because === void 0 ? {} : { because: args.because },
7267
+ ...args.dryRun === true ? { dry_run: true } : {}
7268
+ });
7269
+ var cloudLifecycle = (brain, action, args) => call(brain, "POST", `/${action}`, {
7270
+ ref: args.ref,
7271
+ ...args.reason === void 0 ? {} : { reason: args.reason }
7272
+ });
7273
+ var cloudExclude = (brain, mode, args) => call(brain, "POST", `/${mode}`, {
7274
+ path: args.path,
7275
+ ...args.reason === void 0 ? {} : { reason: args.reason }
7276
+ });
7277
+ var cloudResurface = (brain, args = {}) => call(brain, "POST", "/resurface", {
7278
+ ...args.limit === void 0 ? {} : { limit: args.limit }
7279
+ });
7280
+ async function listCloudBrains(token, baseUrl) {
7281
+ const res = await apiRequest({
7282
+ method: "GET",
7283
+ path: "/v1/brains",
7284
+ token,
7285
+ baseUrl
7286
+ });
7287
+ return res.body?.brains ?? [];
7288
+ }
7289
+ async function createCloudBrain(token, baseUrl, name) {
7290
+ const res = await apiRequest({
7291
+ method: "POST",
7292
+ path: "/v1/brains",
7293
+ token,
7294
+ baseUrl,
7295
+ body: { name }
7296
+ });
7297
+ return res.body;
7298
+ }
7299
+
7300
+ // src/lib/target.ts
7301
+ init_errors();
7302
+ init_api_client();
7303
+ init_config();
7304
+ init_vault();
7305
+ async function resolveTarget(opts = {}) {
7306
+ if (opts.local !== true) {
7307
+ const cloud = await cloudTarget();
7308
+ if (cloud) return cloud;
7309
+ }
7310
+ const { brain, via } = await resolveBrain(opts.vault);
7311
+ return { kind: "local", brain, via };
7312
+ }
7313
+ async function cloudTarget() {
7314
+ const base = apiUrl();
7315
+ const config = await loadConfig();
7316
+ const cloud = config.brain.cloud;
7317
+ if (!cloud || cloud.api_url !== base) return null;
7318
+ const credentials = await currentCredentials(base);
7319
+ if (!credentials) return null;
7320
+ return { kind: "cloud", brainId: cloud.id, name: cloud.name, apiUrl: base };
7321
+ }
7322
+
7323
+ // src/commands/curation.ts
6076
7324
  init_vault();
6077
7325
  init_ui();
6078
7326
 
@@ -6082,28 +7330,28 @@ init_errors();
6082
7330
  import { Command as Command4 } from "commander";
6083
7331
 
6084
7332
  // src/lib/events.ts
6085
- import { appendFile as appendFile2, mkdir as mkdir7, readFile as readFile12, readdir as readdir4 } from "fs/promises";
6086
- import { join as join11 } from "path";
6087
- import { z as z8 } from "zod";
6088
- var EventSchema = z8.object({
6089
- ts: z8.string(),
6090
- verb: z8.enum(["recall", "open", "resurface"]),
7333
+ import { appendFile as appendFile2, mkdir as mkdir9, readFile as readFile14, readdir as readdir4 } from "fs/promises";
7334
+ import { join as join13 } from "path";
7335
+ import { z as z10 } from "zod";
7336
+ var EventSchema = z10.object({
7337
+ ts: z10.string(),
7338
+ verb: z10.enum(["recall", "open", "resurface"]),
6091
7339
  /** Absent for resurface, which is the point of it. */
6092
- query: z8.string().nullable().default(null),
7340
+ query: z10.string().nullable().default(null),
6093
7341
  /** Where the caller was. The signal resurface ranks by. */
6094
- context_dir: z8.string(),
6095
- detail: z8.string(),
6096
- limit: z8.number().int().positive().nullable().default(null),
7342
+ context_dir: z10.string(),
7343
+ detail: z10.string(),
7344
+ limit: z10.number().int().positive().nullable().default(null),
6097
7345
  /** Ids the call returned — the denominator for "was any of this used". */
6098
- returned_ids: z8.array(z8.string()).default([]),
7346
+ returned_ids: z10.array(z10.string()).default([]),
6099
7347
  /** The one that got opened, when this event is an open. */
6100
- opened_id: z8.string().nullable().default(null)
7348
+ opened_id: z10.string().nullable().default(null)
6101
7349
  });
6102
7350
  function eventsDir(vaultRoot) {
6103
7351
  return stateDirFor(vaultRoot);
6104
7352
  }
6105
7353
  function eventsFile(vaultRoot, ts) {
6106
- return join11(eventsDir(vaultRoot), `events-${ts.slice(0, 7)}.jsonl`);
7354
+ return join13(eventsDir(vaultRoot), `events-${ts.slice(0, 7)}.jsonl`);
6107
7355
  }
6108
7356
  async function recordEvent(vaultRoot, event) {
6109
7357
  try {
@@ -6118,7 +7366,7 @@ async function recordEvent(vaultRoot, event) {
6118
7366
  returned_ids: [...event.returnedIds ?? []],
6119
7367
  opened_id: event.openedId ?? null
6120
7368
  });
6121
- await mkdir7(eventsDir(vaultRoot), { recursive: true });
7369
+ await mkdir9(eventsDir(vaultRoot), { recursive: true });
6122
7370
  await appendFile2(eventsFile(vaultRoot, ts), `${JSON.stringify(parsed)}
6123
7371
  `, {
6124
7372
  encoding: "utf8",
@@ -6136,7 +7384,7 @@ async function readEvents(vaultRoot) {
6136
7384
  }
6137
7385
  const events = [];
6138
7386
  for (const file of files2) {
6139
- const raw = await readFile12(join11(eventsDir(vaultRoot), file), "utf8");
7387
+ const raw = await readFile14(join13(eventsDir(vaultRoot), file), "utf8");
6140
7388
  for (const line of raw.split("\n")) {
6141
7389
  if (line.trim() === "") continue;
6142
7390
  try {
@@ -6202,7 +7450,7 @@ var BASE_FILES = [
6202
7450
  }
6203
7451
  ];
6204
7452
  var HUB_PATH = "00_HUB/HUB.md";
6205
- function bodyOf(content) {
7453
+ function bodyOf2(content) {
6206
7454
  const stripped = content.replace(/^/, "");
6207
7455
  const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
6208
7456
  return (fence ? stripped.slice(fence[0].length) : stripped).trim();
@@ -6210,8 +7458,8 @@ function bodyOf(content) {
6210
7458
  function bodyStillTemplate(path, content) {
6211
7459
  const template = files("brain")[path];
6212
7460
  if (template === void 0) return false;
6213
- const body = bodyOf(content);
6214
- return body !== "" && body === bodyOf(template);
7461
+ const body = bodyOf2(content);
7462
+ return body !== "" && body === bodyOf2(template);
6215
7463
  }
6216
7464
  async function scaffoldingAmong(adapter, notes) {
6217
7465
  const basePaths = new Set(BASE_FILES.map((f) => f.path));
@@ -6287,112 +7535,147 @@ async function assembleStage(adapter, entry, detail, sectionName) {
6287
7535
  init_vault();
6288
7536
  init_ui();
6289
7537
  function openCommand() {
6290
- return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(async (ref, opts) => {
6291
- const detail = parseDetail(opts.detail, opts.section ? "chunks" : "card");
6292
- const { brain } = await resolveBrain(opts.vault);
6293
- const adapter = await openBrainOrExplain(brain.root);
6294
- const built = await openIndex(adapter, brain.root);
6295
- const entry = resolveRef(built.entries, ref);
6296
- await recordEvent(brain.root, {
6297
- verb: "open",
6298
- detail,
6299
- returnedIds: [entry.memory.id],
6300
- openedId: entry.memory.id
6301
- });
6302
- const stage = await assembleStage(adapter, entry, detail, opts.section);
6303
- const claims = entry.claims;
6304
- const now = /* @__PURE__ */ new Date();
6305
- const handle2 = displayHandle(
6306
- entry.memory.id,
6307
- built.entries.map((e) => e.memory.id)
6308
- );
6309
- const scaffolding = (await scaffoldingAmong(adapter, [{ path: entry.path, title: entry.memory.title }])).has(entry.path);
6310
- printDual({
6311
- json: {
6312
- id: entry.memory.id,
6313
- handle: handle2,
6314
- path: entry.path,
7538
+ return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(
7539
+ async (ref, opts) => {
7540
+ const detail = parseDetail(opts.detail, opts.section ? "chunks" : "card");
7541
+ const target = await resolveTarget({
7542
+ ...opts.local === void 0 ? {} : { local: opts.local },
7543
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
7544
+ });
7545
+ if (target.kind === "cloud") {
7546
+ const cloud = await openCloudBrain(target);
7547
+ const found = await cloudOpen(cloud, ref);
7548
+ printDual({
7549
+ json: {
7550
+ id: found.id,
7551
+ handle: found.handle,
7552
+ path: found.path,
7553
+ revision: found.revision,
7554
+ detail,
7555
+ memory: found.memory,
7556
+ claims: found.claims,
7557
+ relations: found.relations,
7558
+ ...detail === "card" ? {} : { body: found.body },
7559
+ brain: { hosted: true, name: target.name }
7560
+ },
7561
+ human: () => {
7562
+ printHuman(`## ${String(found.memory["title"] ?? found.path)}
7563
+ `);
7564
+ const summary = found.memory["summary"];
7565
+ if (typeof summary === "string" && summary !== "") printHuman(`> ${summary}
7566
+ `);
7567
+ printHuman(c.gray(`${found.path} \xB7 ${target.name}`));
7568
+ if (detail !== "card" && found.body !== "") printHuman(`
7569
+ ${found.body}`);
7570
+ }
7571
+ });
7572
+ return;
7573
+ }
7574
+ const brain = target.brain;
7575
+ const adapter = await openBrainOrExplain(brain.root);
7576
+ const built = await openIndex(adapter, brain.root);
7577
+ const entry = resolveRef(built.entries, ref);
7578
+ await recordEvent(brain.root, {
7579
+ verb: "open",
6315
7580
  detail,
6316
- scaffolding,
6317
- memory: entry.memory,
6318
- claims,
6319
- relations: entry.relations,
6320
- ...stage
6321
- },
6322
- human: () => {
6323
- printHuman(`## ${entry.memory.title}
7581
+ returnedIds: [entry.memory.id],
7582
+ openedId: entry.memory.id
7583
+ });
7584
+ const stage = await assembleStage(adapter, entry, detail, opts.section);
7585
+ const claims = entry.claims;
7586
+ const now = /* @__PURE__ */ new Date();
7587
+ const handle2 = displayHandle(
7588
+ entry.memory.id,
7589
+ built.entries.map((e) => e.memory.id)
7590
+ );
7591
+ const scaffolding = (await scaffoldingAmong(adapter, [{ path: entry.path, title: entry.memory.title }])).has(entry.path);
7592
+ printDual({
7593
+ json: {
7594
+ id: entry.memory.id,
7595
+ handle: handle2,
7596
+ path: entry.path,
7597
+ detail,
7598
+ scaffolding,
7599
+ memory: entry.memory,
7600
+ claims,
7601
+ relations: entry.relations,
7602
+ ...stage
7603
+ },
7604
+ human: () => {
7605
+ printHuman(`## ${entry.memory.title}
6324
7606
  `);
6325
- if (scaffolding) {
6326
- printHuman(
6327
- c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote")
6328
- );
6329
- } else {
6330
- printHuman(
6331
- `${confidenceOf(entry.memory, claims)} \xB7 ${freshnessOf(entry.memory, claims, now)} \xB7 ${entry.memory.provenance.mapping}`
6332
- );
6333
- }
6334
- printHuman(`
7607
+ if (scaffolding) {
7608
+ printHuman(
7609
+ c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote")
7610
+ );
7611
+ } else {
7612
+ printHuman(
7613
+ `${confidenceOf(entry.memory, claims)} \xB7 ${freshnessOf(entry.memory, claims, now)} \xB7 ${entry.memory.provenance.mapping}`
7614
+ );
7615
+ }
7616
+ printHuman(`
6335
7617
  \`id: ${entry.memory.id}\` \xB7 \`${entry.path}\`
6336
7618
  `);
6337
- if (entry.memory.summary) printHuman(quoteBlock(entry.memory.summary) + "\n");
6338
- for (const claim of claims) {
6339
- printHuman(`- ${claim.text}`);
6340
- printHuman(c.gray(` ${claim.confidence} \xB7 ${claim.status}`));
6341
- }
6342
- if (claims.length === 0 && scaffolding) {
6343
- printHuman(
6344
- c.gray(
6345
- `_Nobody has written this yet. The line above is the prompt \`init\` left, and the
7619
+ if (entry.memory.summary) printHuman(quoteBlock(entry.memory.summary) + "\n");
7620
+ for (const claim of claims) {
7621
+ printHuman(`- ${claim.text}`);
7622
+ printHuman(c.gray(` ${claim.confidence} \xB7 ${claim.status}`));
7623
+ }
7624
+ if (claims.length === 0 && scaffolding) {
7625
+ printHuman(
7626
+ c.gray(
7627
+ `_Nobody has written this yet. The line above is the prompt \`init\` left, and the
6346
7628
  body is its instructions \u2014 neither is something the user said, so do not answer
6347
7629
  from them._
6348
7630
 
6349
7631
  This one is filled in by asking. \`memgineering onboard\` prints the questions;
6350
7632
  write the body, then record the conclusion:
6351
7633
  memgineering revise ${handle2} --claim "<what they told you>" --summary "<one line>"`
6352
- )
6353
- );
6354
- } else if (claims.length === 0) {
6355
- printHuman(
6356
- c.gray(
6357
- entry.memory.provenance.mapping === "explicit" ? "_The title and summary above came from this note, but it declares no `claim` \u2014 so nothing here states a conclusion. That is what `stated` above refers to: where the fields came from, not that a conclusion exists._" : "_No claim declared \u2014 this card was inferred from the note._"
6358
- )
6359
- );
6360
- printHuman(
6361
- c.gray(
6362
- `To state one:
7634
+ )
7635
+ );
7636
+ } else if (claims.length === 0) {
7637
+ printHuman(
7638
+ c.gray(
7639
+ entry.memory.provenance.mapping === "explicit" ? "_The title and summary above came from this note, but it declares no `claim` \u2014 so nothing here states a conclusion. That is what `stated` above refers to: where the fields came from, not that a conclusion exists._" : "_No claim declared \u2014 this card was inferred from the note._"
7640
+ )
7641
+ );
7642
+ printHuman(
7643
+ c.gray(
7644
+ `To state one:
6363
7645
  memgineering revise ${handle2} --claim "<the conclusion>" --summary "<one line>"
6364
7646
  Add --contradicts <id> when it disagrees with another memory; that edge is what
6365
7647
  makes the two surface together as a conflict.`
6366
- )
6367
- );
6368
- }
6369
- if (stage.outline && stage.outline.length > 0) {
6370
- printHuman("\n### sections\n");
6371
- for (const heading of stage.outline) printHuman(`- ${heading}`);
6372
- printHuman("");
6373
- }
6374
- if (stage.sections) {
6375
- for (const section of stage.sections) {
6376
- printHuman(`
7648
+ )
7649
+ );
7650
+ }
7651
+ if (stage.outline && stage.outline.length > 0) {
7652
+ printHuman("\n### sections\n");
7653
+ for (const heading of stage.outline) printHuman(`- ${heading}`);
7654
+ printHuman("");
7655
+ }
7656
+ if (stage.sections) {
7657
+ for (const section of stage.sections) {
7658
+ printHuman(`
6377
7659
  ### ${section.label}
6378
7660
  `);
6379
- printHuman(quoteBlock(section.body));
7661
+ printHuman(quoteBlock(section.body));
7662
+ }
7663
+ printHuman("");
7664
+ }
7665
+ if (stage.body) {
7666
+ printHuman("\n### full\n");
7667
+ printHuman(quoteBlock(stage.body));
7668
+ printHuman("");
7669
+ }
7670
+ if (detail === "card") {
7671
+ printHuman(
7672
+ c.gray(`Deeper: \`memgineering open ${handle2} --detail summary | chunks | full\``)
7673
+ );
6380
7674
  }
6381
- printHuman("");
6382
- }
6383
- if (stage.body) {
6384
- printHuman("\n### full\n");
6385
- printHuman(quoteBlock(stage.body));
6386
- printHuman("");
6387
- }
6388
- if (detail === "card") {
6389
- printHuman(
6390
- c.gray(`Deeper: \`memgineering open ${handle2} --detail summary | chunks | full\``)
6391
- );
6392
7675
  }
6393
- }
6394
- });
6395
- });
7676
+ });
7677
+ }
7678
+ );
6396
7679
  }
6397
7680
  function resolveRef(entries, ref) {
6398
7681
  const ids = entries.map((e) => e.memory.id);
@@ -6450,8 +7733,16 @@ init_link();
6450
7733
  function lifecycleCommand(action) {
6451
7734
  return new Command6(action).description(
6452
7735
  action === "retire" ? "mark a memory as no longer current \u2014 the note itself is not changed" : "mark a retired memory as current again"
6453
- ).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").action(async (ref, opts) => {
7736
+ ).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (ref, opts) => {
6454
7737
  const rationale = sanitizeRationale(opts.reason);
7738
+ const where = await resolveTarget({
7739
+ ...opts.local === void 0 ? {} : { local: opts.local },
7740
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
7741
+ });
7742
+ if (where.kind === "cloud") {
7743
+ await lifecycleHosted(where, action, ref, opts.reason);
7744
+ return;
7745
+ }
6455
7746
  const { brain } = await resolveBrain(opts.vault);
6456
7747
  const adapter = await LocalFileCasAdapter.openVault(brain.root, {
6457
7748
  createPrefix: brain.observationsDir
@@ -6459,40 +7750,8 @@ function lifecycleCommand(action) {
6459
7750
  const built = await openIndex(adapter, brain.root);
6460
7751
  const entry = resolveRef(built.entries, ref);
6461
7752
  const result = commit(
6462
- {
6463
- candidate: {
6464
- subject: entry.memory.title,
6465
- scope: entry.memory.scope,
6466
- kind: entry.memory.type,
6467
- candidate_claim: entry.memory.summary ?? entry.memory.title,
6468
- entities: [],
6469
- importance: "normal"
6470
- },
6471
- matches: [
6472
- {
6473
- id: entry.memory.id,
6474
- title: entry.memory.title,
6475
- summary: entry.memory.summary,
6476
- path: entry.path,
6477
- revision: entry.revision,
6478
- freshness: "current",
6479
- score: 1
6480
- }
6481
- ],
6482
- contradicts: [],
6483
- scopes: entry.memory.scope ? [entry.memory.scope] : []
6484
- },
6485
- {
6486
- action,
6487
- memory_id: entry.memory.id,
6488
- claim: null,
6489
- reason: opts.reason ?? `${action}d directly`,
6490
- // For a standing change the memory's own summary IS the passage the
6491
- // decision is about: it is what the note currently says, and it is
6492
- // what stops being current.
6493
- source_excerpt: entry.memory.summary ?? entry.memory.title,
6494
- target_path: entry.path
6495
- }
7753
+ asSingleMatch(entry),
7754
+ lifecycleInput(entry, action, opts.reason ?? null)
6496
7755
  );
6497
7756
  if (result.action === "ignore") {
6498
7757
  throw memgError(
@@ -6554,8 +7813,16 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
6554
7813
  function ignoreCommand(mode) {
6555
7814
  return new Command6(mode).description(
6556
7815
  mode === "exclude" ? "stop reading a note entirely \u2014 the note itself is not changed" : "read a note again after excluding it"
6557
- ).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the note is in").action(async (path, opts) => {
7816
+ ).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the note is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (path, opts) => {
6558
7817
  const rationale = sanitizeRationale(opts.reason);
7818
+ const where = await resolveTarget({
7819
+ ...opts.local === void 0 ? {} : { local: opts.local },
7820
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
7821
+ });
7822
+ if (where.kind === "cloud") {
7823
+ await ignoreHosted(where, mode, path, opts.reason);
7824
+ return;
7825
+ }
6559
7826
  const { brain } = await resolveBrain(opts.vault);
6560
7827
  const adapter = await LocalFileCasAdapter.openVault(brain.root, {
6561
7828
  createPrefix: brain.observationsDir
@@ -6608,6 +7875,57 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
6608
7875
  });
6609
7876
  });
6610
7877
  }
7878
+ async function lifecycleHosted(where, action, ref, reason) {
7879
+ const brain = await openCloudBrain(where);
7880
+ const result = await cloudLifecycle(brain, action, {
7881
+ ref,
7882
+ ...reason === void 0 ? {} : { reason }
7883
+ });
7884
+ printDual({
7885
+ json: { brain: where.name, ...result },
7886
+ human: () => {
7887
+ printHuman(
7888
+ action === "retire" ? `Retired in **${where.name}**.
7889
+ ` : `Current again in **${where.name}**.
7890
+ `
7891
+ );
7892
+ printHuman(`\`${result.target}\`
7893
+ `);
7894
+ printHuman(c.gray(result.note));
7895
+ printHuman(
7896
+ c.gray(
7897
+ `
7898
+ To put it back: \`memgineering ${action === "retire" ? "unretire" : "retire"} ${ref}\``
7899
+ )
7900
+ );
7901
+ }
7902
+ });
7903
+ }
7904
+ async function ignoreHosted(where, mode, path, reason) {
7905
+ const brain = await openCloudBrain(where);
7906
+ const result = await cloudExclude(brain, mode, {
7907
+ path,
7908
+ ...reason === void 0 ? {} : { reason }
7909
+ });
7910
+ printDual({
7911
+ json: { brain: where.name, ...result },
7912
+ human: () => {
7913
+ if (!result.changed) {
7914
+ printHuman(c.gray(result.note));
7915
+ return;
7916
+ }
7917
+ printHuman(
7918
+ mode === "exclude" ? `No longer reading \`${result.path}\` in **${where.name}**.
7919
+ ` : `Reading \`${result.path}\` again in **${where.name}**.
7920
+ `
7921
+ );
7922
+ printHuman(c.gray(result.note));
7923
+ if (result.op_id) printHuman(c.gray(`
7924
+ Undo: \`memgineering ${opposite(mode)} ${path}\``));
7925
+ }
7926
+ });
7927
+ }
7928
+ var opposite = (mode) => mode === "exclude" ? "unexclude" : "exclude";
6611
7929
  async function mustMatchAFile(root, adapter, path) {
6612
7930
  const wanted = path.normalize("NFC").replace(/^\/+/, "");
6613
7931
  const absolute = resolve6(root, wanted);
@@ -6633,8 +7951,31 @@ init_index_build();
6633
7951
  init_vault();
6634
7952
  init_ui();
6635
7953
  function evidenceCommand() {
6636
- return new Command7("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").action(async (ref, opts) => {
6637
- const { brain } = await resolveBrain(opts.vault);
7954
+ return new Command7("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").action(async (ref, opts) => {
7955
+ const target = await resolveTarget({
7956
+ ...opts.local === void 0 ? {} : { local: opts.local },
7957
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
7958
+ });
7959
+ if (target.kind === "cloud") {
7960
+ const cloud = await openCloudBrain(target);
7961
+ const report2 = await cloudEvidence(cloud, ref);
7962
+ printDual({
7963
+ json: { ...report2, brain: { hosted: true, name: target.name } },
7964
+ human: () => {
7965
+ const window = report2["counts_cover_days"];
7966
+ printHuman(
7967
+ `surfaced ${String(report2["surfaced"] ?? 0)} \xB7 opened ${String(report2["opened"] ?? 0)}`
7968
+ );
7969
+ if (typeof window === "number") {
7970
+ printHuman(c.gray(` counts cover the last ${window} days, not all time`));
7971
+ }
7972
+ const why = report2["last_reason"];
7973
+ if (typeof why === "string" && why !== "") printHuman(c.gray(` last change: ${why}`));
7974
+ }
7975
+ });
7976
+ return;
7977
+ }
7978
+ const brain = target.brain;
6638
7979
  const adapter = await openBrainOrExplain(brain.root);
6639
7980
  const built = await openIndex(adapter, brain.root);
6640
7981
  const entry = resolveRef(built.entries, ref);
@@ -6798,8 +8139,41 @@ import { Command as Command8 } from "commander";
6798
8139
  init_vault();
6799
8140
  init_ui();
6800
8141
  function logCommand() {
6801
- return new Command8("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").action(async (opts) => {
6802
- const { brain } = await resolveBrain(opts.vault);
8142
+ return new Command8("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").action(async (opts) => {
8143
+ const target = await resolveTarget({
8144
+ ...opts.local === void 0 ? {} : { local: opts.local },
8145
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
8146
+ });
8147
+ if (target.kind === "cloud") {
8148
+ const cloud = await openCloudBrain(target);
8149
+ const history = await cloudLog(cloud, {
8150
+ limit: opts.limit,
8151
+ ...opts.verbose === true ? { verbose: true } : {}
8152
+ });
8153
+ printDual({
8154
+ json: { ...history, brain: { hosted: true, name: target.name } },
8155
+ human: () => {
8156
+ const ops2 = history["operations"] ?? [];
8157
+ const diverged2 = history["divergence"] ?? [];
8158
+ printHuman(`## log \u2014 ${target.name} (${ops2.length} operations)
8159
+ `);
8160
+ for (const op of ops2) {
8161
+ printHuman(
8162
+ `${String(op["ts"] ?? "")} ${String(op["verb"] ?? "")} ${String(op["target"] ?? "")}`
8163
+ );
8164
+ printHuman(c.gray(` \`${String(op["op_id"] ?? "")}\``));
8165
+ }
8166
+ if (diverged2.length > 0) {
8167
+ printHuman(
8168
+ c.yellow(`
8169
+ ${diverged2.length} of these no longer match the note on the server.`)
8170
+ );
8171
+ }
8172
+ }
8173
+ });
8174
+ return;
8175
+ }
8176
+ const brain = target.brain;
6803
8177
  const adapter = await openBrainOrExplain(brain.root);
6804
8178
  const { ops, skipped } = await readOps(brain.root);
6805
8179
  const recent = ops.slice(-opts.limit).reverse();
@@ -6896,12 +8270,111 @@ async function readOrNull(adapter, path) {
6896
8270
  }
6897
8271
  }
6898
8272
 
8273
+ // src/program.ts
8274
+ init_login();
8275
+
8276
+ // src/commands/logout.ts
8277
+ init_errors();
8278
+ init_api_client();
8279
+ init_credentials();
8280
+ init_pending_login();
8281
+ init_ui();
8282
+ import { Command as Command10 } from "commander";
8283
+ function logoutCommand() {
8284
+ return new Command10("logout").description("sign out of a hosted brain").option("--all", "revoke every token on the account, not just this machine\u2019s").option(
8285
+ "--local",
8286
+ "forget the token here without telling the server \u2014 only for a server you cannot reach"
8287
+ ).action(async (opts) => {
8288
+ const base = apiUrl();
8289
+ const credentials = await readCredentials();
8290
+ await clearPendingLogin();
8291
+ if (!credentials) {
8292
+ printDual({
8293
+ json: { signed_out: true, was_signed_in: false, api_url: base },
8294
+ human: () => printHuman("Not signed in \u2014 nothing to do.")
8295
+ });
8296
+ return;
8297
+ }
8298
+ if (opts.local) {
8299
+ await clearCredentials();
8300
+ printDual({
8301
+ json: { signed_out: true, revoked_on_server: false, api_url: credentials.api_url },
8302
+ human: () => {
8303
+ printHuman("Forgotten here.");
8304
+ printHuman(
8305
+ c.yellow(
8306
+ " The token was NOT revoked \u2014 it still works for anyone holding a copy.\n Run `memgineering logout --all` once the server is reachable."
8307
+ )
8308
+ );
8309
+ }
8310
+ });
8311
+ return;
8312
+ }
8313
+ let revoked = null;
8314
+ try {
8315
+ if (opts.all) {
8316
+ const res = await apiRequest({
8317
+ method: "POST",
8318
+ path: "/v1/tokens/revoke-all",
8319
+ token: credentials.token,
8320
+ baseUrl: credentials.api_url
8321
+ });
8322
+ revoked = res.body?.revoked ?? null;
8323
+ } else {
8324
+ const me = await apiRequest({
8325
+ method: "GET",
8326
+ path: "/v1/me",
8327
+ token: credentials.token,
8328
+ baseUrl: credentials.api_url
8329
+ });
8330
+ const id = me.body?.tokens?.current_id;
8331
+ if (id) {
8332
+ await apiRequest({
8333
+ method: "DELETE",
8334
+ path: `/v1/tokens/${id}`,
8335
+ token: credentials.token,
8336
+ baseUrl: credentials.api_url
8337
+ });
8338
+ }
8339
+ revoked = 1;
8340
+ }
8341
+ } catch (err) {
8342
+ const code = err instanceof MemgError ? err.code : "";
8343
+ if (code !== "not_authenticated" && code !== "not_found") {
8344
+ throw err instanceof MemgError ? new MemgError({
8345
+ code: err.code,
8346
+ message: err.message,
8347
+ hint: `${err.hint} The token is still stored here and still works; nothing was changed. \`memgineering logout --local\` forgets it on this machine without revoking it.`
8348
+ }) : err;
8349
+ }
8350
+ }
8351
+ await clearCredentials();
8352
+ printDual({
8353
+ json: {
8354
+ signed_out: true,
8355
+ revoked_on_server: true,
8356
+ revoked,
8357
+ all: opts.all === true,
8358
+ api_url: credentials.api_url
8359
+ },
8360
+ human: () => {
8361
+ printHuman(opts.all ? "Signed out everywhere." : "Signed out.");
8362
+ printHuman(
8363
+ c.gray(
8364
+ opts.all ? " Every token on the account is dead. `memgineering login` starts a new one." : " This machine\u2019s token is revoked. Other machines are unaffected."
8365
+ )
8366
+ );
8367
+ }
8368
+ });
8369
+ });
8370
+ }
8371
+
6899
8372
  // src/commands/onboard.ts
6900
- import { Command as Command9 } from "commander";
8373
+ import { Command as Command11 } from "commander";
6901
8374
  init_vault();
6902
8375
  init_ui();
6903
8376
  function onboardCommand() {
6904
- return new Command9("onboard").description("what to ask so the brain knows who it belongs to").option("--vault <path>", "which brain").action(async (opts) => {
8377
+ return new Command11("onboard").description("what to ask so the brain knows who it belongs to").option("--vault <path>", "which brain").action(async (opts) => {
6905
8378
  const { brain } = await resolveBrain(opts.vault);
6906
8379
  const adapter = await openBrainOrExplain(brain.root);
6907
8380
  const untouched = await untouchedBase(
@@ -6963,48 +8436,402 @@ function onboardCommand() {
6963
8436
  });
6964
8437
  }
6965
8438
 
8439
+ // src/commands/pull.ts
8440
+ init_errors();
8441
+ init_api_client();
8442
+ import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
8443
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join16, resolve as resolve7, sep as sep2 } from "path";
8444
+ import { Command as Command12 } from "commander";
8445
+ init_ui();
8446
+ function pullCommand() {
8447
+ return new Command12("pull").description("download your hosted brain to a folder on this machine").argument("[destination]", "folder to write into (created if missing)").option("--brain <name>", "which hosted brain to download").option("--dry-run", "say what would be written and write nothing").action(async (destination, opts) => {
8448
+ const source = await resolveSource(opts.brain);
8449
+ const target = resolve7(destination ?? `./${source.name}`);
8450
+ const first = await cloudListNotes(source, { limit: 1 });
8451
+ if (first.total === 0) {
8452
+ throw memgError(
8453
+ "invalid_input",
8454
+ `\u201C${source.name}\u201D has no notes to download`,
8455
+ "Nothing was written. `memgineering push` carries a brain on this disk up to the hosted one, and `remember` writes a first note into it."
8456
+ );
8457
+ }
8458
+ if (opts.dryRun) {
8459
+ const preview = await collectPaths(source);
8460
+ printDual({
8461
+ json: {
8462
+ dry_run: true,
8463
+ brain: { id: source.brainId, name: source.name },
8464
+ into: target,
8465
+ notes: first.total,
8466
+ excluded: preview.filter((n) => n.excluded === true).length,
8467
+ paths: preview.slice(0, 100).map((n) => n.path),
8468
+ ...preview.length > 100 ? { paths_truncated: preview.length - 100 } : {}
8469
+ },
8470
+ human: () => {
8471
+ printHuman(`## pull (dry run)
8472
+ `);
8473
+ printHuman(`${first.total} note(s) from \u201C${source.name}\u201D into \`${target}\`
8474
+ `);
8475
+ for (const note of preview.slice(0, 20)) {
8476
+ printHuman(c.gray(` ${note.path}${note.excluded === true ? " (excluded)" : ""}`));
8477
+ }
8478
+ if (preview.length > 20) printHuman(c.gray(` \u2026 and ${preview.length - 20} more`));
8479
+ printHuman(
8480
+ c.gray(
8481
+ "\nNothing was written. A real run never replaces a file that is already\nthere \u2014 it skips it and says so."
8482
+ )
8483
+ );
8484
+ }
8485
+ });
8486
+ return;
8487
+ }
8488
+ progress(`downloading ${first.total} note(s) from \u201C${source.name}\u201D\u2026`);
8489
+ const written = [];
8490
+ const already = [];
8491
+ const excluded = [];
8492
+ let cursor;
8493
+ do {
8494
+ const page = await cloudListNotes(source, {
8495
+ ...cursor === void 0 ? {} : { cursor },
8496
+ content: true
8497
+ });
8498
+ for (const note of page.notes) {
8499
+ const file = safeJoin(target, note.path);
8500
+ if (await exists(file)) {
8501
+ already.push(note.path);
8502
+ continue;
8503
+ }
8504
+ await mkdir11(dirname3(file), { recursive: true });
8505
+ await writeFile9(file, note.content ?? "", "utf8");
8506
+ written.push(note.path);
8507
+ if (note.excluded === true) excluded.push(note.path);
8508
+ }
8509
+ cursor = page.next_cursor;
8510
+ } while (cursor);
8511
+ printDual({
8512
+ json: {
8513
+ brain: { id: source.brainId, name: source.name },
8514
+ into: target,
8515
+ written: written.length,
8516
+ already_there: already.length,
8517
+ excluded: excluded.length,
8518
+ next: `\`memgineering link ${target}\` reads this folder as a brain on this machine. Until then it is markdown and nothing else.`
8519
+ },
8520
+ human: () => {
8521
+ printHuman(c.green(`\u2713 ${written.length} note(s) written to \`${target}\``));
8522
+ if (already.length > 0) {
8523
+ printHuman(
8524
+ c.gray(` ${already.length} were already there \u2014 left untouched, not overwritten`)
8525
+ );
8526
+ }
8527
+ if (excluded.length > 0) {
8528
+ printHuman(
8529
+ c.yellow(` ${excluded.length} of them are notes this brain excludes from recall`) + c.gray("\n they came down because they are yours; `.memgignore` came with them")
8530
+ );
8531
+ }
8532
+ printHuman(
8533
+ c.gray(
8534
+ `
8535
+ This is a folder of markdown, not yet a brain on this machine.
8536
+ \`memgineering link ${target}\` makes it one \u2014 it shows what it would read first.`
8537
+ )
8538
+ );
8539
+ }
8540
+ });
8541
+ });
8542
+ }
8543
+ async function collectPaths(source) {
8544
+ const all = [];
8545
+ let cursor;
8546
+ do {
8547
+ const page = await cloudListNotes(source, cursor === void 0 ? {} : { cursor });
8548
+ all.push(...page.notes);
8549
+ cursor = page.next_cursor;
8550
+ } while (cursor);
8551
+ return all;
8552
+ }
8553
+ async function resolveSource(named) {
8554
+ const base = apiUrl();
8555
+ const credentials = await requireCredentials(base);
8556
+ const brains = await listCloudBrains(credentials.token, base);
8557
+ if (brains.length === 0) {
8558
+ throw memgError(
8559
+ "invalid_input",
8560
+ "this account has no hosted brains",
8561
+ "Nothing to download. `memgineering push` carries the brain on this disk up to a hosted one."
8562
+ );
8563
+ }
8564
+ const found = named ? brains.find((b) => b.name === named || b.id === named) : brains.length === 1 ? brains[0] : void 0;
8565
+ if (!found) {
8566
+ throw memgError(
8567
+ "invalid_input",
8568
+ named ? `no hosted brain called \u201C${named}\u201D` : "more than one hosted brain \u2014 say which",
8569
+ `Pass one of: ${brains.map((b) => b.name).join(", ")} (e.g. \`memgineering pull --brain ${brains[0]?.name}\`)`
8570
+ );
8571
+ }
8572
+ return { brainId: found.id, name: found.name, token: credentials.token, apiUrl: base };
8573
+ }
8574
+ function safeJoin(root, relative4) {
8575
+ if (isAbsolute2(relative4)) {
8576
+ throw memgError(
8577
+ "server_refused",
8578
+ `the server sent an absolute path: ${relative4}`,
8579
+ "Nothing was written for it. This should not happen \u2014 please report it."
8580
+ );
8581
+ }
8582
+ const full = resolve7(join16(root, relative4));
8583
+ if (full !== root && !full.startsWith(root + sep2)) {
8584
+ throw memgError(
8585
+ "server_refused",
8586
+ `the server sent a path that escapes the destination: ${relative4}`,
8587
+ "Nothing was written for it. This should not happen \u2014 please report it."
8588
+ );
8589
+ }
8590
+ return full;
8591
+ }
8592
+ async function exists(file) {
8593
+ try {
8594
+ await readFile16(file);
8595
+ return true;
8596
+ } catch {
8597
+ return false;
8598
+ }
8599
+ }
8600
+
8601
+ // src/commands/push.ts
8602
+ init_errors();
8603
+ init_config();
8604
+ init_api_client();
8605
+ import { Command as Command13 } from "commander";
8606
+ init_vault();
8607
+ init_ui();
8608
+ function pushCommand() {
8609
+ return new Command13("push").description("upload the brain on this machine to your hosted one").option("--vault <path>", "which local brain to upload").option("--brain <name>", "the hosted brain to upload into (created if it does not exist)").option("--dry-run", "list what would be uploaded and send nothing").action(async (opts) => {
8610
+ const { brain: local } = await resolveBrain(opts.vault);
8611
+ const adapter = await openBrainOrExplain(local.root);
8612
+ const scan = await adapter.scan();
8613
+ const paths = scan.files.filter((p) => /\.(md|markdown)$/i.test(p));
8614
+ if (paths.length === 0) {
8615
+ throw memgError(
8616
+ "invalid_input",
8617
+ `there is nothing to upload in ${local.root}`,
8618
+ "This brain has no markdown notes that the rules allow. `memgineering reindex` prints what is being left out and why."
8619
+ );
8620
+ }
8621
+ if (opts.dryRun) {
8622
+ printDual({
8623
+ json: {
8624
+ dry_run: true,
8625
+ from: local.root,
8626
+ notes: paths.length,
8627
+ paths: paths.slice(0, 100),
8628
+ ...paths.length > 100 ? { paths_truncated: paths.length - 100 } : {}
8629
+ },
8630
+ human: () => {
8631
+ printHuman(`## push (dry run)
8632
+ `);
8633
+ printHuman(`${paths.length} note(s) from \`${local.root}\`
8634
+ `);
8635
+ for (const path of paths.slice(0, 20)) printHuman(c.gray(` ${path}`));
8636
+ if (paths.length > 20) printHuman(c.gray(` \u2026 and ${paths.length - 20} more`));
8637
+ printHuman(
8638
+ c.gray(
8639
+ "\nNothing was sent. Notes carrying credential material are refused by the\nserver and would be listed in the summary; a dry run cannot know which,\nbecause it does not read the bytes."
8640
+ )
8641
+ );
8642
+ }
8643
+ });
8644
+ return;
8645
+ }
8646
+ const destination = await resolveDestination(opts.brain, local.root);
8647
+ progress(`uploading ${paths.length} note(s) to \u201C${destination.name}\u201D\u2026`);
8648
+ const uploaded = [];
8649
+ const already = [];
8650
+ const refused = [];
8651
+ for (const path of paths) {
8652
+ let content;
8653
+ try {
8654
+ content = (await adapter.read(path)).content;
8655
+ } catch (err) {
8656
+ refused.push({
8657
+ path,
8658
+ code: err.code ?? "read_failed",
8659
+ message: err.message
8660
+ });
8661
+ continue;
8662
+ }
8663
+ try {
8664
+ await cloudPutNote(destination, { path, content });
8665
+ uploaded.push(path);
8666
+ } catch (err) {
8667
+ const code = err.code;
8668
+ if (code === "note_exists") {
8669
+ already.push(path);
8670
+ continue;
8671
+ }
8672
+ if (code === "write_refused" || code === "invalid_path" || code === "rationale_refused") {
8673
+ refused.push({ path, code, message: err.message });
8674
+ continue;
8675
+ }
8676
+ throw Object.assign(err, {
8677
+ hint: `${uploaded.length} note(s) went up before this. Nothing was deleted locally, and running \`memgineering push\` again resumes from where it stopped. ` + (err.hint ?? "")
8678
+ });
8679
+ }
8680
+ }
8681
+ await pointConfigAt(destination);
8682
+ printDual({
8683
+ json: {
8684
+ from: local.root,
8685
+ brain: { id: destination.brainId, name: destination.name },
8686
+ uploaded: uploaded.length,
8687
+ already_there: already.length,
8688
+ refused
8689
+ },
8690
+ human: () => {
8691
+ printHuman(c.green(`\u2713 ${uploaded.length} note(s) uploaded to \u201C${destination.name}\u201D`));
8692
+ if (already.length > 0) {
8693
+ printHuman(c.gray(` ${already.length} were already there \u2014 left as they are`));
8694
+ }
8695
+ if (refused.length > 0) {
8696
+ printHuman(
8697
+ c.yellow(`
8698
+ \u26A0 ${refused.length} note(s) were NOT uploaded:`) + c.gray("\n they are untouched on this machine and can be fixed and pushed again")
8699
+ );
8700
+ for (const r of refused.slice(0, 10)) {
8701
+ printHuman(c.gray(` ${r.path} (${r.code})`));
8702
+ }
8703
+ if (refused.length > 10) {
8704
+ printHuman(c.gray(` \u2026 and ${refused.length - 10} more`));
8705
+ }
8706
+ }
8707
+ printHuman(
8708
+ c.gray(
8709
+ `
8710
+ This machine now reads \u201C${destination.name}\u201D. Nothing was deleted from
8711
+ \`${local.root}\` \u2014 \`memgineering recall --local\` still reads it.`
8712
+ )
8713
+ );
8714
+ }
8715
+ });
8716
+ });
8717
+ }
8718
+ async function resolveDestination(named, root) {
8719
+ const base = apiUrl();
8720
+ const credentials = await requireCredentials(base);
8721
+ const config = await loadConfig();
8722
+ const brains = await listCloudBrains(credentials.token, base);
8723
+ const pick = (summary) => ({
8724
+ brainId: summary.id,
8725
+ name: summary.name,
8726
+ token: credentials.token,
8727
+ apiUrl: base
8728
+ });
8729
+ if (named) {
8730
+ const found = brains.find((b) => b.name === named || b.id === named);
8731
+ return pick(found ?? await createCloudBrain(credentials.token, base, named));
8732
+ }
8733
+ const current = config.brain.cloud;
8734
+ if (current && current.api_url === base) {
8735
+ const found = brains.find((b) => b.id === current.id);
8736
+ if (found) return pick(found);
8737
+ }
8738
+ const fallbackName = root.split("/").filter(Boolean).pop() ?? "brain";
8739
+ const existing = brains.find((b) => b.name === fallbackName);
8740
+ return pick(existing ?? await createCloudBrain(credentials.token, base, fallbackName));
8741
+ }
8742
+ async function pointConfigAt(destination) {
8743
+ const config = await loadConfig();
8744
+ await saveConfig({
8745
+ ...config,
8746
+ brain: {
8747
+ ...config.brain,
8748
+ cloud: { id: destination.brainId, name: destination.name, api_url: destination.apiUrl }
8749
+ }
8750
+ });
8751
+ }
8752
+
6966
8753
  // src/commands/recall.ts
6967
8754
  init_src();
6968
- import { Command as Command10 } from "commander";
8755
+ import { Command as Command14 } from "commander";
6969
8756
  init_index_build();
8757
+ init_errors();
6970
8758
  init_vault();
6971
8759
  init_ui();
6972
8760
  function recallCommand() {
6973
- return new Command10("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option("--detail <level>", "title | card | summary | chunks | full (default: card)").action(
8761
+ return new Command14("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--local", "search the brain on this machine, even when signed in to a hosted one").option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option("--detail <level>", "title | card | summary | chunks | full (default: card)").action(
6974
8762
  async (query, opts) => {
6975
8763
  const detail = parseDetail(opts.detail, "card");
6976
- const { brain } = await resolveBrain(opts.vault);
6977
- const adapter = await openBrainOrExplain(brain.root);
6978
- const built = await openIndex(adapter, brain.root);
6979
- const result = recall(built.entries, query, {
6980
- limit: opts.limit,
6981
- ...opts.scope ? { scope: opts.scope } : {}
8764
+ const target = await resolveTarget({
8765
+ ...opts.local === void 0 ? {} : { local: opts.local },
8766
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
6982
8767
  });
6983
- await recordEvent(brain.root, {
6984
- verb: "recall",
6985
- query,
6986
- detail,
6987
- limit: opts.limit,
6988
- returnedIds: result.memory_cards.map((card) => card.id)
6989
- });
6990
- const allIds = built.entries.map((e) => e.memory.id);
6991
- const byId = new Map(built.entries.map((e) => [e.memory.id, e]));
6992
- const rows = await Promise.all(
6993
- result.memory_cards.map(async (card) => ({
6994
- card,
6995
- handle: displayHandle(card.id, allIds),
6996
- stage: await stageFor(adapter, byId.get(card.id), detail)
6997
- }))
6998
- );
6999
- const scaffolding = await scaffoldingAmong(
7000
- adapter,
7001
- rows.map(({ card }) => {
7002
- const entry = byId.get(card.id);
7003
- return { path: entry?.path ?? "", title: entry?.memory.title ?? "" };
7004
- })
7005
- );
7006
- const isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
7007
- const reasons = result.conflicts.length > 0 ? await conflictReasons(brain.root, result.conflicts, byId) : /* @__PURE__ */ new Map();
8768
+ let rows;
8769
+ let isScaffolding;
8770
+ let reasons;
8771
+ let pathOf;
8772
+ let entries;
8773
+ let result;
8774
+ if (target.kind === "cloud") {
8775
+ if (detail !== "title" && detail !== "card") {
8776
+ throw memgError(
8777
+ "invalid_input",
8778
+ `\`--detail ${detail}\` is not available on a hosted brain`,
8779
+ `Nothing was read. Hosted recall returns cards; \`memgineering open <handle> --detail ${detail}\` fetches one note's prose, and \`--local\` runs the whole search against the brain on this machine.`
8780
+ );
8781
+ }
8782
+ const cloud = await openCloudBrain(target);
8783
+ const answered = await cloudRecall(cloud, {
8784
+ query,
8785
+ limit: opts.limit,
8786
+ ...opts.scope ? { scope: opts.scope } : {}
8787
+ });
8788
+ rows = answered.memory_cards.map((card) => ({ card, handle: card.handle, stage: {} }));
8789
+ isScaffolding = () => false;
8790
+ reasons = /* @__PURE__ */ new Map();
8791
+ pathOf = () => null;
8792
+ entries = null;
8793
+ result = {
8794
+ memory_cards: answered.memory_cards,
8795
+ related: answered.related ?? [],
8796
+ conflicts: answered.conflicts ?? []
8797
+ };
8798
+ } else {
8799
+ const brain = target.brain;
8800
+ const adapter = await openBrainOrExplain(brain.root);
8801
+ const built = await openIndex(adapter, brain.root);
8802
+ const local = recall(built.entries, query, {
8803
+ limit: opts.limit,
8804
+ ...opts.scope ? { scope: opts.scope } : {}
8805
+ });
8806
+ result = local;
8807
+ entries = built.entries;
8808
+ await recordEvent(brain.root, {
8809
+ verb: "recall",
8810
+ query,
8811
+ detail,
8812
+ limit: opts.limit,
8813
+ returnedIds: result.memory_cards.map((card) => card.id)
8814
+ });
8815
+ const allIds = built.entries.map((e) => e.memory.id);
8816
+ const byId = new Map(built.entries.map((e) => [e.memory.id, e]));
8817
+ pathOf = (card) => byId.get(card.id)?.path ?? null;
8818
+ rows = await Promise.all(
8819
+ result.memory_cards.map(async (card) => ({
8820
+ card,
8821
+ handle: displayHandle(card.id, allIds),
8822
+ stage: await stageFor(adapter, byId.get(card.id), detail)
8823
+ }))
8824
+ );
8825
+ const scaffolding = await scaffoldingAmong(
8826
+ adapter,
8827
+ rows.map(({ card }) => {
8828
+ const entry = byId.get(card.id);
8829
+ return { path: entry?.path ?? "", title: entry?.memory.title ?? "" };
8830
+ })
8831
+ );
8832
+ isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
8833
+ reasons = result.conflicts.length > 0 ? await conflictReasons(brain.root, result.conflicts, byId) : /* @__PURE__ */ new Map();
8834
+ }
7008
8835
  printDual({
7009
8836
  json: {
7010
8837
  query,
@@ -7012,7 +8839,7 @@ function recallCommand() {
7012
8839
  cards: rows.map(({ card, handle: handle2, stage }) => ({
7013
8840
  ...card,
7014
8841
  handle: handle2,
7015
- path: byId.get(card.id)?.path ?? null,
8842
+ path: pathOf(card),
7016
8843
  // A fact about the file, not a judgement about the match: this
7017
8844
  // note is still exactly what `init` wrote, so nobody has answered
7018
8845
  // it yet.
@@ -7028,7 +8855,7 @@ function recallCommand() {
7028
8855
  },
7029
8856
  human: () => {
7030
8857
  const reachedSomethingWritten = rows.some(({ card }) => !isScaffolding(card));
7031
- const otherScript = reachedSomethingWritten ? null : unreachableScript(query, built.entries);
8858
+ const otherScript = reachedSomethingWritten ? null : entries ? unreachableScript(query, entries) : null;
7032
8859
  const sayWhy = () => {
7033
8860
  printHuman(
7034
8861
  c.yellow(
@@ -7166,7 +8993,8 @@ function ago2(days) {
7166
8993
  function unreachableScript(query, entries) {
7167
8994
  const queryScripts = scriptsIn(query);
7168
8995
  if (queryScripts.size === 0) return null;
7169
- const written = entries.filter((e) => !e.path.startsWith("01_BASE/"));
8996
+ const SCAFFOLD = ["01_BASE/", "00_HUB/"];
8997
+ const written = entries.filter((e) => !SCAFFOLD.some((dir) => e.path.startsWith(dir)));
7170
8998
  const counts = /* @__PURE__ */ new Map();
7171
8999
  for (const entry of written) {
7172
9000
  const script = dominantScriptOf(`${entry.memory.title} ${entry.memory.summary ?? ""}`);
@@ -7175,6 +9003,12 @@ function unreachableScript(query, entries) {
7175
9003
  if (counts.size === 0) return null;
7176
9004
  const [dominant, seen] = [...counts].sort((a, b) => b[1] - a[1])[0];
7177
9005
  if (queryScripts.has(dominant)) return null;
9006
+ if (queryScripts.size > 0) {
9007
+ for (const entry of written) {
9008
+ const script = dominantScriptOf(`${entry.memory.title} ${entry.memory.summary ?? ""}`);
9009
+ if (script && queryScripts.has(script)) return null;
9010
+ }
9011
+ }
7178
9012
  return seen >= Math.max(2, written.length / 2) ? dominant : null;
7179
9013
  }
7180
9014
  var SCRIPT_RANGES = [
@@ -7213,73 +9047,103 @@ function scriptsIn(text) {
7213
9047
  }
7214
9048
 
7215
9049
  // src/commands/remember.ts
7216
- import { Command as Command11 } from "commander";
9050
+ import { Command as Command15 } from "commander";
7217
9051
  init_errors();
7218
9052
  init_vault();
7219
9053
  init_ui();
7220
9054
  function rememberCommand() {
7221
- return new Command11("remember").description("write something down now, with no approval step").argument("<text>", "what to remember").option("--vault <path>", "which brain to write to").option("--scope <scope>", "what this is about \u2014 a project, an area").option(
9055
+ return new Command15("remember").description("write something down now, with no approval step").argument("<text>", "what to remember").option("--vault <path>", "which brain to write to").option("--local", "write to the brain on this machine, even when signed in to a hosted one").option("--scope <scope>", "what this is about \u2014 a project, an area").option(
7222
9056
  "--reason <text>",
7223
9057
  "why you are recording this \u2014 goes in the ledger, not in the note (the fact itself belongs in <text>)"
7224
- ).action(async (text, opts) => {
7225
- const body = stripControl(text).trim();
7226
- if (body === "") {
7227
- throw memgError(
7228
- "invalid_input",
7229
- "nothing to remember",
7230
- 'Pass the text: `memgineering remember "deploys are manual \u2014 launchctl by hand"`'
7231
- );
7232
- }
7233
- const rationale = sanitizeRationale(opts.reason);
7234
- const { brain } = await resolveBrain(opts.vault);
7235
- await openBrainOrExplain(brain.root);
7236
- const now = /* @__PURE__ */ new Date();
7237
- const id = newId();
7238
- const month = now.toISOString().slice(0, 7);
7239
- const dir = `${brain.observationsDir.replace(/\/+$/, "")}/${month}`;
7240
- const path = `${dir}/obs-${id}.md`;
7241
- const content = renderObservation({
7242
- id,
7243
- text: body,
7244
- now,
7245
- ...opts.scope ? { scope: opts.scope } : {}
7246
- });
7247
- const record = await withWriteLock(brain.root, async () => {
7248
- const adapter = await LocalFileCasAdapter.openVault(brain.root, {
7249
- // Writes are confined to the observations folder. `remember` creates;
7250
- // it has no business being able to author a file anywhere in someone's
7251
- // vault, and the adapter enforces that rather than this command
7252
- // promising it.
7253
- createPrefix: brain.observationsDir
7254
- });
7255
- const result = await adapter.create(path, content);
7256
- if (!result.ok) {
9058
+ ).action(
9059
+ async (text, opts) => {
9060
+ const body = stripControl(text).trim();
9061
+ if (body === "") {
7257
9062
  throw memgError(
7258
9063
  "invalid_input",
7259
- `a note already exists at ${path}`,
7260
- "This should not happen \u2014 ids are random. Run the command again."
9064
+ "nothing to remember",
9065
+ 'Pass the text: `memgineering remember "deploys are manual \u2014 launchctl by hand"`'
7261
9066
  );
7262
9067
  }
7263
- return appendOp(brain.root, {
7264
- verb: "remember",
7265
- target: path,
7266
- memoryId: id,
7267
- hashBefore: null,
7268
- hashAfter: hashContent(content),
7269
- rationale,
7270
- now
9068
+ const rationale = sanitizeRationale(opts.reason);
9069
+ const target = await resolveTarget({
9070
+ ...opts.local === void 0 ? {} : { local: opts.local },
9071
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
7271
9072
  });
7272
- });
7273
- printDual({
7274
- json: { id, path, op_id: record.op_id, scope: opts.scope ?? null },
7275
- human: () => {
7276
- printHuman(`Remembered.
9073
+ if (target.kind === "cloud") {
9074
+ const cloud = await openCloudBrain(target);
9075
+ const written = await cloudRemember(cloud, {
9076
+ text: body,
9077
+ ...opts.scope ? { scope: opts.scope } : {},
9078
+ ...rationale ? { reason: rationale } : {}
9079
+ });
9080
+ printDual({
9081
+ json: {
9082
+ id: written.id,
9083
+ path: written.path,
9084
+ op_id: written.op_id,
9085
+ scope: written.scope,
9086
+ brain: { hosted: true, name: target.name }
9087
+ },
9088
+ human: () => {
9089
+ printHuman(`Remembered in ${target.name}.
7277
9090
  `);
7278
- printHuman(`note: \`${id}\` \xB7 \`${path}\``);
7279
- printHuman(c.gray(`undo: \`${undoHint(record.op_id, opts.vault)}\``));
9091
+ printHuman(`note: \`${written.id}\` \xB7 \`${written.path}\``);
9092
+ printHuman(c.gray(`undo: \`memgineering undo ${written.op_id}\``));
9093
+ }
9094
+ });
9095
+ return;
7280
9096
  }
7281
- });
7282
- });
9097
+ const brain = target.brain;
9098
+ await openBrainOrExplain(brain.root);
9099
+ const now = /* @__PURE__ */ new Date();
9100
+ const id = newId();
9101
+ const month = now.toISOString().slice(0, 7);
9102
+ const dir = `${brain.observationsDir.replace(/\/+$/, "")}/${month}`;
9103
+ const path = `${dir}/obs-${id}.md`;
9104
+ const content = renderObservation({
9105
+ id,
9106
+ text: body,
9107
+ now,
9108
+ ...opts.scope ? { scope: opts.scope } : {}
9109
+ });
9110
+ const record = await withWriteLock(brain.root, async () => {
9111
+ const adapter = await LocalFileCasAdapter.openVault(brain.root, {
9112
+ // Writes are confined to the observations folder. `remember` creates;
9113
+ // it has no business being able to author a file anywhere in someone's
9114
+ // vault, and the adapter enforces that rather than this command
9115
+ // promising it.
9116
+ createPrefix: brain.observationsDir
9117
+ });
9118
+ const result = await adapter.create(path, content);
9119
+ if (!result.ok) {
9120
+ throw memgError(
9121
+ "invalid_input",
9122
+ `a note already exists at ${path}`,
9123
+ "This should not happen \u2014 ids are random. Run the command again."
9124
+ );
9125
+ }
9126
+ return appendOp(brain.root, {
9127
+ verb: "remember",
9128
+ target: path,
9129
+ memoryId: id,
9130
+ hashBefore: null,
9131
+ hashAfter: hashContent(content),
9132
+ rationale,
9133
+ now
9134
+ });
9135
+ });
9136
+ printDual({
9137
+ json: { id, path, op_id: record.op_id, scope: opts.scope ?? null },
9138
+ human: () => {
9139
+ printHuman(`Remembered.
9140
+ `);
9141
+ printHuman(`note: \`${id}\` \xB7 \`${path}\``);
9142
+ printHuman(c.gray(`undo: \`${undoHint(record.op_id, opts.vault)}\``));
9143
+ }
9144
+ });
9145
+ }
9146
+ );
7283
9147
  }
7284
9148
  function renderObservation(args) {
7285
9149
  const firstLine = args.text.split("\n")[0] ?? args.text;
@@ -7310,163 +9174,170 @@ function yamlString(value) {
7310
9174
 
7311
9175
  // src/commands/resurface.ts
7312
9176
  init_src();
7313
- import { Command as Command12 } from "commander";
9177
+ import { Command as Command16 } from "commander";
7314
9178
  init_index_build();
7315
9179
  init_vault();
7316
9180
  init_ui();
7317
9181
  function resurfaceCommand() {
7318
- return new Command12("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").action(async (opts) => {
7319
- const contextDir = opts.contextDir ?? process.cwd();
7320
- const { brain } = await resolveBrain(opts.vault, contextDir);
7321
- const adapter = await openBrainOrExplain(brain.root);
7322
- const built = await openIndex(adapter, brain.root);
7323
- const events = await readEvents(brain.root);
7324
- const now = /* @__PURE__ */ new Date();
7325
- const ranked = rank(built.entries, events, { contextDir, now }).slice(0, opts.limit);
7326
- const untouched = await untouchedBase(
7327
- adapter,
7328
- ranked.map((r) => r.entry.path)
7329
- );
7330
- const allIds = built.entries.map((e) => e.memory.id);
7331
- await recordEvent(brain.root, {
7332
- verb: "resurface",
7333
- contextDir,
7334
- detail: "card",
7335
- limit: opts.limit,
7336
- returnedIds: ranked.map((r) => r.entry.memory.id),
7337
- now
7338
- });
7339
- printDual({
7340
- json: {
7341
- context_dir: contextDir,
7342
- cards: ranked.map(({ entry, score, why }) => ({
7343
- id: entry.memory.id,
7344
- handle: displayHandle(entry.memory.id, allIds),
7345
- title: entry.memory.title,
7346
- summary: entry.memory.summary,
7347
- path: entry.path,
7348
- score: Number(score.toFixed(3)),
7349
- why,
7350
- unfilled: untouched.has(entry.path)
7351
- }))
7352
- },
7353
- human: () => {
7354
- if (ranked.length === 0) {
7355
- printHuman("## resurface\n");
7356
- printHuman("_Nothing to bring up yet._\n");
7357
- printHuman(
7358
- c.gray(
7359
- "This ranks by what has been recalled here before, so it gets useful once the brain has been used a few times."
7360
- )
7361
- );
7362
- return;
7363
- }
7364
- printHuman(`## resurface (${ranked.length} cards)
7365
- `);
7366
- for (const [i, { entry, why }] of ranked.entries()) {
7367
- const blank = untouched.has(entry.path);
7368
- printHuman(`### ${i + 1}. ${entry.memory.title}`);
7369
- printHuman(blank ? c.yellow("not filled in yet") : why.join(" \xB7 "));
7370
- if (blank) {
9182
+ return new Command16("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(
9183
+ async (opts) => {
9184
+ const contextDir = opts.contextDir ?? process.cwd();
9185
+ const where = await resolveTarget({
9186
+ ...opts.local === void 0 ? {} : { local: opts.local },
9187
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
9188
+ });
9189
+ if (where.kind === "cloud") {
9190
+ await resurfaceHosted(where, opts.limit);
9191
+ return;
9192
+ }
9193
+ const { brain } = await resolveBrain(opts.vault, contextDir);
9194
+ const adapter = await openBrainOrExplain(brain.root);
9195
+ const built = await openIndex(adapter, brain.root);
9196
+ const events = await readEvents(brain.root);
9197
+ const now = /* @__PURE__ */ new Date();
9198
+ const ranked = rank(built.entries, events, {
9199
+ now,
9200
+ // The CLI is the only side that knows where a question was asked from,
9201
+ // so it is the only side that can answer this.
9202
+ here: (event) => isInside(event.context_dir, contextDir)
9203
+ });
9204
+ const TEMPLATE_SLOTS = 2;
9205
+ const untouchedPaths = await untouchedBase(
9206
+ adapter,
9207
+ ranked.map((r) => r.entry.path)
9208
+ );
9209
+ const isTemplate = (r) => untouchedPaths.has(r.entry.path);
9210
+ const templates = ranked.filter(isTemplate);
9211
+ const written = ranked.filter((r) => !isTemplate(r));
9212
+ const shown = [
9213
+ ...templates.slice(0, TEMPLATE_SLOTS),
9214
+ ...written.slice(0, Math.max(0, opts.limit - Math.min(TEMPLATE_SLOTS, templates.length)))
9215
+ ].sort((a, b) => ranked.indexOf(a) - ranked.indexOf(b)).slice(0, opts.limit);
9216
+ const rankedShown = shown.length > 0 ? shown : ranked.slice(0, opts.limit);
9217
+ const untouched = untouchedPaths;
9218
+ const allIds = built.entries.map((e) => e.memory.id);
9219
+ await recordEvent(brain.root, {
9220
+ verb: "resurface",
9221
+ contextDir,
9222
+ detail: "card",
9223
+ limit: opts.limit,
9224
+ returnedIds: rankedShown.map((r) => r.entry.memory.id),
9225
+ now
9226
+ });
9227
+ printDual({
9228
+ json: {
9229
+ context_dir: contextDir,
9230
+ cards: rankedShown.map(({ entry, score, why }) => ({
9231
+ id: entry.memory.id,
9232
+ handle: displayHandle(entry.memory.id, allIds),
9233
+ title: entry.memory.title,
9234
+ summary: entry.memory.summary,
9235
+ path: entry.path,
9236
+ score: Number(score.toFixed(3)),
9237
+ why,
9238
+ unfilled: untouched.has(entry.path)
9239
+ }))
9240
+ },
9241
+ human: () => {
9242
+ if (rankedShown.length === 0) {
9243
+ printHuman("## resurface\n");
9244
+ printHuman("_Nothing to bring up yet._\n");
7371
9245
  printHuman(
7372
9246
  c.gray(
7373
- `
7374
- This is still the file \`init\` created \u2014 the line below is its prompt, not
7375
- something you wrote. Fill it in and it becomes a memory like any other.`
9247
+ "This ranks by what has been recalled here before, so it gets useful once the brain has been used a few times."
7376
9248
  )
7377
9249
  );
9250
+ return;
7378
9251
  }
7379
- if (entry.memory.summary) printHuman(`
7380
- ${quoteBlock(entry.memory.summary)}`);
7381
- printHuman(`
7382
- \`open: ${displayHandle(entry.memory.id, allIds)}\`
9252
+ printHuman(`## resurface (${rankedShown.length} cards)
7383
9253
  `);
7384
- }
7385
- }
7386
- });
7387
- });
7388
- }
7389
- var WEIGHT = {
7390
- contextRecall: 3,
7391
- anyRecall: 1,
7392
- recency: 2,
7393
- staleBase: 4
7394
- };
7395
- var STALE_BASE_DAYS = 30;
7396
- function rank(entries, events, opts) {
7397
- const recalls = events.filter((e) => e.verb !== "resurface");
7398
- const here = recalls.filter((e) => isInside(e.context_dir, opts.contextDir));
7399
- const countIn = (source) => {
7400
- const counts = /* @__PURE__ */ new Map();
7401
- for (const event of source) {
7402
- for (const id of event.returned_ids) counts.set(id, (counts.get(id) ?? 0) + 1);
7403
- if (event.opened_id) counts.set(event.opened_id, (counts.get(event.opened_id) ?? 0) + 1);
7404
- }
7405
- return counts;
7406
- };
7407
- const hereCounts = countIn(here);
7408
- const anyCounts = countIn(recalls);
7409
- const lastTouched = /* @__PURE__ */ new Map();
7410
- for (const event of recalls) {
7411
- const t = Date.parse(event.ts);
7412
- if (!Number.isFinite(t)) continue;
7413
- for (const id of [...event.returned_ids, event.opened_id].filter(Boolean)) {
7414
- lastTouched.set(id, Math.max(lastTouched.get(id) ?? 0, t));
7415
- }
7416
- }
7417
- const hasBase = entries.some((e) => e.path.startsWith("01_BASE/"));
7418
- const scored = [];
7419
- for (const entry of entries) {
7420
- const id = entry.memory.id;
7421
- const freshness = freshnessOf(entry.memory, entry.claims, opts.now);
7422
- if (freshness === "retired") continue;
7423
- const why = [];
7424
- let score = 0;
7425
- const hereCount = hereCounts.get(id) ?? 0;
7426
- if (hereCount > 0) {
7427
- score += WEIGHT.contextRecall * Math.log2(1 + hereCount);
7428
- why.push(`recalled ${hereCount}\xD7 in this folder`);
7429
- }
7430
- const anyCount = (anyCounts.get(id) ?? 0) - hereCount;
7431
- if (anyCount > 0) {
7432
- score += WEIGHT.anyRecall * Math.log2(1 + anyCount);
7433
- if (hereCount === 0) why.push(`recalled ${anyCount}\xD7 elsewhere`);
7434
- }
7435
- const touched = lastTouched.get(id);
7436
- if (touched !== void 0) {
7437
- const days = (opts.now.getTime() - touched) / 864e5;
7438
- score += WEIGHT.recency * Math.pow(0.5, days / 7);
9254
+ const noEvidenceYet = !rankedShown.some(
9255
+ ({ why }) => why.some((w) => w.startsWith("recalled"))
9256
+ );
9257
+ for (const [i, { entry, why }] of rankedShown.entries()) {
9258
+ const blank = untouched.has(entry.path);
9259
+ printHuman(`### ${i + 1}. ${entry.memory.title}`);
9260
+ printHuman(blank ? c.yellow("not filled in yet") : why.join(" \xB7 "));
9261
+ if (blank) {
9262
+ const hasWritten = rankedShown.some((r) => !untouched.has(r.entry.path));
9263
+ printHuman(
9264
+ c.gray(
9265
+ hasWritten ? `
9266
+ Still the template \`init\` wrote \u2014 the line below is its prompt.` : `
9267
+ This is still the file \`init\` created \u2014 the line below is its prompt, not
9268
+ something you wrote. Fill it in and it becomes a memory like any other.`
9269
+ )
9270
+ );
9271
+ }
9272
+ if (entry.memory.summary) printHuman(`
9273
+ ${quoteBlock(entry.memory.summary)}`);
9274
+ printHuman(`
9275
+ \`open: ${displayHandle(entry.memory.id, allIds)}\`
9276
+ `);
9277
+ }
9278
+ printHuman(
9279
+ c.gray(
9280
+ (noEvidenceYet ? "Nothing has been recalled in this folder yet, so the order above is what the brain holds rather than what matters here.\n" : "") + 'These are the user\u2019s own notes and decisions \u2014 follow them without being asked, and\nsay so if you are about to do something one of them rules out. Before answering\nanything that sounds already settled, `memgineering recall "<their words>"`; when\nthis session decides something durable, `memgineering remember "<it>" --reason "<why>"`.'
9281
+ )
9282
+ );
9283
+ }
9284
+ });
7439
9285
  }
7440
- if (hasBase && entry.path.startsWith("01_BASE/")) {
7441
- const days = touched === void 0 ? Number.POSITIVE_INFINITY : (opts.now.getTime() - touched) / 864e5;
7442
- if (days > STALE_BASE_DAYS) {
7443
- score += WEIGHT.staleBase;
7444
- why.push(
7445
- touched === void 0 ? "never read this session" : `not read in ${Math.floor(days)} days`
7446
- );
9286
+ );
9287
+ }
9288
+ async function resurfaceHosted(where, limit) {
9289
+ const brain = await openCloudBrain(where);
9290
+ const result = await cloudResurface(brain, { limit });
9291
+ printDual({
9292
+ json: {
9293
+ brain: where.name,
9294
+ cards: result.cards,
9295
+ ranked_by: result.ranked_by,
9296
+ ...result.hint ? { hint: result.hint } : {}
9297
+ },
9298
+ human: () => {
9299
+ if (result.cards.length === 0) {
9300
+ printHuman(`## resurface \u2014 ${where.name}
9301
+ `);
9302
+ printHuman("_Nothing to bring up yet._\n");
9303
+ if (result.hint) printHuman(c.gray(result.hint));
9304
+ return;
9305
+ }
9306
+ printHuman(`## resurface \u2014 ${where.name} (${result.cards.length} cards)
9307
+ `);
9308
+ for (const [i, card] of result.cards.entries()) {
9309
+ printHuman(`### ${i + 1}. ${card.title}`);
9310
+ if (card.why.length > 0) printHuman(card.why.join(" \xB7 "));
9311
+ if (card.summary) printHuman(`
9312
+ ${quoteBlock(card.summary)}`);
9313
+ printHuman(`
9314
+ \`open: ${card.handle}\`
9315
+ `);
7447
9316
  }
9317
+ printHuman(
9318
+ c.gray(
9319
+ `Ranked by ${result.ranked_by.join(", ")}. A hosted brain does not record where a
9320
+ question was asked from, so "in this folder" is not one of them \u2014 that signal
9321
+ exists only for a brain on this machine.`
9322
+ )
9323
+ );
7448
9324
  }
7449
- if (freshness !== "current") why.push(freshness);
7450
- if (score > 0) scored.push({ entry, score, why });
7451
- }
7452
- return scored.sort(
7453
- (a, b) => b.score - a.score || a.entry.memory.id.localeCompare(b.entry.memory.id)
7454
- );
9325
+ });
7455
9326
  }
7456
9327
 
7457
9328
  // src/commands/revise.ts
7458
9329
  init_src();
7459
- import { readFile as readFile14 } from "fs/promises";
7460
- import { Command as Command13 } from "commander";
9330
+ import { readFile as readFile17 } from "fs/promises";
9331
+ import { Command as Command17 } from "commander";
7461
9332
  init_errors();
7462
9333
  init_index_build();
7463
9334
  init_vault();
7464
9335
  init_ui();
7465
9336
  function reviseCommand() {
7466
- return new Command13("revise").description("change a memory's conclusion, and record how to undo it").argument("<ref>", "handle, id, path, or exact title").option("--claim <text>", "what is now true").option("--summary <text>", "the one line recall will show (defaults to the claim)").option("--title <text>", "the heading recall shows above it (unchanged when omitted)").option(
9337
+ return new Command17("revise").description("change a memory's conclusion, and record how to undo it").argument("<ref>", "handle, id, path, or exact title").option("--claim <text>", "what is now true").option("--summary <text>", "the one line recall will show (defaults to the claim)").option("--title <text>", "the heading recall shows above it (unchanged when omitted)").option(
7467
9338
  "--valid-from <iso>",
7468
9339
  "when this became true (defaults: supersede = now, otherwise unchanged)"
7469
- ).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").addHelpText(
9340
+ ).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").option("--local", "use the brain on this machine, even when signed in to a hosted one").addHelpText(
7470
9341
  "after",
7471
9342
  [
7472
9343
  "",
@@ -7482,6 +9353,14 @@ function reviseCommand() {
7482
9353
  "always true, you have only just been told it. `supersede` would date it today."
7483
9354
  ].join("\n")
7484
9355
  ).action(async (ref, opts) => {
9356
+ const where = await resolveTarget({
9357
+ ...opts.local === void 0 ? {} : { local: opts.local },
9358
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
9359
+ });
9360
+ if (where.kind === "cloud") {
9361
+ await reviseHosted(where, ref, opts);
9362
+ return;
9363
+ }
7485
9364
  const { brain } = await resolveBrain(opts.vault);
7486
9365
  await openBrainOrExplain(brain.root);
7487
9366
  const adapter = await LocalFileCasAdapter.openVault(brain.root, {
@@ -7502,7 +9381,7 @@ function reviseCommand() {
7502
9381
  );
7503
9382
  }
7504
9383
  const proposal = result.proposal;
7505
- if (proposal.action !== "conflict" && rationale === null && saysNothingNew(entry, input)) {
9384
+ if (proposal.action !== "conflict" && rationale === null && saysNothingNew(entry, input, stripControl)) {
7506
9385
  throw memgError(
7507
9386
  "invalid_input",
7508
9387
  `${entry.memory.title} already says this \u2014 nothing was written`,
@@ -7572,7 +9451,12 @@ To see what is there now: \`memgineering open ` + ref + "`"
7572
9451
  });
7573
9452
  return { record, content: outcome.content };
7574
9453
  });
7575
- const drift = proposal.action === "supersede" ? bodyStillAsserts(applied.content, previousClaim(entry), proposal.claim?.text ?? null) : null;
9454
+ const drift = proposal.action === "supersede" ? bodyStillAsserts(
9455
+ applied.content,
9456
+ previousClaim(entry),
9457
+ proposal.claim?.text ?? null,
9458
+ stripControl
9459
+ ) : null;
7576
9460
  const unwrittenBody = bodyStillTemplate(entry.path, applied.content);
7577
9461
  printDual({
7578
9462
  json: {
@@ -7632,7 +9516,95 @@ Undo: \`${undoHint(applied.record.op_id, opts.vault)}\``));
7632
9516
  });
7633
9517
  });
7634
9518
  }
7635
- var NO_REASON_GIVEN = "revised directly";
9519
+ async function reviseHosted(where, ref, opts) {
9520
+ if (opts.input) {
9521
+ throw memgError(
9522
+ "invalid_input",
9523
+ "--input is not supported against a hosted brain",
9524
+ "It can name a different memory and set fields the flags do not expose, so honouring part of it would report a change nobody asked for. Pass the change as flags (`--claim`, `--summary`, `--action`), or use `--local` to apply the JSON to the brain on this machine. Nothing was sent."
9525
+ );
9526
+ }
9527
+ if (!opts.claim) {
9528
+ throw memgError(
9529
+ "invalid_input",
9530
+ "revise needs to know what is now true",
9531
+ 'Pass `--claim "<the new conclusion>"`.'
9532
+ );
9533
+ }
9534
+ const brain = await openCloudBrain(where);
9535
+ const result = await cloudRevise(brain, {
9536
+ ref,
9537
+ claim: opts.claim,
9538
+ action: opts.action,
9539
+ ...opts.summary === void 0 ? {} : { summary: opts.summary },
9540
+ ...opts.title === void 0 ? {} : { title: opts.title },
9541
+ ...opts.validFrom === void 0 ? {} : { validFrom: opts.validFrom },
9542
+ ...opts.contradicts === void 0 ? {} : { contradicts: opts.contradicts },
9543
+ ...opts.reason === void 0 ? {} : { reason: opts.reason },
9544
+ ...opts.because === void 0 ? {} : { because: opts.because },
9545
+ ...opts.dryRun === true ? { dryRun: true } : {}
9546
+ });
9547
+ printDual({
9548
+ json: { brain: where.name, ...result },
9549
+ human: () => {
9550
+ if (result.dry_run) {
9551
+ printHuman(`## revise (dry run) \u2014 ${where.name}
9552
+ `);
9553
+ printHuman(`\`${result.target}\`
9554
+ `);
9555
+ printHuman(renderDiff(result.before, result.after));
9556
+ printHuman(c.gray("\nNothing was written. Drop --dry-run to apply."));
9557
+ return;
9558
+ }
9559
+ if (opts.action === "conflict") {
9560
+ printHuman(`Recorded a disagreement in **${where.name}**.
9561
+ `);
9562
+ printHuman(`\`${result.target}\`
9563
+ `);
9564
+ printHuman(
9565
+ c.gray(
9566
+ "The claim you passed was NOT written. A conflict says two memories disagree\nand leaves both standing \u2014 recall surfaces them together so you can decide.\nTo change what this note concludes, use `--action supersede`.\n"
9567
+ )
9568
+ );
9569
+ } else {
9570
+ printHuman(`Revised in **${where.name}**.
9571
+ `);
9572
+ printHuman(`\`${result.target}\`
9573
+ `);
9574
+ }
9575
+ printHuman(renderDiff(result.before, result.after));
9576
+ if (result.body_drift) {
9577
+ printHuman(
9578
+ c.yellow(`
9579
+ \u26A0 the body still reads "${result.body_drift}"`) + c.gray(
9580
+ "\n \u2014 the frontmatter now concludes differently. The prose is yours and was not\n touched; edit it yourself if it should agree."
9581
+ )
9582
+ );
9583
+ }
9584
+ if (result.critical_target) {
9585
+ printHuman(
9586
+ c.yellow(
9587
+ `
9588
+ \u26A0 critical target \u2014 this note defines how the agent behaves. Tell your owner what changed.`
9589
+ )
9590
+ );
9591
+ }
9592
+ if (result.reverse?.supported && result.reverse.body?.claim) {
9593
+ printHuman(
9594
+ c.gray(
9595
+ `
9596
+ To put it back: \`memgineering revise ${ref} --claim ${JSON.stringify(
9597
+ result.reverse.body.claim
9598
+ )}\``
9599
+ )
9600
+ );
9601
+ } else if (result.reverse?.hint) {
9602
+ printHuman(c.gray(`
9603
+ ${result.reverse.hint}`));
9604
+ }
9605
+ }
9606
+ });
9607
+ }
7636
9608
  async function buildCommitInput(entry, opts) {
7637
9609
  if (opts.input) {
7638
9610
  const raw = await readInput(opts.input);
@@ -7653,104 +9625,20 @@ async function buildCommitInput(entry, opts) {
7653
9625
  "reinforce (same conclusion, new support) \xB7 supersede (replaces it) \xB7 conflict (records a disagreement without deciding).\nTo change standing instead, use `retire` or `unretire`."
7654
9626
  );
7655
9627
  }
7656
- return {
9628
+ return reviseInput(entry, {
7657
9629
  action,
7658
- memory_id: entry.memory.id,
7659
- claim: {
7660
- text: opts.claim,
7661
- scope: entry.memory.scope,
7662
- // WHEN THE FACT BECAME TRUE, not when somebody edited the note.
7663
- //
7664
- // This was `new Date()` unconditionally, so `reinforce` — the action for
7665
- // "same conclusion, new support" — walked the date forward every time it
7666
- // ran. A tester added a reason to a memory and watched its start date
7667
- // move a day later than the thing it describes. `valid_from` is the field
7668
- // a temporal memory store exists to get right, and it was being used as
7669
- // an updated-at; the drift is silent and cumulative.
7670
- //
7671
- // `supersede` still stamps now, because a replaced conclusion IS starting
7672
- // now — that is what supersede means. `reinforce` and `conflict` keep
7673
- // whatever the memory already declared, and `--valid-from` overrides
7674
- // either when the caller knows the real date.
7675
- valid_from: validFrom(action, opts, entry),
7676
- confidence: "stated",
7677
- summary: action === "conflict" ? null : opts.summary ?? opts.claim,
7678
- // Never defaulted from the claim, unlike the summary. A summary that is
7679
- // missing makes the change invisible, so filling it in is a rescue; a
7680
- // title that is missing simply means the author's heading still fits, and
7681
- // overwriting it would be rewriting their classification uninvited.
7682
- title: action === "conflict" ? null : opts.title ?? null
7683
- },
7684
- relations: {
7685
- depends_on: [],
7686
- relates_to: [],
7687
- contradicts: opts.contradicts ?? []
7688
- },
7689
- reason: opts.reason ?? NO_REASON_GIVEN,
7690
- // The engine refuses to submit without an excerpt, because approving a
7691
- // diff on an assertion alone is not consent. Nothing here is approved by a
7692
- // person, but the requirement still earns its keep: it forces the record
7693
- // to say what the change was based on rather than only what it did.
7694
- source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
7695
- target_path: entry.path
7696
- };
7697
- }
7698
- function asSingleMatch(entry) {
7699
- return {
7700
- candidate: {
7701
- subject: entry.memory.title,
7702
- scope: entry.memory.scope,
7703
- kind: entry.memory.type,
7704
- candidate_claim: entry.memory.summary ?? entry.memory.title,
7705
- entities: [],
7706
- importance: "normal"
7707
- },
7708
- matches: [
7709
- {
7710
- id: entry.memory.id,
7711
- title: entry.memory.title,
7712
- summary: entry.memory.summary,
7713
- path: entry.path,
7714
- revision: entry.revision,
7715
- freshness: freshnessOf(entry.memory, entry.claims, /* @__PURE__ */ new Date()),
7716
- score: 1
7717
- }
7718
- ],
7719
- contradicts: [],
7720
- scopes: entry.memory.scope ? [entry.memory.scope] : []
7721
- };
9630
+ claim: opts.claim,
9631
+ ...opts.summary === void 0 ? {} : { summary: opts.summary },
9632
+ ...opts.title === void 0 ? {} : { title: opts.title },
9633
+ ...opts.validFrom === void 0 ? {} : { validFrom: opts.validFrom },
9634
+ ...opts.contradicts === void 0 ? {} : { contradicts: opts.contradicts },
9635
+ ...opts.reason === void 0 ? {} : { reason: opts.reason },
9636
+ ...opts.because === void 0 ? {} : { because: opts.because }
9637
+ });
7722
9638
  }
7723
9639
  function isCritical(entry) {
7724
9640
  return /^01_BASE\//.test(entry.path);
7725
9641
  }
7726
- function previousClaim(entry) {
7727
- const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
7728
- return current?.text ?? null;
7729
- }
7730
- function saysNothingNew(entry, input) {
7731
- const norm = (v) => stripControl(v ?? "").trim();
7732
- const claim = input.claim;
7733
- if (!claim || norm(claim.text) === "") return false;
7734
- if (norm(claim.text) !== norm(previousClaim(entry))) return false;
7735
- if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
7736
- if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
7737
- const declared = new Set(
7738
- entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
7739
- );
7740
- return (input.relations?.contradicts ?? []).every((id) => declared.has(id));
7741
- }
7742
- function bodyStillAsserts(content, oldClaim, newClaim) {
7743
- if (oldClaim === null) return null;
7744
- const line = stripControl(oldClaim.split("\n")[0] ?? "").trim();
7745
- if (line === "") return null;
7746
- if (line === stripControl(newClaim?.split("\n")[0] ?? "").trim()) return null;
7747
- return bodyOf2(content).includes(line) ? line : null;
7748
- }
7749
- function bodyOf2(content) {
7750
- const stripped = content.replace(/^/, "");
7751
- const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
7752
- return fence ? stripped.slice(fence[0].length) : stripped;
7753
- }
7754
9642
  function renderDiff(before, after) {
7755
9643
  return ["```diff", ...lines(before, "-"), ...lines(after, "+"), "```"].join("\n");
7756
9644
  }
@@ -7758,13 +9646,13 @@ function lines(text, marker) {
7758
9646
  return text === "" ? [] : text.split("\n").map((l) => `${marker} ${l}`);
7759
9647
  }
7760
9648
  async function readInput(source) {
7761
- const raw = source === "-" ? await new Promise((resolve10, reject) => {
9649
+ const raw = source === "-" ? await new Promise((resolve11, reject) => {
7762
9650
  let buf = "";
7763
9651
  process.stdin.setEncoding("utf8");
7764
9652
  process.stdin.on("data", (chunk) => buf += chunk);
7765
- process.stdin.on("end", () => resolve10(buf));
9653
+ process.stdin.on("end", () => resolve11(buf));
7766
9654
  process.stdin.on("error", reject);
7767
- }) : await readFile14(source, "utf8").catch(() => source);
9655
+ }) : await readFile17(source, "utf8").catch(() => source);
7768
9656
  try {
7769
9657
  return JSON.parse(raw);
7770
9658
  } catch {
@@ -7775,25 +9663,19 @@ async function readInput(source) {
7775
9663
  );
7776
9664
  }
7777
9665
  }
7778
- function validFrom(action, opts, entry) {
7779
- if (opts.validFrom) return opts.validFrom;
7780
- if (action === "supersede") return (/* @__PURE__ */ new Date()).toISOString();
7781
- const declared = entry.claims.find((c2) => c2.valid_from)?.valid_from;
7782
- return declared ?? null;
7783
- }
7784
9666
 
7785
9667
  // src/program.ts
7786
9668
  init_setup();
7787
9669
 
7788
9670
  // src/commands/update.ts
7789
- import { Command as Command15 } from "commander";
9671
+ import { Command as Command19 } from "commander";
7790
9672
 
7791
9673
  // src/version.ts
7792
9674
  import { readFileSync } from "fs";
7793
9675
  import { fileURLToPath as fileURLToPath2 } from "url";
7794
- import { dirname as dirname4, join as join16 } from "path";
7795
- var __dir = dirname4(fileURLToPath2(import.meta.url));
7796
- var pkgPath = join16(__dir, "..", "package.json");
9676
+ import { dirname as dirname5, join as join19 } from "path";
9677
+ var __dir = dirname5(fileURLToPath2(import.meta.url));
9678
+ var pkgPath = join19(__dir, "..", "package.json");
7797
9679
  var VERSION = (() => {
7798
9680
  try {
7799
9681
  const parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
@@ -7809,7 +9691,7 @@ init_config();
7809
9691
  // src/lib/provenance.ts
7810
9692
  import { execFile } from "child_process";
7811
9693
  import { realpath as realpath3 } from "fs/promises";
7812
- import { dirname as dirname5, join as join17, resolve as resolve8 } from "path";
9694
+ import { dirname as dirname6, join as join20, resolve as resolve9 } from "path";
7813
9695
  import { fileURLToPath as fileURLToPath3 } from "url";
7814
9696
  import { promisify } from "util";
7815
9697
  var run = promisify(execFile);
@@ -7830,7 +9712,7 @@ var MANAGERS = [
7830
9712
  ];
7831
9713
  async function detectProvenance(selfPath) {
7832
9714
  const entry = selfPath ?? fileURLToPath3(import.meta.url);
7833
- const selfDir = await realpath3(dirname5(entry)).catch(() => dirname5(entry));
9715
+ const selfDir = await realpath3(dirname6(entry)).catch(() => dirname6(entry));
7834
9716
  if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
7835
9717
  return {
7836
9718
  kind: "npx",
@@ -7875,8 +9757,8 @@ async function prefixOf(bin, args) {
7875
9757
  }
7876
9758
  }
7877
9759
  function isInside2(child, parent) {
7878
- const c2 = resolve8(child);
7879
- const p = resolve8(parent);
9760
+ const c2 = resolve9(child);
9761
+ const p = resolve9(parent);
7880
9762
  return c2 === p || c2.startsWith(p.endsWith("/") ? p : `${p}/`);
7881
9763
  }
7882
9764
 
@@ -7884,16 +9766,16 @@ function isInside2(child, parent) {
7884
9766
  init_atomic();
7885
9767
  init_brand();
7886
9768
  import { execFile as execFile2 } from "child_process";
7887
- import { mkdir as mkdir10, readFile as readFile16, writeFile as writeFile7 } from "fs/promises";
7888
- import { join as join18 } from "path";
9769
+ import { mkdir as mkdir13, readFile as readFile19, writeFile as writeFile10 } from "fs/promises";
9770
+ import { join as join21 } from "path";
7889
9771
  import { promisify as promisify2 } from "util";
7890
9772
  import semver from "semver";
7891
- import { z as z9 } from "zod";
9773
+ import { z as z11 } from "zod";
7892
9774
  var run2 = promisify2(execFile2);
7893
- var PolicySchema = z9.object({
7894
- latest: z9.string().optional(),
7895
- minimum_supported: z9.string().optional(),
7896
- notice: z9.string().optional()
9775
+ var PolicySchema = z11.object({
9776
+ latest: z11.string().optional(),
9777
+ minimum_supported: z11.string().optional(),
9778
+ notice: z11.string().optional()
7897
9779
  });
7898
9780
  var POLICY_URL = "https://memgineering.com/version-policy.json";
7899
9781
  var NOTICE_MAX = 300;
@@ -7958,19 +9840,19 @@ function shouldCheck(lastCheck, now) {
7958
9840
  if (!Number.isFinite(last)) return true;
7959
9841
  return now.getTime() - last > 864e5;
7960
9842
  }
7961
- var UpdateStateSchema = z9.object({
7962
- at: z9.string(),
7963
- ok: z9.boolean(),
7964
- from: z9.string(),
7965
- to: z9.string().nullable(),
7966
- message: z9.string().nullable()
9843
+ var UpdateStateSchema = z11.object({
9844
+ at: z11.string(),
9845
+ ok: z11.boolean(),
9846
+ from: z11.string(),
9847
+ to: z11.string().nullable(),
9848
+ message: z11.string().nullable()
7967
9849
  });
7968
9850
  function statePath() {
7969
- return join18(brandHome(), "update-state.json");
9851
+ return join21(brandHome(), "update-state.json");
7970
9852
  }
7971
9853
  async function readUpdateState() {
7972
9854
  try {
7973
- const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile16(statePath(), "utf8")));
9855
+ const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile19(statePath(), "utf8")));
7974
9856
  return parsed.success ? parsed.data : null;
7975
9857
  } catch {
7976
9858
  return null;
@@ -7978,7 +9860,7 @@ async function readUpdateState() {
7978
9860
  }
7979
9861
  async function writeUpdateState(state) {
7980
9862
  try {
7981
- await mkdir10(brandHome(), { recursive: true });
9863
+ await mkdir13(brandHome(), { recursive: true });
7982
9864
  await writeThenRename(statePath(), `${JSON.stringify(state, null, 2)}
7983
9865
  `);
7984
9866
  } catch {
@@ -8034,7 +9916,7 @@ function shortMessage(err) {
8034
9916
  // src/commands/update.ts
8035
9917
  init_ui();
8036
9918
  function updateCommand() {
8037
- return new Command15("update").description("update memgineering itself").option("--check", "report what is available and change nothing").action(async (opts) => {
9919
+ return new Command19("update").description("update memgineering itself").option("--check", "report what is available and change nothing").action(async (opts) => {
8038
9920
  const status = await checkForUpdate(VERSION);
8039
9921
  const provenance = await detectProvenance();
8040
9922
  const previous = await readUpdateState();
@@ -8140,117 +10022,140 @@ function reportBelowMinimum(below) {
8140
10022
  }
8141
10023
 
8142
10024
  // src/commands/undo.ts
8143
- import { readFile as readFile17, rm as rm8 } from "fs/promises";
8144
- import { join as join19 } from "path";
8145
- import { Command as Command16 } from "commander";
10025
+ import { readFile as readFile20, rm as rm8 } from "fs/promises";
10026
+ import { join as join22 } from "path";
10027
+ import { Command as Command20 } from "commander";
8146
10028
  init_atomic();
8147
10029
  init_errors();
8148
10030
  init_vault();
8149
10031
  init_ui();
8150
10032
  function undoCommand() {
8151
- return new Command16("undo").description("take back the last change, or a named one").argument("[op]", "operation id (default: the most recent)").option("--reason <text>", "why it is being taken back \u2014 kept in the ledger").option("--vault <path>", "which brain to act on").action(async (opId, opts) => {
8152
- const rationale = sanitizeRationale(opts.reason);
8153
- const { brain } = await resolveBrain(opts.vault);
8154
- await openBrainOrExplain(brain.root);
8155
- const { ops, skipped } = await readOps(brain.root);
8156
- if (opId === void 0 && skipped > 0) {
8157
- throw memgError(
8158
- "ledger_divergence",
8159
- `this brain has ${skipped} unreadable ledger line(s), so "the last change" is not knowable`,
8160
- 'Usually the tail of a run that was interrupted mid-write \u2014 and the line that was lost is normally the newest, so undoing "the last change" would reverse an older one instead. Run `memgineering log` to see what is readable, then undo by id.'
8161
- );
8162
- }
8163
- const matches = opId ? matchIds(
8164
- opId,
8165
- ops.map((o) => o.op_id)
8166
- ) : [];
8167
- if (matches.length > 1) {
8168
- throw memgError(
8169
- "ambiguous_ref",
8170
- `"${opId}" matches ${matches.length} operations`,
8171
- `Pass the whole id \u2014 undo deletes a note, so this is not a guess worth making.
10033
+ return new Command20("undo").description("take back the last change, or a named one").argument("[op]", "operation id (default: the most recent)").option("--reason <text>", "why it is being taken back \u2014 kept in the ledger").option("--vault <path>", "which brain to act on").option("--local", "act on the brain on this machine, even when signed in to a hosted one").action(
10034
+ async (opId, opts) => {
10035
+ const rationale = sanitizeRationale(opts.reason);
10036
+ const where = await resolveTarget({
10037
+ ...opts.local === void 0 ? {} : { local: opts.local },
10038
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
10039
+ });
10040
+ if (where.kind === "cloud") {
10041
+ const cloud = await openCloudBrain(where);
10042
+ const undone = await cloudUndo(cloud, {
10043
+ ...opId === void 0 ? {} : { opId },
10044
+ ...rationale ? { reason: rationale } : {}
10045
+ });
10046
+ printDual({
10047
+ json: { ...undone, brain: { hosted: true, name: where.name } },
10048
+ human: () => {
10049
+ printHuman(`Took back \`${undone.undone}\` \u2014 a ${undone.verb}.
10050
+ `);
10051
+ printHuman(c.gray(` ${undone.target}`));
10052
+ printHuman(c.gray(` recorded as \`${undone.op_id}\``));
10053
+ }
10054
+ });
10055
+ return;
10056
+ }
10057
+ const brain = where.brain;
10058
+ await openBrainOrExplain(brain.root);
10059
+ const { ops, skipped } = await readOps(brain.root);
10060
+ if (opId === void 0 && skipped > 0) {
10061
+ throw memgError(
10062
+ "ledger_divergence",
10063
+ `this brain has ${skipped} unreadable ledger line(s), so "the last change" is not knowable`,
10064
+ 'Usually the tail of a run that was interrupted mid-write \u2014 and the line that was lost is normally the newest, so undoing "the last change" would reverse an older one instead. Run `memgineering log` to see what is readable, then undo by id.'
10065
+ );
10066
+ }
10067
+ const matches = opId ? matchIds(
10068
+ opId,
10069
+ ops.map((o) => o.op_id)
10070
+ ) : [];
10071
+ if (matches.length > 1) {
10072
+ throw memgError(
10073
+ "ambiguous_ref",
10074
+ `"${opId}" matches ${matches.length} operations`,
10075
+ `Pass the whole id \u2014 undo deletes a note, so this is not a guess worth making.
8172
10076
  ${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "\n \u2026" : ""}`
8173
- );
8174
- }
8175
- const target = opId ? ops.find((o) => o.op_id === matches[0]) : lastReversible(ops);
8176
- if (!target) {
8177
- throw opId ? memgError(
8178
- "nothing_to_undo",
8179
- `no operation here matches: ${opId}`,
8180
- "An id is 26 characters; a prefix must be at least 5 and must be unique. `memgineering log` shows what this brain recorded."
8181
- ) : memgError(
8182
- "nothing_to_undo",
8183
- "nothing to undo in this brain",
8184
- "Only changes made through memgineering are recorded. Edits made in your editor are yours to revert."
8185
- );
8186
- }
8187
- if (target.verb === "undo") {
8188
- throw memgError(
8189
- "nothing_to_undo",
8190
- "that operation is itself an undo",
8191
- "Undoing an undo is a fresh change \u2014 make it with `revise` or `remember`."
8192
- );
8193
- }
8194
- if (ops.some((o) => o.verb === "undo" && o.supersedes === target.op_id)) {
8195
- throw memgError(
8196
- "nothing_to_undo",
8197
- `${target.op_id} has already been undone`,
8198
- "Run `memgineering log` to see the current state of this brain."
8199
- );
8200
- }
8201
- const record = await withWriteLock(brain.root, async () => {
8202
- const absolute = join19(brain.root, target.target);
8203
- const current = await readFile17(absolute, "utf8").catch(() => null);
8204
- const actualHash = current === null ? null : hashContent(current);
8205
- if (actualHash !== target.hash_after) {
10077
+ );
10078
+ }
10079
+ const target = opId ? ops.find((o) => o.op_id === matches[0]) : lastReversible(ops);
10080
+ if (!target) {
10081
+ throw opId ? memgError(
10082
+ "nothing_to_undo",
10083
+ `no operation here matches: ${opId}`,
10084
+ "An id is 26 characters; a prefix must be at least 5 and must be unique. `memgineering log` shows what this brain recorded."
10085
+ ) : memgError(
10086
+ "nothing_to_undo",
10087
+ "nothing to undo in this brain",
10088
+ "Only changes made through memgineering are recorded. Edits made in your editor are yours to revert."
10089
+ );
10090
+ }
10091
+ if (target.verb === "undo") {
8206
10092
  throw memgError(
8207
- "undo_conflict",
8208
- `${target.target} has changed since that operation`,
8209
- await conflictHint(brain.root, target)
10093
+ "nothing_to_undo",
10094
+ "that operation is itself an undo",
10095
+ "Undoing an undo is a fresh change \u2014 make it with `revise` or `remember`."
8210
10096
  );
8211
10097
  }
8212
- const reverted = await reverse(brain, target, current);
8213
- return appendOp(brain.root, {
8214
- verb: "undo",
8215
- target: target.target,
8216
- memoryId: target.memory_id,
8217
- hashBefore: actualHash,
8218
- hashAfter: reverted === null ? null : hashContent(reverted),
8219
- rationale,
8220
- supersedes: target.op_id
10098
+ if (ops.some((o) => o.verb === "undo" && o.supersedes === target.op_id)) {
10099
+ throw memgError(
10100
+ "nothing_to_undo",
10101
+ `${target.op_id} has already been undone`,
10102
+ "Run `memgineering log` to see the current state of this brain."
10103
+ );
10104
+ }
10105
+ const record = await withWriteLock(brain.root, async () => {
10106
+ const absolute = join22(brain.root, target.target);
10107
+ const current = await readFile20(absolute, "utf8").catch(() => null);
10108
+ const actualHash = current === null ? null : hashContent(current);
10109
+ if (actualHash !== target.hash_after) {
10110
+ throw memgError(
10111
+ "undo_conflict",
10112
+ `${target.target} has changed since that operation`,
10113
+ await conflictHint(brain.root, target)
10114
+ );
10115
+ }
10116
+ const reverted = await reverse(brain, target, current);
10117
+ return appendOp(brain.root, {
10118
+ verb: "undo",
10119
+ target: target.target,
10120
+ memoryId: target.memory_id,
10121
+ hashBefore: actualHash,
10122
+ hashAfter: reverted === null ? null : hashContent(reverted),
10123
+ rationale,
10124
+ supersedes: target.op_id
10125
+ });
8221
10126
  });
8222
- });
8223
- printDual({
8224
- json: {
8225
- undone: target.op_id,
8226
- verb: target.verb,
8227
- target: target.target,
8228
- undone_ts: target.ts,
8229
- undone_reason: target.rationale,
8230
- op_id: record.op_id
8231
- },
8232
- human: () => {
8233
- printHuman(`Undone: ${target.verb} on \`${target.target}\`
10127
+ printDual({
10128
+ json: {
10129
+ undone: target.op_id,
10130
+ verb: target.verb,
10131
+ target: target.target,
10132
+ undone_ts: target.ts,
10133
+ undone_reason: target.rationale,
10134
+ op_id: record.op_id
10135
+ },
10136
+ human: () => {
10137
+ printHuman(`Undone: ${target.verb} on \`${target.target}\`
8234
10138
  `);
8235
- if (opId === void 0) {
8236
- printHuman(c.gray(`It was recorded ${target.ts}.`));
8237
- if (target.rationale === null) printHuman(c.gray("No reason was recorded for it."));
8238
- else printHuman(quoteBlock(target.rationale));
8239
- const minutes = minutesSince(target.ts);
8240
- if (minutes !== null && minutes >= STALE_UNDO_MINUTES) {
8241
- printHuman(
8242
- c.gray(
8243
- `
10139
+ if (opId === void 0) {
10140
+ printHuman(c.gray(`It was recorded ${target.ts}.`));
10141
+ if (target.rationale === null) printHuman(c.gray("No reason was recorded for it."));
10142
+ else printHuman(quoteBlock(target.rationale));
10143
+ const minutes = minutesSince(target.ts);
10144
+ if (minutes !== null && minutes >= STALE_UNDO_MINUTES) {
10145
+ printHuman(
10146
+ c.gray(
10147
+ `
8244
10148
  that operation was ${minutes} minutes old \u2014 pass the op id to be sure it was the one you meant.`
8245
- )
8246
- );
10149
+ )
10150
+ );
10151
+ }
8247
10152
  }
8248
- }
8249
- printHuman(c.gray(`
10153
+ printHuman(c.gray(`
8250
10154
  Recorded as ${record.op_id} \u2014 the history keeps both.`));
8251
- }
8252
- });
8253
- });
10155
+ }
10156
+ });
10157
+ }
10158
+ );
8254
10159
  }
8255
10160
  var STALE_UNDO_MINUTES = 10;
8256
10161
  function minutesSince(iso) {
@@ -8259,7 +10164,7 @@ function minutesSince(iso) {
8259
10164
  return Math.max(0, Math.floor((Date.now() - at) / 6e4));
8260
10165
  }
8261
10166
  async function reverse(brain, op, current) {
8262
- const absolute = join19(brain.root, op.target);
10167
+ const absolute = join22(brain.root, op.target);
8263
10168
  if (op.verb === "remember") {
8264
10169
  if (current === null) {
8265
10170
  throw memgError(
@@ -8324,7 +10229,7 @@ async function conflictHint(vaultRoot, op) {
8324
10229
  Read what it says now, then decide what should stay.`;
8325
10230
  const snapshot = await readSnapshot(vaultRoot, op.op_id);
8326
10231
  return snapshot === null ? base : `${base}
8327
- The text from before that change is still on this machine: ${join19(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
10232
+ The text from before that change is still on this machine: ${join22(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
8328
10233
  }
8329
10234
 
8330
10235
  // src/commands/use.ts
@@ -8334,8 +10239,8 @@ init_errors();
8334
10239
  init_config();
8335
10240
  init_vault();
8336
10241
  init_ui();
8337
- import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve9 } from "path";
8338
- import { Command as Command17 } from "commander";
10242
+ import { isAbsolute as isAbsolute4, relative as relative3, resolve as resolve10 } from "path";
10243
+ import { Command as Command21 } from "commander";
8339
10244
  var VIA_MEANS = {
8340
10245
  flag: "because --vault named it",
8341
10246
  pointer: `because a \`${BRAND.pointerFileName}\` file here points at it`,
@@ -8344,11 +10249,11 @@ var VIA_MEANS = {
8344
10249
  default: "because it is your default brain (`memgineering use --default <path>` changes it)"
8345
10250
  };
8346
10251
  function useCommand() {
8347
- return new Command17("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").option(
10252
+ return new Command21("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").option(
8348
10253
  "--default",
8349
10254
  "make this the brain used anywhere no pointer applies, instead of binding one folder"
8350
10255
  ).action(async (brainPath, opts) => {
8351
- const where = await canonicalize(resolve9(opts.here ?? process.cwd()));
10256
+ const where = await canonicalize(resolve10(opts.here ?? process.cwd()));
8352
10257
  if (!brainPath) {
8353
10258
  const pointer = await readPointer(where);
8354
10259
  const resolved = await resolveBrain(void 0, where).catch((err) => err);
@@ -8385,7 +10290,7 @@ ${hint}`));
8385
10290
  });
8386
10291
  return;
8387
10292
  }
8388
- const root = await canonicalize(resolve9(brainPath));
10293
+ const root = await canonicalize(resolve10(brainPath));
8389
10294
  const cfg = await loadConfig();
8390
10295
  if (!findBrain(cfg, root)) {
8391
10296
  throw memgError(
@@ -8412,16 +10317,16 @@ A pointer to an unlinked folder would fail on every command instead of at this o
8412
10317
  return;
8413
10318
  }
8414
10319
  const value = isInside(root, where) ? relative3(where, root) || "." : root;
8415
- const file = resolve9(where, BRAND.pointerFileName);
10320
+ const file = resolve10(where, BRAND.pointerFileName);
8416
10321
  await writeThenRename(file, `${value}
8417
10322
  `);
8418
10323
  printDual({
8419
- json: { pointer: file, brain: root, relative: !isAbsolute3(value) },
10324
+ json: { pointer: file, brain: root, relative: !isAbsolute4(value) },
8420
10325
  human: () => {
8421
10326
  printHuman(`This folder now uses \`${root}\`.
8422
10327
  `);
8423
10328
  printHuman(c.gray(` ${file}`));
8424
- if (!isAbsolute3(value)) {
10329
+ if (!isAbsolute4(value)) {
8425
10330
  printHuman(
8426
10331
  c.gray(
8427
10332
  " Written as a relative path \u2014 safe to commit, and it will resolve on a teammate\u2019s machine once they link the same brain."
@@ -8433,11 +10338,56 @@ A pointer to an unlinked folder would fail on every command instead of at this o
8433
10338
  });
8434
10339
  }
8435
10340
 
10341
+ // src/commands/whoami.ts
10342
+ init_api_client();
10343
+ init_ui();
10344
+ import { Command as Command22 } from "commander";
10345
+ function whoamiCommand() {
10346
+ return new Command22("whoami").description("show which account this machine is signed in as").action(async () => {
10347
+ const base = apiUrl();
10348
+ const credentials = await requireCredentials(base);
10349
+ const res = await apiRequest({
10350
+ method: "GET",
10351
+ path: "/v1/me",
10352
+ token: credentials.token,
10353
+ baseUrl: base
10354
+ });
10355
+ const account = res.body?.account ?? {};
10356
+ const live = res.body?.tokens?.live ?? null;
10357
+ printDual({
10358
+ json: {
10359
+ account: {
10360
+ id: account.id ?? credentials.account_id,
10361
+ provider: account.provider ?? null,
10362
+ display_name: account.display_name ?? null,
10363
+ email: account.email ?? null,
10364
+ created_at: account.created_at ?? null
10365
+ },
10366
+ tokens: { live },
10367
+ api_url: base,
10368
+ signed_in_at: credentials.signed_in_at
10369
+ },
10370
+ human: () => {
10371
+ printHuman(c.bold(account.display_name ?? account.id ?? credentials.account_id));
10372
+ if (account.email) printHuman(c.gray(` ${account.email}`));
10373
+ printHuman(c.gray(` on ${base}`));
10374
+ if (live !== null) {
10375
+ printHuman(
10376
+ c.gray(
10377
+ ` ${live} ${live === 1 ? "token" : "tokens"} signed in \u2014 \`memgineering logout --all\` ends them all.`
10378
+ )
10379
+ );
10380
+ }
10381
+ }
10382
+ });
10383
+ });
10384
+ }
10385
+
8436
10386
  // src/program.ts
8437
10387
  init_ui();
8438
10388
  init_brand();
8439
10389
  function buildProgram() {
8440
- const program = new Command18("memgineering").version(VERSION).description("memgineering \u2014 one memory for the AI you connect").option("--json", "emit JSON to stdout instead of markdown", false).hook("preAction", (thisCommand) => {
10390
+ const program = new Command23("memgineering").version(VERSION).description("memgineering \u2014 one memory for the AI you connect").option("--json", "emit JSON to stdout instead of markdown", false).hook("preAction", (thisCommand) => {
8441
10391
  const opts = thisCommand.optsWithGlobals();
8442
10392
  if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
8443
10393
  });
@@ -8449,6 +10399,8 @@ function buildProgram() {
8449
10399
  program.addCommand(reviseCommand());
8450
10400
  program.addCommand(undoCommand());
8451
10401
  program.addCommand(resurfaceCommand());
10402
+ program.addCommand(pushCommand());
10403
+ program.addCommand(pullCommand());
8452
10404
  program.addCommand(onboardCommand());
8453
10405
  program.addCommand(initCommand());
8454
10406
  program.addCommand(linkCommand());
@@ -8462,6 +10414,9 @@ function buildProgram() {
8462
10414
  program.addCommand(excludeCommand());
8463
10415
  program.addCommand(unexcludeCommand());
8464
10416
  program.addCommand(setupCommand());
10417
+ program.addCommand(loginCommand());
10418
+ program.addCommand(logoutCommand());
10419
+ program.addCommand(whoamiCommand());
8465
10420
  program.addCommand(updateCommand());
8466
10421
  return program;
8467
10422
  }