memgineering 0.4.2 → 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
  }
@@ -4126,12 +5157,12 @@ __export(setup_web_exports, {
4126
5157
  runSetupWeb: () => runSetupWeb,
4127
5158
  startSetupWebServer: () => startSetupWebServer
4128
5159
  });
4129
- import { spawn } from "child_process";
5160
+ import "child_process";
4130
5161
  import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
4131
5162
  import { readdir as readdir6, stat as stat3 } from "fs/promises";
4132
5163
  import { createServer } from "http";
4133
5164
  import { homedir as homedir3 } from "os";
4134
- 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";
4135
5166
  async function startSetupWebServer(opts) {
4136
5167
  const token = randomBytes2(32).toString("hex");
4137
5168
  const idleMs = opts.idleMs ?? IDLE_TIMEOUT_MS;
@@ -4143,11 +5174,11 @@ async function startSetupWebServer(opts) {
4143
5174
  sockets.add(socket);
4144
5175
  socket.on("close", () => sockets.delete(socket));
4145
5176
  });
4146
- await new Promise((resolve10, reject) => {
5177
+ await new Promise((resolve11, reject) => {
4147
5178
  server.once("error", reject);
4148
5179
  server.listen(0, "127.0.0.1", () => {
4149
5180
  server.off("error", reject);
4150
- resolve10();
5181
+ resolve11();
4151
5182
  });
4152
5183
  });
4153
5184
  const address = server.address();
@@ -4155,8 +5186,8 @@ async function startSetupWebServer(opts) {
4155
5186
  const origin = `http://127.0.0.1:${port}`;
4156
5187
  const ownOrigins = /* @__PURE__ */ new Set([origin, `http://localhost:${port}`]);
4157
5188
  let resolveDone;
4158
- const done = new Promise((resolve10) => {
4159
- resolveDone = resolve10;
5189
+ const done = new Promise((resolve11) => {
5190
+ resolveDone = resolve11;
4160
5191
  });
4161
5192
  let stopped = false;
4162
5193
  let idleTimer;
@@ -4166,8 +5197,8 @@ async function startSetupWebServer(opts) {
4166
5197
  if (idleTimer) clearTimeout(idleTimer);
4167
5198
  clearTimeout(maxTimer);
4168
5199
  process.off("SIGINT", onInterrupt);
4169
- await new Promise((resolve10) => {
4170
- server.close(() => resolve10());
5200
+ await new Promise((resolve11) => {
5201
+ server.close(() => resolve11());
4171
5202
  for (const socket of sockets) socket.destroy();
4172
5203
  });
4173
5204
  resolveDone(reason);
@@ -4184,7 +5215,13 @@ async function startSetupWebServer(opts) {
4184
5215
  void stop("interrupted");
4185
5216
  }
4186
5217
  process.once("SIGINT", onInterrupt);
4187
- 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
+ };
4188
5225
  server.on("request", (req, res) => {
4189
5226
  void handle(req, res, state, origin, touch).then((outcome) => {
4190
5227
  if (outcome === "applied") setTimeout(() => void stop("applied"), lingerMs).unref();
@@ -4210,7 +5247,10 @@ async function startSetupWebServer(opts) {
4210
5247
  };
4211
5248
  }
4212
5249
  async function runSetupWeb(opts) {
4213
- 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
+ });
4214
5254
  const opened = opts.printUrl ? false : await openBrowser(server.url);
4215
5255
  printDual({
4216
5256
  // The URL carries the key that authorizes reading folders and writing into
@@ -4223,7 +5263,9 @@ async function runSetupWeb(opts) {
4223
5263
  // So it is emitted only when it is the way in — the browser did not open,
4224
5264
  // or `--print-url` asked for it. When the browser DID open, the person is
4225
5265
  // already looking at the screen and nobody needs the string.
4226
- json: disclosure(server.url, server.port, opened),
5266
+ json: {
5267
+ ...disclosure(server.url, server.port, opened)
5268
+ },
4227
5269
  human: () => {
4228
5270
  if (opened) {
4229
5271
  printHuman("Opened setup in your browser.\n");
@@ -4247,7 +5289,7 @@ async function runSetupWeb(opts) {
4247
5289
  const ending = {
4248
5290
  applied: "Setup applied. Restart your agent session for it to take effect.",
4249
5291
  cancelled: "Closed without applying \u2014 nothing was changed.",
4250
- 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.",
4251
5293
  interrupted: "Stopped \u2014 nothing was changed.",
4252
5294
  closed: "Setup screen closed."
4253
5295
  };
@@ -4348,7 +5390,11 @@ async function stateDocument(state) {
4348
5390
  hook: state.detected.some((t) => t.hooks)
4349
5391
  },
4350
5392
  brains: cfg.brain.brains.map((b) => b.root),
4351
- 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
4352
5398
  };
4353
5399
  }
4354
5400
  async function candidateFolders(linked) {
@@ -4359,16 +5405,17 @@ async function candidateFolders(linked) {
4359
5405
  if (out.length >= MAX_CANDIDATES || seen.has(path)) return;
4360
5406
  seen.add(path);
4361
5407
  if (!await isDirectory(path)) return;
4362
- if (!await holdsNotes(path)) return;
4363
- 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 });
4364
5411
  };
4365
- for (const entry of await entriesOf(join14(home, "Documents"))) {
4366
- await consider(join14(home, "Documents", entry));
5412
+ for (const entry of await entriesOf(join17(home, "Documents"))) {
5413
+ await consider(join17(home, "Documents", entry));
4367
5414
  }
4368
5415
  for (const entry of await entriesOf(home)) {
4369
- if (/^brain/i.test(entry)) await consider(join14(home, entry));
5416
+ if (/^brain/i.test(entry)) await consider(join17(home, entry));
4370
5417
  }
4371
- for (const name of ["notes", "Notes"]) await consider(join14(home, name));
5418
+ for (const name of ["notes", "Notes"]) await consider(join17(home, name));
4372
5419
  return out;
4373
5420
  }
4374
5421
  async function entriesOf(dir) {
@@ -4377,21 +5424,24 @@ async function entriesOf(dir) {
4377
5424
  async function isDirectory(path) {
4378
5425
  return stat3(path).then((s) => s.isDirectory()).catch(() => false);
4379
5426
  }
4380
- async function holdsNotes(dir) {
5427
+ async function countNotes(dir) {
5428
+ const isNote = (n) => /\.(md|markdown)$/i.test(n);
4381
5429
  const entries = await entriesOf(dir);
4382
- 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;
4383
5432
  for (const entry of entries.slice(0, 40)) {
4384
- const child = join14(dir, entry);
5433
+ const child = join17(dir, entry);
4385
5434
  if (!await isDirectory(child)) continue;
4386
- 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;
4387
5437
  }
4388
- return false;
5438
+ return total;
4389
5439
  }
4390
5440
  function isSystemPath(path) {
4391
5441
  return SYSTEM_ROOTS.some((root) => {
4392
5442
  if (root === "/") return path === "/";
4393
5443
  const rel = relative2(root, path);
4394
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
5444
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
4395
5445
  });
4396
5446
  }
4397
5447
  async function inspectFromScreen(req, res, state) {
@@ -4418,7 +5468,7 @@ async function inspectFromScreen(req, res, state) {
4418
5468
  });
4419
5469
  return "served";
4420
5470
  }
4421
- const path = resolve7(expandHome(asked.trim()));
5471
+ const path = resolve8(expandHome(asked.trim()));
4422
5472
  if (isSystemPath(path)) {
4423
5473
  sendJson(res, 400, {
4424
5474
  error: {
@@ -4510,14 +5560,41 @@ async function applyFromScreen(req, res, state) {
4510
5560
  const changes = await applySetup(choice.value, false);
4511
5561
  const brain = await connectBrain(choice.brain);
4512
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);
4513
5565
  sendJson(res, 200, {
4514
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"] } : {},
4515
5577
  tools: choice.value.tools.map((t) => t.id),
4516
5578
  auto_update: choice.value.autoUpdate,
4517
5579
  hook: choice.value.hook,
4518
5580
  changes,
4519
5581
  brain,
4520
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
+ } : {},
4521
5598
  ...cfg.brain.brains.length === 0 ? {
4522
5599
  // Same sentence the CLI ends on. Which folder holds someone's notes
4523
5600
  // is the one decision neither surface makes for them, and `link` is
@@ -4557,6 +5634,15 @@ function readChoice(parsed, detected) {
4557
5634
  const body = parsed ?? {};
4558
5635
  const ids = Array.isArray(body.tools) ? body.tools.map(String) : [];
4559
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
+ }
4560
5646
  if (ids.length === 0) {
4561
5647
  return {
4562
5648
  error: {
@@ -4600,7 +5686,8 @@ function readChoice(parsed, detected) {
4600
5686
  // is not an error, it simply has nowhere to go — same rule as `--hook`.
4601
5687
  hook: body.hook === true && tools.some((t) => t.hooks)
4602
5688
  },
4603
- brain: brain.value
5689
+ brain: brain.value,
5690
+ account: body.account
4604
5691
  };
4605
5692
  }
4606
5693
  function readBrainChoice(raw) {
@@ -4637,8 +5724,8 @@ function readBrainChoice(raw) {
4637
5724
  }
4638
5725
  function expandHome(input) {
4639
5726
  if (input === "~") return homedir3();
4640
- if (input.startsWith("~/")) return join14(homedir3(), input.slice(2));
4641
- return isAbsolute2(input) ? input : resolve7(input);
5727
+ if (input.startsWith("~/")) return join17(homedir3(), input.slice(2));
5728
+ return isAbsolute3(input) ? input : resolve8(input);
4642
5729
  }
4643
5730
  function errorEnvelope(err) {
4644
5731
  if (err instanceof MemgError) return { code: err.code, message: err.message, hint: err.hint };
@@ -4675,7 +5762,7 @@ async function readJsonBody(req, res) {
4675
5762
  }
4676
5763
  }
4677
5764
  async function readBody(req) {
4678
- return new Promise((resolve10, reject) => {
5765
+ return new Promise((resolve11, reject) => {
4679
5766
  let size = 0;
4680
5767
  let refused = false;
4681
5768
  let chunks = [];
@@ -4685,14 +5772,14 @@ async function readBody(req) {
4685
5772
  if (!refused) {
4686
5773
  refused = true;
4687
5774
  chunks = [];
4688
- resolve10(null);
5775
+ resolve11(null);
4689
5776
  }
4690
5777
  return;
4691
5778
  }
4692
5779
  chunks.push(chunk);
4693
5780
  });
4694
5781
  req.once("end", () => {
4695
- if (!refused) resolve10(Buffer.concat(chunks).toString("utf8"));
5782
+ if (!refused) resolve11(Buffer.concat(chunks).toString("utf8"));
4696
5783
  });
4697
5784
  req.once("error", reject);
4698
5785
  });
@@ -4737,45 +5824,26 @@ function sendHtml(res, html) {
4737
5824
  });
4738
5825
  res.end(html);
4739
5826
  }
4740
- async function openBrowser(url) {
4741
- const { file, args } = browserOpener(url);
4742
- return new Promise((resolve10) => {
4743
- let settled = false;
4744
- const finish = (opened) => {
4745
- if (settled) return;
4746
- settled = true;
4747
- resolve10(opened);
4748
- };
4749
- const child = spawn(file, args, { detached: true, stdio: "ignore" });
4750
- child.on("error", () => finish(false));
4751
- child.on("exit", (code) => {
4752
- if (code !== null && code !== 0) finish(false);
4753
- });
4754
- child.unref();
4755
- setTimeout(() => finish(true), 300).unref();
4756
- });
4757
- }
4758
- function browserOpener(url) {
4759
- if (process.platform === "darwin") return { file: "open", args: [url] };
4760
- if (process.platform === "win32") return { file: "cmd", args: ["/c", "start", "", url] };
4761
- return { file: "xdg-open", args: [url] };
4762
- }
4763
- 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;
4764
5828
  var init_setup_web = __esm({
4765
5829
  "src/commands/setup-web.ts"() {
4766
5830
  "use strict";
4767
5831
  init_errors();
5832
+ init_open_browser();
5833
+ init_setup_signin();
5834
+ init_login();
4768
5835
  init_config();
4769
5836
  init_ui();
4770
5837
  init_init();
4771
5838
  init_link();
4772
5839
  init_setup();
4773
5840
  init_setup_web_page();
4774
- IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
5841
+ IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
4775
5842
  MAX_LIFETIME_MS = 60 * 60 * 1e3;
4776
5843
  APPLY_LINGER_MS = 1e3;
4777
5844
  MAX_BODY_BYTES = 64 * 1024;
4778
5845
  MAX_CANDIDATES = 8;
5846
+ COUNT_CAP = 40;
4779
5847
  PREVIEW_CAP = 5e3;
4780
5848
  SYSTEM_ROOTS = [
4781
5849
  "/",
@@ -4794,14 +5862,17 @@ var init_setup_web = __esm({
4794
5862
  });
4795
5863
 
4796
5864
  // src/commands/setup.ts
4797
- import { access, mkdir as mkdir9, readFile as readFile15 } from "fs/promises";
5865
+ import { access, mkdir as mkdir12, readFile as readFile18 } from "fs/promises";
4798
5866
  import { homedir as homedir4 } from "os";
4799
- import { dirname as dirname3, join as join15 } from "path";
5867
+ import { dirname as dirname4, join as join18 } from "path";
4800
5868
  import { fileURLToPath } from "url";
4801
- import { Command as Command14 } from "commander";
4802
- import prompts2 from "prompts";
5869
+ import { Command as Command18 } from "commander";
5870
+ import prompts3 from "prompts";
4803
5871
  function setupCommand() {
4804
- 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) => {
4805
5876
  const detected = await detectTools();
4806
5877
  if (detected.length === 0) {
4807
5878
  throw memgError(
@@ -4812,9 +5883,23 @@ function setupCommand() {
4812
5883
  }
4813
5884
  if (opts.web) {
4814
5885
  const { runSetupWeb: runSetupWeb2 } = await Promise.resolve().then(() => (init_setup_web(), setup_web_exports));
4815
- 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
+ });
4816
5891
  return;
4817
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
+ });
4818
5903
  const mode = await resolveMode(opts);
4819
5904
  const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts);
4820
5905
  const defaultsTaken = !mode.chosen ? [
@@ -4831,6 +5916,7 @@ function setupCommand() {
4831
5916
  printDual({
4832
5917
  json: {
4833
5918
  mode: mode.kind,
5919
+ ...signInStepJson(signIn),
4834
5920
  defaults_used: defaultsTaken,
4835
5921
  dry_run: opts.dryRun === true,
4836
5922
  tools: choice.tools.map((t) => t.id),
@@ -4845,11 +5931,13 @@ function setupCommand() {
4845
5931
  human: () => {
4846
5932
  if (opts.silent && changed.length === 0) return;
4847
5933
  if (opts.dryRun) {
5934
+ printSignInStep(signIn);
4848
5935
  printHuman(`Would change ${changed.length} file(s):
4849
5936
  `);
4850
5937
  for (const r of changed) printHuman(c.gray(` ${r.tool} \xB7 ${r.what}`));
4851
5938
  return;
4852
5939
  }
5940
+ printSignInStep(signIn);
4853
5941
  printHuman(`Registered with ${choice.tools.map((t) => t.label).join(", ")}.
4854
5942
  `);
4855
5943
  printHuman(c.gray(` auto-update: ${choice.autoUpdate}`));
@@ -4885,7 +5973,7 @@ function setupCommand() {
4885
5973
  }
4886
5974
  });
4887
5975
  if (mode.kind === "human" && brains.length === 0 && !opts.dryRun && isInteractive()) {
4888
- const { path } = await prompts2({
5976
+ const { path } = await prompts3({
4889
5977
  type: "text",
4890
5978
  name: "path",
4891
5979
  message: "Path to your notes folder (blank to skip)"
@@ -4905,15 +5993,15 @@ async function applySetup(choice, dryRun) {
4905
5993
  results.push({
4906
5994
  tool: tool.label,
4907
5995
  what: "guide",
4908
- result: await injectBlock(join15(homedir4(), tool.hubFile), hub, dryRun)
5996
+ result: await injectBlock(join18(homedir4(), tool.hubFile), hub, dryRun)
4909
5997
  });
4910
5998
  for (const skill of BRAND.subSkills) {
4911
- const content = await loadAsset(join15(skill, "SKILL.md"));
5999
+ const content = await loadAsset(join18(skill, "SKILL.md"));
4912
6000
  results.push({
4913
6001
  tool: tool.label,
4914
6002
  what: skill,
4915
6003
  result: await writeIfDifferent(
4916
- join15(homedir4(), tool.skillsDir, skill, "SKILL.md"),
6004
+ join18(homedir4(), tool.skillsDir, skill, "SKILL.md"),
4917
6005
  content,
4918
6006
  dryRun
4919
6007
  )
@@ -4949,7 +6037,7 @@ async function resolveMode(opts) {
4949
6037
  return { kind: "human", chosen: true };
4950
6038
  }
4951
6039
  if (!isInteractive() || isJsonMode()) return { kind: "agent", chosen: false };
4952
- const { mode } = await prompts2({
6040
+ const { mode } = await prompts3({
4953
6041
  type: "select",
4954
6042
  name: "mode",
4955
6043
  message: "How would you like to set this up?",
@@ -4969,7 +6057,7 @@ async function detectTools() {
4969
6057
  const home = homedir4();
4970
6058
  const found = [];
4971
6059
  for (const tool of TOOLS) {
4972
- if (await exists(join15(home, tool.dir))) found.push(tool);
6060
+ if (await exists2(join18(home, tool.dir))) found.push(tool);
4973
6061
  }
4974
6062
  return found;
4975
6063
  }
@@ -5006,7 +6094,7 @@ async function askHuman(detected, opts) {
5006
6094
  printHuman(c.bold("\nmemgineering \u2014 setup\n"));
5007
6095
  printHuman(c.gray(" Found: ") + detected.map((t) => t.label).join(", "));
5008
6096
  printHuman("");
5009
- const answers = await prompts2([
6097
+ const answers = await prompts3([
5010
6098
  {
5011
6099
  type: "multiselect",
5012
6100
  name: "tools",
@@ -5046,10 +6134,10 @@ async function injectBlock(filePath, content, dryRun) {
5046
6134
  const block = `${BRAND.hubMarkerStart}
5047
6135
  ${content.trimEnd()}
5048
6136
  ${BRAND.hubMarkerEnd}`;
5049
- const existing = await readFile15(filePath, "utf8").catch(() => null);
6137
+ const existing = await readFile18(filePath, "utf8").catch(() => null);
5050
6138
  if (existing === null) {
5051
6139
  if (!dryRun) {
5052
- await mkdir9(dirname3(filePath), { recursive: true });
6140
+ await mkdir12(dirname4(filePath), { recursive: true });
5053
6141
  await writeThenRename(filePath, `${block}
5054
6142
  `);
5055
6143
  }
@@ -5074,17 +6162,17 @@ ${block}
5074
6162
  return "injected";
5075
6163
  }
5076
6164
  async function writeIfDifferent(filePath, content, dryRun) {
5077
- const existing = await readFile15(filePath, "utf8").catch(() => null);
6165
+ const existing = await readFile18(filePath, "utf8").catch(() => null);
5078
6166
  if (existing === content) return "unchanged";
5079
6167
  if (!dryRun) {
5080
- await mkdir9(dirname3(filePath), { recursive: true });
6168
+ await mkdir12(dirname4(filePath), { recursive: true });
5081
6169
  await writeThenRename(filePath, content);
5082
6170
  }
5083
6171
  return existing === null ? "injected" : "updated";
5084
6172
  }
5085
6173
  async function installHook(dryRun) {
5086
- const settingsPath = join15(homedir4(), ".claude", "settings.json");
5087
- 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);
5088
6176
  let settings = {};
5089
6177
  if (raw !== null) {
5090
6178
  try {
@@ -5107,14 +6195,14 @@ async function installHook(dryRun) {
5107
6195
  sessionStart.push({ hooks: [{ type: "command", command }] });
5108
6196
  const next = { ...settings, hooks: { ...hooks, SessionStart: sessionStart } };
5109
6197
  if (!dryRun) {
5110
- await mkdir9(dirname3(settingsPath), { recursive: true });
6198
+ await mkdir12(dirname4(settingsPath), { recursive: true });
5111
6199
  await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
5112
6200
  `);
5113
6201
  }
5114
6202
  return raw === null ? "injected" : "updated";
5115
6203
  }
5116
6204
  async function loadAsset(relPath) {
5117
- const raw = await readFile15(join15(await assetRoot(), relPath), "utf8");
6205
+ const raw = await readFile18(join18(await assetRoot(), relPath), "utf8");
5118
6206
  if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
5119
6207
  const end = raw.indexOf("\n---", 4);
5120
6208
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -5122,14 +6210,14 @@ async function loadAsset(relPath) {
5122
6210
  return raw;
5123
6211
  }
5124
6212
  async function assetRoot() {
5125
- const here = dirname3(fileURLToPath(import.meta.url));
5126
- const bundled = join15(here, "..", "assets");
5127
- 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;
5128
6216
  let dir = here;
5129
6217
  for (; ; ) {
5130
- const candidate = join15(dir, "packages", "skills");
5131
- if (await exists(join15(candidate, BRAND.hubFileName))) return candidate;
5132
- 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);
5133
6221
  if (parent === dir) break;
5134
6222
  dir = parent;
5135
6223
  }
@@ -5139,7 +6227,7 @@ async function assetRoot() {
5139
6227
  "Reinstall with `npm i -g memgineering`. If you are running from a checkout, build first: `pnpm -F memgineering build`."
5140
6228
  );
5141
6229
  }
5142
- async function exists(path) {
6230
+ async function exists2(path) {
5143
6231
  try {
5144
6232
  await access(path);
5145
6233
  return true;
@@ -5154,6 +6242,7 @@ var init_setup = __esm({
5154
6242
  init_atomic();
5155
6243
  init_brand();
5156
6244
  init_errors();
6245
+ init_setup_signin();
5157
6246
  init_config();
5158
6247
  init_ui();
5159
6248
  TOOLS = [
@@ -5186,7 +6275,7 @@ var init_setup = __esm({
5186
6275
  });
5187
6276
 
5188
6277
  // src/program.ts
5189
- import { Command as Command18 } from "commander";
6278
+ import { Command as Command23 } from "commander";
5190
6279
 
5191
6280
  // src/help.ts
5192
6281
  init_ui();
@@ -5207,6 +6296,11 @@ var COMMAND_GROUPS = [
5207
6296
  blurb: "occasional \u2014 correct what is remembered and what is read",
5208
6297
  commands: ["retire", "unretire", "exclude", "unexclude"]
5209
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
+ },
5210
6304
  {
5211
6305
  title: "SYSTEM",
5212
6306
  blurb: "install into your agents, and stay current",
@@ -5217,7 +6311,8 @@ var defaultFormatHelp = Help.prototype.formatHelp;
5217
6311
  var ENV_ROWS = [
5218
6312
  ["MEMGINEERING_HOME=<dir>", "state + derived index (default: ~/.memgineering)"],
5219
6313
  ["MEMGINEERING_JSON=1", "force --json framing without the flag"],
5220
- ["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)"]
5221
6316
  ];
5222
6317
  function commandLabel(sub) {
5223
6318
  const args = sub.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
@@ -5347,8 +6442,17 @@ var OpVerbSchema = z7.enum([
5347
6442
  "retire",
5348
6443
  "unretire",
5349
6444
  "exclude",
5350
- "unexclude"
5351
- ]);
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"
6455
+ ]);
5352
6456
  var OpRecordSchema = z7.object({
5353
6457
  op_id: z7.string().min(1),
5354
6458
  ts: z7.string(),
@@ -5696,18 +6800,18 @@ async function commit2(root, base, next) {
5696
6800
  function excludesPath(content, relPath) {
5697
6801
  return isExcluded(normalizeName(relPath), false, parseIgnoreFile(content));
5698
6802
  }
5699
- async function addIgnoreEntry(root, relPath, base) {
6803
+ function planAddIgnoreEntry(content, relPath) {
5700
6804
  const entry = normalizeName(relPath);
5701
- if (excludesPath(base.content, relPath)) {
5702
- return { ok: true, content: base.content, changed: false };
6805
+ if (excludesPath(content, relPath)) {
6806
+ return { content, changed: false };
5703
6807
  }
5704
- const needsNewline = base.content !== "" && !base.content.endsWith("\n");
5705
- const next = `${base.content}${needsNewline ? "\n" : ""}${entry}
6808
+ const needsNewline = content !== "" && !content.endsWith("\n");
6809
+ const next = `${content}${needsNewline ? "\n" : ""}${entry}
5706
6810
  `;
5707
6811
  const parsed = parseIgnoreFile(next);
5708
6812
  if (parsed.unsupported.includes(entry)) {
5709
6813
  throw Object.assign(new Error(`${FILE} cannot express a rule for this path: ${relPath}`), {
5710
- 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."
5711
6815
  });
5712
6816
  }
5713
6817
  if (!excludesPath(next, relPath)) {
@@ -5715,24 +6819,32 @@ async function addIgnoreEntry(root, relPath, base) {
5715
6819
  hint: `This is a defect in the editor, not in your ${FILE}. Nothing was written.`
5716
6820
  });
5717
6821
  }
5718
- assertNothingLost(base.content, next, "excluding");
5719
- return commit2(root, base, next);
6822
+ assertNothingLost(content, next, "excluding");
6823
+ return { content: next, changed: true };
5720
6824
  }
5721
- async function removeIgnoreEntry(root, relPath, base) {
6825
+ function planRemoveIgnoreEntry(content, relPath) {
5722
6826
  const entry = normalizeName(relPath);
5723
- const lines2 = base.content.split("\n");
6827
+ const lines2 = content.split("\n");
5724
6828
  const kept = lines2.filter((line) => normalizeName(line.trim()) !== entry);
5725
- if (kept.length === lines2.length) {
5726
- return { ok: true, content: base.content, changed: false };
5727
- }
6829
+ if (kept.length === lines2.length) return { content, changed: false };
5728
6830
  const next = kept.join("\n");
5729
6831
  if (excludesPath(next, relPath)) {
5730
6832
  throw Object.assign(new Error(`${relPath} is still excluded after removing its line`), {
5731
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.`
5732
6834
  });
5733
6835
  }
5734
- assertNothingLost(next, base.content, "un-excluding");
5735
- 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);
5736
6848
  }
5737
6849
 
5738
6850
  // ../../packages/storage-adapter/src/local-file/cas-adapter.ts
@@ -6091,6 +7203,124 @@ async function readLock(path) {
6091
7203
 
6092
7204
  // src/commands/curation.ts
6093
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
6094
7324
  init_vault();
6095
7325
  init_ui();
6096
7326
 
@@ -6100,28 +7330,28 @@ init_errors();
6100
7330
  import { Command as Command4 } from "commander";
6101
7331
 
6102
7332
  // src/lib/events.ts
6103
- import { appendFile as appendFile2, mkdir as mkdir7, readFile as readFile12, readdir as readdir4 } from "fs/promises";
6104
- import { join as join11 } from "path";
6105
- import { z as z8 } from "zod";
6106
- var EventSchema = z8.object({
6107
- ts: z8.string(),
6108
- 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"]),
6109
7339
  /** Absent for resurface, which is the point of it. */
6110
- query: z8.string().nullable().default(null),
7340
+ query: z10.string().nullable().default(null),
6111
7341
  /** Where the caller was. The signal resurface ranks by. */
6112
- context_dir: z8.string(),
6113
- detail: z8.string(),
6114
- 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),
6115
7345
  /** Ids the call returned — the denominator for "was any of this used". */
6116
- returned_ids: z8.array(z8.string()).default([]),
7346
+ returned_ids: z10.array(z10.string()).default([]),
6117
7347
  /** The one that got opened, when this event is an open. */
6118
- opened_id: z8.string().nullable().default(null)
7348
+ opened_id: z10.string().nullable().default(null)
6119
7349
  });
6120
7350
  function eventsDir(vaultRoot) {
6121
7351
  return stateDirFor(vaultRoot);
6122
7352
  }
6123
7353
  function eventsFile(vaultRoot, ts) {
6124
- return join11(eventsDir(vaultRoot), `events-${ts.slice(0, 7)}.jsonl`);
7354
+ return join13(eventsDir(vaultRoot), `events-${ts.slice(0, 7)}.jsonl`);
6125
7355
  }
6126
7356
  async function recordEvent(vaultRoot, event) {
6127
7357
  try {
@@ -6136,7 +7366,7 @@ async function recordEvent(vaultRoot, event) {
6136
7366
  returned_ids: [...event.returnedIds ?? []],
6137
7367
  opened_id: event.openedId ?? null
6138
7368
  });
6139
- await mkdir7(eventsDir(vaultRoot), { recursive: true });
7369
+ await mkdir9(eventsDir(vaultRoot), { recursive: true });
6140
7370
  await appendFile2(eventsFile(vaultRoot, ts), `${JSON.stringify(parsed)}
6141
7371
  `, {
6142
7372
  encoding: "utf8",
@@ -6154,7 +7384,7 @@ async function readEvents(vaultRoot) {
6154
7384
  }
6155
7385
  const events = [];
6156
7386
  for (const file of files2) {
6157
- const raw = await readFile12(join11(eventsDir(vaultRoot), file), "utf8");
7387
+ const raw = await readFile14(join13(eventsDir(vaultRoot), file), "utf8");
6158
7388
  for (const line of raw.split("\n")) {
6159
7389
  if (line.trim() === "") continue;
6160
7390
  try {
@@ -6220,7 +7450,7 @@ var BASE_FILES = [
6220
7450
  }
6221
7451
  ];
6222
7452
  var HUB_PATH = "00_HUB/HUB.md";
6223
- function bodyOf(content) {
7453
+ function bodyOf2(content) {
6224
7454
  const stripped = content.replace(/^/, "");
6225
7455
  const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
6226
7456
  return (fence ? stripped.slice(fence[0].length) : stripped).trim();
@@ -6228,8 +7458,8 @@ function bodyOf(content) {
6228
7458
  function bodyStillTemplate(path, content) {
6229
7459
  const template = files("brain")[path];
6230
7460
  if (template === void 0) return false;
6231
- const body = bodyOf(content);
6232
- return body !== "" && body === bodyOf(template);
7461
+ const body = bodyOf2(content);
7462
+ return body !== "" && body === bodyOf2(template);
6233
7463
  }
6234
7464
  async function scaffoldingAmong(adapter, notes) {
6235
7465
  const basePaths = new Set(BASE_FILES.map((f) => f.path));
@@ -6305,112 +7535,147 @@ async function assembleStage(adapter, entry, detail, sectionName) {
6305
7535
  init_vault();
6306
7536
  init_ui();
6307
7537
  function openCommand() {
6308
- 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) => {
6309
- const detail = parseDetail(opts.detail, opts.section ? "chunks" : "card");
6310
- const { brain } = await resolveBrain(opts.vault);
6311
- const adapter = await openBrainOrExplain(brain.root);
6312
- const built = await openIndex(adapter, brain.root);
6313
- const entry = resolveRef(built.entries, ref);
6314
- await recordEvent(brain.root, {
6315
- verb: "open",
6316
- detail,
6317
- returnedIds: [entry.memory.id],
6318
- openedId: entry.memory.id
6319
- });
6320
- const stage = await assembleStage(adapter, entry, detail, opts.section);
6321
- const claims = entry.claims;
6322
- const now = /* @__PURE__ */ new Date();
6323
- const handle2 = displayHandle(
6324
- entry.memory.id,
6325
- built.entries.map((e) => e.memory.id)
6326
- );
6327
- const scaffolding = (await scaffoldingAmong(adapter, [{ path: entry.path, title: entry.memory.title }])).has(entry.path);
6328
- printDual({
6329
- json: {
6330
- id: entry.memory.id,
6331
- handle: handle2,
6332
- 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",
6333
7580
  detail,
6334
- scaffolding,
6335
- memory: entry.memory,
6336
- claims,
6337
- relations: entry.relations,
6338
- ...stage
6339
- },
6340
- human: () => {
6341
- 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}
6342
7606
  `);
6343
- if (scaffolding) {
6344
- printHuman(
6345
- c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote")
6346
- );
6347
- } else {
6348
- printHuman(
6349
- `${confidenceOf(entry.memory, claims)} \xB7 ${freshnessOf(entry.memory, claims, now)} \xB7 ${entry.memory.provenance.mapping}`
6350
- );
6351
- }
6352
- 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(`
6353
7617
  \`id: ${entry.memory.id}\` \xB7 \`${entry.path}\`
6354
7618
  `);
6355
- if (entry.memory.summary) printHuman(quoteBlock(entry.memory.summary) + "\n");
6356
- for (const claim of claims) {
6357
- printHuman(`- ${claim.text}`);
6358
- printHuman(c.gray(` ${claim.confidence} \xB7 ${claim.status}`));
6359
- }
6360
- if (claims.length === 0 && scaffolding) {
6361
- printHuman(
6362
- c.gray(
6363
- `_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
6364
7628
  body is its instructions \u2014 neither is something the user said, so do not answer
6365
7629
  from them._
6366
7630
 
6367
7631
  This one is filled in by asking. \`memgineering onboard\` prints the questions;
6368
7632
  write the body, then record the conclusion:
6369
7633
  memgineering revise ${handle2} --claim "<what they told you>" --summary "<one line>"`
6370
- )
6371
- );
6372
- } else if (claims.length === 0) {
6373
- printHuman(
6374
- c.gray(
6375
- 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._"
6376
- )
6377
- );
6378
- printHuman(
6379
- c.gray(
6380
- `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:
6381
7645
  memgineering revise ${handle2} --claim "<the conclusion>" --summary "<one line>"
6382
7646
  Add --contradicts <id> when it disagrees with another memory; that edge is what
6383
7647
  makes the two surface together as a conflict.`
6384
- )
6385
- );
6386
- }
6387
- if (stage.outline && stage.outline.length > 0) {
6388
- printHuman("\n### sections\n");
6389
- for (const heading of stage.outline) printHuman(`- ${heading}`);
6390
- printHuman("");
6391
- }
6392
- if (stage.sections) {
6393
- for (const section of stage.sections) {
6394
- 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(`
6395
7659
  ### ${section.label}
6396
7660
  `);
6397
- 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
+ );
6398
7674
  }
6399
- printHuman("");
6400
- }
6401
- if (stage.body) {
6402
- printHuman("\n### full\n");
6403
- printHuman(quoteBlock(stage.body));
6404
- printHuman("");
6405
- }
6406
- if (detail === "card") {
6407
- printHuman(
6408
- c.gray(`Deeper: \`memgineering open ${handle2} --detail summary | chunks | full\``)
6409
- );
6410
7675
  }
6411
- }
6412
- });
6413
- });
7676
+ });
7677
+ }
7678
+ );
6414
7679
  }
6415
7680
  function resolveRef(entries, ref) {
6416
7681
  const ids = entries.map((e) => e.memory.id);
@@ -6468,8 +7733,16 @@ init_link();
6468
7733
  function lifecycleCommand(action) {
6469
7734
  return new Command6(action).description(
6470
7735
  action === "retire" ? "mark a memory as no longer current \u2014 the note itself is not changed" : "mark a retired memory as current again"
6471
- ).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) => {
6472
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
+ }
6473
7746
  const { brain } = await resolveBrain(opts.vault);
6474
7747
  const adapter = await LocalFileCasAdapter.openVault(brain.root, {
6475
7748
  createPrefix: brain.observationsDir
@@ -6477,40 +7750,8 @@ function lifecycleCommand(action) {
6477
7750
  const built = await openIndex(adapter, brain.root);
6478
7751
  const entry = resolveRef(built.entries, ref);
6479
7752
  const result = commit(
6480
- {
6481
- candidate: {
6482
- subject: entry.memory.title,
6483
- scope: entry.memory.scope,
6484
- kind: entry.memory.type,
6485
- candidate_claim: entry.memory.summary ?? entry.memory.title,
6486
- entities: [],
6487
- importance: "normal"
6488
- },
6489
- matches: [
6490
- {
6491
- id: entry.memory.id,
6492
- title: entry.memory.title,
6493
- summary: entry.memory.summary,
6494
- path: entry.path,
6495
- revision: entry.revision,
6496
- freshness: "current",
6497
- score: 1
6498
- }
6499
- ],
6500
- contradicts: [],
6501
- scopes: entry.memory.scope ? [entry.memory.scope] : []
6502
- },
6503
- {
6504
- action,
6505
- memory_id: entry.memory.id,
6506
- claim: null,
6507
- reason: opts.reason ?? `${action}d directly`,
6508
- // For a standing change the memory's own summary IS the passage the
6509
- // decision is about: it is what the note currently says, and it is
6510
- // what stops being current.
6511
- source_excerpt: entry.memory.summary ?? entry.memory.title,
6512
- target_path: entry.path
6513
- }
7753
+ asSingleMatch(entry),
7754
+ lifecycleInput(entry, action, opts.reason ?? null)
6514
7755
  );
6515
7756
  if (result.action === "ignore") {
6516
7757
  throw memgError(
@@ -6572,8 +7813,16 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
6572
7813
  function ignoreCommand(mode) {
6573
7814
  return new Command6(mode).description(
6574
7815
  mode === "exclude" ? "stop reading a note entirely \u2014 the note itself is not changed" : "read a note again after excluding it"
6575
- ).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) => {
6576
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
+ }
6577
7826
  const { brain } = await resolveBrain(opts.vault);
6578
7827
  const adapter = await LocalFileCasAdapter.openVault(brain.root, {
6579
7828
  createPrefix: brain.observationsDir
@@ -6626,6 +7875,57 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
6626
7875
  });
6627
7876
  });
6628
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";
6629
7929
  async function mustMatchAFile(root, adapter, path) {
6630
7930
  const wanted = path.normalize("NFC").replace(/^\/+/, "");
6631
7931
  const absolute = resolve6(root, wanted);
@@ -6651,8 +7951,31 @@ init_index_build();
6651
7951
  init_vault();
6652
7952
  init_ui();
6653
7953
  function evidenceCommand() {
6654
- 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) => {
6655
- 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;
6656
7979
  const adapter = await openBrainOrExplain(brain.root);
6657
7980
  const built = await openIndex(adapter, brain.root);
6658
7981
  const entry = resolveRef(built.entries, ref);
@@ -6816,8 +8139,41 @@ import { Command as Command8 } from "commander";
6816
8139
  init_vault();
6817
8140
  init_ui();
6818
8141
  function logCommand() {
6819
- 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) => {
6820
- 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;
6821
8177
  const adapter = await openBrainOrExplain(brain.root);
6822
8178
  const { ops, skipped } = await readOps(brain.root);
6823
8179
  const recent = ops.slice(-opts.limit).reverse();
@@ -6914,12 +8270,111 @@ async function readOrNull(adapter, path) {
6914
8270
  }
6915
8271
  }
6916
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
+
6917
8372
  // src/commands/onboard.ts
6918
- import { Command as Command9 } from "commander";
8373
+ import { Command as Command11 } from "commander";
6919
8374
  init_vault();
6920
8375
  init_ui();
6921
8376
  function onboardCommand() {
6922
- 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) => {
6923
8378
  const { brain } = await resolveBrain(opts.vault);
6924
8379
  const adapter = await openBrainOrExplain(brain.root);
6925
8380
  const untouched = await untouchedBase(
@@ -6981,48 +8436,402 @@ function onboardCommand() {
6981
8436
  });
6982
8437
  }
6983
8438
 
6984
- // src/commands/recall.ts
6985
- init_src();
6986
- import { Command as Command10 } from "commander";
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
+
8753
+ // src/commands/recall.ts
8754
+ init_src();
8755
+ import { Command as Command14 } from "commander";
6987
8756
  init_index_build();
8757
+ init_errors();
6988
8758
  init_vault();
6989
8759
  init_ui();
6990
8760
  function recallCommand() {
6991
- 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(
6992
8762
  async (query, opts) => {
6993
8763
  const detail = parseDetail(opts.detail, "card");
6994
- const { brain } = await resolveBrain(opts.vault);
6995
- const adapter = await openBrainOrExplain(brain.root);
6996
- const built = await openIndex(adapter, brain.root);
6997
- const result = recall(built.entries, query, {
6998
- limit: opts.limit,
6999
- ...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 }
7000
8767
  });
7001
- await recordEvent(brain.root, {
7002
- verb: "recall",
7003
- query,
7004
- detail,
7005
- limit: opts.limit,
7006
- returnedIds: result.memory_cards.map((card) => card.id)
7007
- });
7008
- const allIds = built.entries.map((e) => e.memory.id);
7009
- const byId = new Map(built.entries.map((e) => [e.memory.id, e]));
7010
- const rows = await Promise.all(
7011
- result.memory_cards.map(async (card) => ({
7012
- card,
7013
- handle: displayHandle(card.id, allIds),
7014
- stage: await stageFor(adapter, byId.get(card.id), detail)
7015
- }))
7016
- );
7017
- const scaffolding = await scaffoldingAmong(
7018
- adapter,
7019
- rows.map(({ card }) => {
7020
- const entry = byId.get(card.id);
7021
- return { path: entry?.path ?? "", title: entry?.memory.title ?? "" };
7022
- })
7023
- );
7024
- const isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
7025
- 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
+ }
7026
8835
  printDual({
7027
8836
  json: {
7028
8837
  query,
@@ -7030,7 +8839,7 @@ function recallCommand() {
7030
8839
  cards: rows.map(({ card, handle: handle2, stage }) => ({
7031
8840
  ...card,
7032
8841
  handle: handle2,
7033
- path: byId.get(card.id)?.path ?? null,
8842
+ path: pathOf(card),
7034
8843
  // A fact about the file, not a judgement about the match: this
7035
8844
  // note is still exactly what `init` wrote, so nobody has answered
7036
8845
  // it yet.
@@ -7046,7 +8855,7 @@ function recallCommand() {
7046
8855
  },
7047
8856
  human: () => {
7048
8857
  const reachedSomethingWritten = rows.some(({ card }) => !isScaffolding(card));
7049
- const otherScript = reachedSomethingWritten ? null : unreachableScript(query, built.entries);
8858
+ const otherScript = reachedSomethingWritten ? null : entries ? unreachableScript(query, entries) : null;
7050
8859
  const sayWhy = () => {
7051
8860
  printHuman(
7052
8861
  c.yellow(
@@ -7184,7 +8993,8 @@ function ago2(days) {
7184
8993
  function unreachableScript(query, entries) {
7185
8994
  const queryScripts = scriptsIn(query);
7186
8995
  if (queryScripts.size === 0) return null;
7187
- 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)));
7188
8998
  const counts = /* @__PURE__ */ new Map();
7189
8999
  for (const entry of written) {
7190
9000
  const script = dominantScriptOf(`${entry.memory.title} ${entry.memory.summary ?? ""}`);
@@ -7193,6 +9003,12 @@ function unreachableScript(query, entries) {
7193
9003
  if (counts.size === 0) return null;
7194
9004
  const [dominant, seen] = [...counts].sort((a, b) => b[1] - a[1])[0];
7195
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
+ }
7196
9012
  return seen >= Math.max(2, written.length / 2) ? dominant : null;
7197
9013
  }
7198
9014
  var SCRIPT_RANGES = [
@@ -7231,73 +9047,103 @@ function scriptsIn(text) {
7231
9047
  }
7232
9048
 
7233
9049
  // src/commands/remember.ts
7234
- import { Command as Command11 } from "commander";
9050
+ import { Command as Command15 } from "commander";
7235
9051
  init_errors();
7236
9052
  init_vault();
7237
9053
  init_ui();
7238
9054
  function rememberCommand() {
7239
- 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(
7240
9056
  "--reason <text>",
7241
9057
  "why you are recording this \u2014 goes in the ledger, not in the note (the fact itself belongs in <text>)"
7242
- ).action(async (text, opts) => {
7243
- const body = stripControl(text).trim();
7244
- if (body === "") {
7245
- throw memgError(
7246
- "invalid_input",
7247
- "nothing to remember",
7248
- 'Pass the text: `memgineering remember "deploys are manual \u2014 launchctl by hand"`'
7249
- );
7250
- }
7251
- const rationale = sanitizeRationale(opts.reason);
7252
- const { brain } = await resolveBrain(opts.vault);
7253
- await openBrainOrExplain(brain.root);
7254
- const now = /* @__PURE__ */ new Date();
7255
- const id = newId();
7256
- const month = now.toISOString().slice(0, 7);
7257
- const dir = `${brain.observationsDir.replace(/\/+$/, "")}/${month}`;
7258
- const path = `${dir}/obs-${id}.md`;
7259
- const content = renderObservation({
7260
- id,
7261
- text: body,
7262
- now,
7263
- ...opts.scope ? { scope: opts.scope } : {}
7264
- });
7265
- const record = await withWriteLock(brain.root, async () => {
7266
- const adapter = await LocalFileCasAdapter.openVault(brain.root, {
7267
- // Writes are confined to the observations folder. `remember` creates;
7268
- // it has no business being able to author a file anywhere in someone's
7269
- // vault, and the adapter enforces that rather than this command
7270
- // promising it.
7271
- createPrefix: brain.observationsDir
7272
- });
7273
- const result = await adapter.create(path, content);
7274
- if (!result.ok) {
9058
+ ).action(
9059
+ async (text, opts) => {
9060
+ const body = stripControl(text).trim();
9061
+ if (body === "") {
7275
9062
  throw memgError(
7276
9063
  "invalid_input",
7277
- `a note already exists at ${path}`,
7278
- "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"`'
7279
9066
  );
7280
9067
  }
7281
- return appendOp(brain.root, {
7282
- verb: "remember",
7283
- target: path,
7284
- memoryId: id,
7285
- hashBefore: null,
7286
- hashAfter: hashContent(content),
7287
- rationale,
7288
- 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 }
7289
9072
  });
7290
- });
7291
- printDual({
7292
- json: { id, path, op_id: record.op_id, scope: opts.scope ?? null },
7293
- human: () => {
7294
- 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}.
7295
9090
  `);
7296
- printHuman(`note: \`${id}\` \xB7 \`${path}\``);
7297
- 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;
7298
9096
  }
7299
- });
7300
- });
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
+ );
7301
9147
  }
7302
9148
  function renderObservation(args) {
7303
9149
  const firstLine = args.text.split("\n")[0] ?? args.text;
@@ -7328,163 +9174,170 @@ function yamlString(value) {
7328
9174
 
7329
9175
  // src/commands/resurface.ts
7330
9176
  init_src();
7331
- import { Command as Command12 } from "commander";
9177
+ import { Command as Command16 } from "commander";
7332
9178
  init_index_build();
7333
9179
  init_vault();
7334
9180
  init_ui();
7335
9181
  function resurfaceCommand() {
7336
- 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) => {
7337
- const contextDir = opts.contextDir ?? process.cwd();
7338
- const { brain } = await resolveBrain(opts.vault, contextDir);
7339
- const adapter = await openBrainOrExplain(brain.root);
7340
- const built = await openIndex(adapter, brain.root);
7341
- const events = await readEvents(brain.root);
7342
- const now = /* @__PURE__ */ new Date();
7343
- const ranked = rank(built.entries, events, { contextDir, now }).slice(0, opts.limit);
7344
- const untouched = await untouchedBase(
7345
- adapter,
7346
- ranked.map((r) => r.entry.path)
7347
- );
7348
- const allIds = built.entries.map((e) => e.memory.id);
7349
- await recordEvent(brain.root, {
7350
- verb: "resurface",
7351
- contextDir,
7352
- detail: "card",
7353
- limit: opts.limit,
7354
- returnedIds: ranked.map((r) => r.entry.memory.id),
7355
- now
7356
- });
7357
- printDual({
7358
- json: {
7359
- context_dir: contextDir,
7360
- cards: ranked.map(({ entry, score, why }) => ({
7361
- id: entry.memory.id,
7362
- handle: displayHandle(entry.memory.id, allIds),
7363
- title: entry.memory.title,
7364
- summary: entry.memory.summary,
7365
- path: entry.path,
7366
- score: Number(score.toFixed(3)),
7367
- why,
7368
- unfilled: untouched.has(entry.path)
7369
- }))
7370
- },
7371
- human: () => {
7372
- if (ranked.length === 0) {
7373
- printHuman("## resurface\n");
7374
- printHuman("_Nothing to bring up yet._\n");
7375
- printHuman(
7376
- c.gray(
7377
- "This ranks by what has been recalled here before, so it gets useful once the brain has been used a few times."
7378
- )
7379
- );
7380
- return;
7381
- }
7382
- printHuman(`## resurface (${ranked.length} cards)
7383
- `);
7384
- for (const [i, { entry, why }] of ranked.entries()) {
7385
- const blank = untouched.has(entry.path);
7386
- printHuman(`### ${i + 1}. ${entry.memory.title}`);
7387
- printHuman(blank ? c.yellow("not filled in yet") : why.join(" \xB7 "));
7388
- 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");
7389
9245
  printHuman(
7390
9246
  c.gray(
7391
- `
7392
- This is still the file \`init\` created \u2014 the line below is its prompt, not
7393
- 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."
7394
9248
  )
7395
9249
  );
9250
+ return;
7396
9251
  }
7397
- if (entry.memory.summary) printHuman(`
7398
- ${quoteBlock(entry.memory.summary)}`);
7399
- printHuman(`
7400
- \`open: ${displayHandle(entry.memory.id, allIds)}\`
9252
+ printHuman(`## resurface (${rankedShown.length} cards)
7401
9253
  `);
7402
- }
7403
- }
7404
- });
7405
- });
7406
- }
7407
- var WEIGHT = {
7408
- contextRecall: 3,
7409
- anyRecall: 1,
7410
- recency: 2,
7411
- staleBase: 4
7412
- };
7413
- var STALE_BASE_DAYS = 30;
7414
- function rank(entries, events, opts) {
7415
- const recalls = events.filter((e) => e.verb !== "resurface");
7416
- const here = recalls.filter((e) => isInside(e.context_dir, opts.contextDir));
7417
- const countIn = (source) => {
7418
- const counts = /* @__PURE__ */ new Map();
7419
- for (const event of source) {
7420
- for (const id of event.returned_ids) counts.set(id, (counts.get(id) ?? 0) + 1);
7421
- if (event.opened_id) counts.set(event.opened_id, (counts.get(event.opened_id) ?? 0) + 1);
7422
- }
7423
- return counts;
7424
- };
7425
- const hereCounts = countIn(here);
7426
- const anyCounts = countIn(recalls);
7427
- const lastTouched = /* @__PURE__ */ new Map();
7428
- for (const event of recalls) {
7429
- const t = Date.parse(event.ts);
7430
- if (!Number.isFinite(t)) continue;
7431
- for (const id of [...event.returned_ids, event.opened_id].filter(Boolean)) {
7432
- lastTouched.set(id, Math.max(lastTouched.get(id) ?? 0, t));
7433
- }
7434
- }
7435
- const hasBase = entries.some((e) => e.path.startsWith("01_BASE/"));
7436
- const scored = [];
7437
- for (const entry of entries) {
7438
- const id = entry.memory.id;
7439
- const freshness = freshnessOf(entry.memory, entry.claims, opts.now);
7440
- if (freshness === "retired") continue;
7441
- const why = [];
7442
- let score = 0;
7443
- const hereCount = hereCounts.get(id) ?? 0;
7444
- if (hereCount > 0) {
7445
- score += WEIGHT.contextRecall * Math.log2(1 + hereCount);
7446
- why.push(`recalled ${hereCount}\xD7 in this folder`);
7447
- }
7448
- const anyCount = (anyCounts.get(id) ?? 0) - hereCount;
7449
- if (anyCount > 0) {
7450
- score += WEIGHT.anyRecall * Math.log2(1 + anyCount);
7451
- if (hereCount === 0) why.push(`recalled ${anyCount}\xD7 elsewhere`);
7452
- }
7453
- const touched = lastTouched.get(id);
7454
- if (touched !== void 0) {
7455
- const days = (opts.now.getTime() - touched) / 864e5;
7456
- 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
+ });
7457
9285
  }
7458
- if (hasBase && entry.path.startsWith("01_BASE/")) {
7459
- const days = touched === void 0 ? Number.POSITIVE_INFINITY : (opts.now.getTime() - touched) / 864e5;
7460
- if (days > STALE_BASE_DAYS) {
7461
- score += WEIGHT.staleBase;
7462
- why.push(
7463
- touched === void 0 ? "never read this session" : `not read in ${Math.floor(days)} days`
7464
- );
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
+ `);
7465
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
+ );
7466
9324
  }
7467
- if (freshness !== "current") why.push(freshness);
7468
- if (score > 0) scored.push({ entry, score, why });
7469
- }
7470
- return scored.sort(
7471
- (a, b) => b.score - a.score || a.entry.memory.id.localeCompare(b.entry.memory.id)
7472
- );
9325
+ });
7473
9326
  }
7474
9327
 
7475
9328
  // src/commands/revise.ts
7476
9329
  init_src();
7477
- import { readFile as readFile14 } from "fs/promises";
7478
- import { Command as Command13 } from "commander";
9330
+ import { readFile as readFile17 } from "fs/promises";
9331
+ import { Command as Command17 } from "commander";
7479
9332
  init_errors();
7480
9333
  init_index_build();
7481
9334
  init_vault();
7482
9335
  init_ui();
7483
9336
  function reviseCommand() {
7484
- 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(
7485
9338
  "--valid-from <iso>",
7486
9339
  "when this became true (defaults: supersede = now, otherwise unchanged)"
7487
- ).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(
7488
9341
  "after",
7489
9342
  [
7490
9343
  "",
@@ -7500,6 +9353,14 @@ function reviseCommand() {
7500
9353
  "always true, you have only just been told it. `supersede` would date it today."
7501
9354
  ].join("\n")
7502
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
+ }
7503
9364
  const { brain } = await resolveBrain(opts.vault);
7504
9365
  await openBrainOrExplain(brain.root);
7505
9366
  const adapter = await LocalFileCasAdapter.openVault(brain.root, {
@@ -7520,7 +9381,7 @@ function reviseCommand() {
7520
9381
  );
7521
9382
  }
7522
9383
  const proposal = result.proposal;
7523
- if (proposal.action !== "conflict" && rationale === null && saysNothingNew(entry, input)) {
9384
+ if (proposal.action !== "conflict" && rationale === null && saysNothingNew(entry, input, stripControl)) {
7524
9385
  throw memgError(
7525
9386
  "invalid_input",
7526
9387
  `${entry.memory.title} already says this \u2014 nothing was written`,
@@ -7590,7 +9451,12 @@ To see what is there now: \`memgineering open ` + ref + "`"
7590
9451
  });
7591
9452
  return { record, content: outcome.content };
7592
9453
  });
7593
- 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;
7594
9460
  const unwrittenBody = bodyStillTemplate(entry.path, applied.content);
7595
9461
  printDual({
7596
9462
  json: {
@@ -7650,7 +9516,95 @@ Undo: \`${undoHint(applied.record.op_id, opts.vault)}\``));
7650
9516
  });
7651
9517
  });
7652
9518
  }
7653
- 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
+ }
7654
9608
  async function buildCommitInput(entry, opts) {
7655
9609
  if (opts.input) {
7656
9610
  const raw = await readInput(opts.input);
@@ -7671,104 +9625,20 @@ async function buildCommitInput(entry, opts) {
7671
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`."
7672
9626
  );
7673
9627
  }
7674
- return {
9628
+ return reviseInput(entry, {
7675
9629
  action,
7676
- memory_id: entry.memory.id,
7677
- claim: {
7678
- text: opts.claim,
7679
- scope: entry.memory.scope,
7680
- // WHEN THE FACT BECAME TRUE, not when somebody edited the note.
7681
- //
7682
- // This was `new Date()` unconditionally, so `reinforce` the action for
7683
- // "same conclusion, new support" — walked the date forward every time it
7684
- // ran. A tester added a reason to a memory and watched its start date
7685
- // move a day later than the thing it describes. `valid_from` is the field
7686
- // a temporal memory store exists to get right, and it was being used as
7687
- // an updated-at; the drift is silent and cumulative.
7688
- //
7689
- // `supersede` still stamps now, because a replaced conclusion IS starting
7690
- // now — that is what supersede means. `reinforce` and `conflict` keep
7691
- // whatever the memory already declared, and `--valid-from` overrides
7692
- // either when the caller knows the real date.
7693
- valid_from: validFrom(action, opts, entry),
7694
- confidence: "stated",
7695
- summary: action === "conflict" ? null : opts.summary ?? opts.claim,
7696
- // Never defaulted from the claim, unlike the summary. A summary that is
7697
- // missing makes the change invisible, so filling it in is a rescue; a
7698
- // title that is missing simply means the author's heading still fits, and
7699
- // overwriting it would be rewriting their classification uninvited.
7700
- title: action === "conflict" ? null : opts.title ?? null
7701
- },
7702
- relations: {
7703
- depends_on: [],
7704
- relates_to: [],
7705
- contradicts: opts.contradicts ?? []
7706
- },
7707
- reason: opts.reason ?? NO_REASON_GIVEN,
7708
- // The engine refuses to submit without an excerpt, because approving a
7709
- // diff on an assertion alone is not consent. Nothing here is approved by a
7710
- // person, but the requirement still earns its keep: it forces the record
7711
- // to say what the change was based on rather than only what it did.
7712
- source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
7713
- target_path: entry.path
7714
- };
7715
- }
7716
- function asSingleMatch(entry) {
7717
- return {
7718
- candidate: {
7719
- subject: entry.memory.title,
7720
- scope: entry.memory.scope,
7721
- kind: entry.memory.type,
7722
- candidate_claim: entry.memory.summary ?? entry.memory.title,
7723
- entities: [],
7724
- importance: "normal"
7725
- },
7726
- matches: [
7727
- {
7728
- id: entry.memory.id,
7729
- title: entry.memory.title,
7730
- summary: entry.memory.summary,
7731
- path: entry.path,
7732
- revision: entry.revision,
7733
- freshness: freshnessOf(entry.memory, entry.claims, /* @__PURE__ */ new Date()),
7734
- score: 1
7735
- }
7736
- ],
7737
- contradicts: [],
7738
- scopes: entry.memory.scope ? [entry.memory.scope] : []
7739
- };
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
+ });
7740
9638
  }
7741
9639
  function isCritical(entry) {
7742
9640
  return /^01_BASE\//.test(entry.path);
7743
9641
  }
7744
- function previousClaim(entry) {
7745
- const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
7746
- return current?.text ?? null;
7747
- }
7748
- function saysNothingNew(entry, input) {
7749
- const norm = (v) => stripControl(v ?? "").trim();
7750
- const claim = input.claim;
7751
- if (!claim || norm(claim.text) === "") return false;
7752
- if (norm(claim.text) !== norm(previousClaim(entry))) return false;
7753
- if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
7754
- if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
7755
- const declared = new Set(
7756
- entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
7757
- );
7758
- return (input.relations?.contradicts ?? []).every((id) => declared.has(id));
7759
- }
7760
- function bodyStillAsserts(content, oldClaim, newClaim) {
7761
- if (oldClaim === null) return null;
7762
- const line = stripControl(oldClaim.split("\n")[0] ?? "").trim();
7763
- if (line === "") return null;
7764
- if (line === stripControl(newClaim?.split("\n")[0] ?? "").trim()) return null;
7765
- return bodyOf2(content).includes(line) ? line : null;
7766
- }
7767
- function bodyOf2(content) {
7768
- const stripped = content.replace(/^/, "");
7769
- const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
7770
- return fence ? stripped.slice(fence[0].length) : stripped;
7771
- }
7772
9642
  function renderDiff(before, after) {
7773
9643
  return ["```diff", ...lines(before, "-"), ...lines(after, "+"), "```"].join("\n");
7774
9644
  }
@@ -7776,13 +9646,13 @@ function lines(text, marker) {
7776
9646
  return text === "" ? [] : text.split("\n").map((l) => `${marker} ${l}`);
7777
9647
  }
7778
9648
  async function readInput(source) {
7779
- const raw = source === "-" ? await new Promise((resolve10, reject) => {
9649
+ const raw = source === "-" ? await new Promise((resolve11, reject) => {
7780
9650
  let buf = "";
7781
9651
  process.stdin.setEncoding("utf8");
7782
9652
  process.stdin.on("data", (chunk) => buf += chunk);
7783
- process.stdin.on("end", () => resolve10(buf));
9653
+ process.stdin.on("end", () => resolve11(buf));
7784
9654
  process.stdin.on("error", reject);
7785
- }) : await readFile14(source, "utf8").catch(() => source);
9655
+ }) : await readFile17(source, "utf8").catch(() => source);
7786
9656
  try {
7787
9657
  return JSON.parse(raw);
7788
9658
  } catch {
@@ -7793,25 +9663,19 @@ async function readInput(source) {
7793
9663
  );
7794
9664
  }
7795
9665
  }
7796
- function validFrom(action, opts, entry) {
7797
- if (opts.validFrom) return opts.validFrom;
7798
- if (action === "supersede") return (/* @__PURE__ */ new Date()).toISOString();
7799
- const declared = entry.claims.find((c2) => c2.valid_from)?.valid_from;
7800
- return declared ?? null;
7801
- }
7802
9666
 
7803
9667
  // src/program.ts
7804
9668
  init_setup();
7805
9669
 
7806
9670
  // src/commands/update.ts
7807
- import { Command as Command15 } from "commander";
9671
+ import { Command as Command19 } from "commander";
7808
9672
 
7809
9673
  // src/version.ts
7810
9674
  import { readFileSync } from "fs";
7811
9675
  import { fileURLToPath as fileURLToPath2 } from "url";
7812
- import { dirname as dirname4, join as join16 } from "path";
7813
- var __dir = dirname4(fileURLToPath2(import.meta.url));
7814
- 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");
7815
9679
  var VERSION = (() => {
7816
9680
  try {
7817
9681
  const parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
@@ -7827,7 +9691,7 @@ init_config();
7827
9691
  // src/lib/provenance.ts
7828
9692
  import { execFile } from "child_process";
7829
9693
  import { realpath as realpath3 } from "fs/promises";
7830
- 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";
7831
9695
  import { fileURLToPath as fileURLToPath3 } from "url";
7832
9696
  import { promisify } from "util";
7833
9697
  var run = promisify(execFile);
@@ -7848,7 +9712,7 @@ var MANAGERS = [
7848
9712
  ];
7849
9713
  async function detectProvenance(selfPath) {
7850
9714
  const entry = selfPath ?? fileURLToPath3(import.meta.url);
7851
- const selfDir = await realpath3(dirname5(entry)).catch(() => dirname5(entry));
9715
+ const selfDir = await realpath3(dirname6(entry)).catch(() => dirname6(entry));
7852
9716
  if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
7853
9717
  return {
7854
9718
  kind: "npx",
@@ -7893,8 +9757,8 @@ async function prefixOf(bin, args) {
7893
9757
  }
7894
9758
  }
7895
9759
  function isInside2(child, parent) {
7896
- const c2 = resolve8(child);
7897
- const p = resolve8(parent);
9760
+ const c2 = resolve9(child);
9761
+ const p = resolve9(parent);
7898
9762
  return c2 === p || c2.startsWith(p.endsWith("/") ? p : `${p}/`);
7899
9763
  }
7900
9764
 
@@ -7902,16 +9766,16 @@ function isInside2(child, parent) {
7902
9766
  init_atomic();
7903
9767
  init_brand();
7904
9768
  import { execFile as execFile2 } from "child_process";
7905
- import { mkdir as mkdir10, readFile as readFile16, writeFile as writeFile7 } from "fs/promises";
7906
- 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";
7907
9771
  import { promisify as promisify2 } from "util";
7908
9772
  import semver from "semver";
7909
- import { z as z9 } from "zod";
9773
+ import { z as z11 } from "zod";
7910
9774
  var run2 = promisify2(execFile2);
7911
- var PolicySchema = z9.object({
7912
- latest: z9.string().optional(),
7913
- minimum_supported: z9.string().optional(),
7914
- 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()
7915
9779
  });
7916
9780
  var POLICY_URL = "https://memgineering.com/version-policy.json";
7917
9781
  var NOTICE_MAX = 300;
@@ -7976,19 +9840,19 @@ function shouldCheck(lastCheck, now) {
7976
9840
  if (!Number.isFinite(last)) return true;
7977
9841
  return now.getTime() - last > 864e5;
7978
9842
  }
7979
- var UpdateStateSchema = z9.object({
7980
- at: z9.string(),
7981
- ok: z9.boolean(),
7982
- from: z9.string(),
7983
- to: z9.string().nullable(),
7984
- 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()
7985
9849
  });
7986
9850
  function statePath() {
7987
- return join18(brandHome(), "update-state.json");
9851
+ return join21(brandHome(), "update-state.json");
7988
9852
  }
7989
9853
  async function readUpdateState() {
7990
9854
  try {
7991
- const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile16(statePath(), "utf8")));
9855
+ const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile19(statePath(), "utf8")));
7992
9856
  return parsed.success ? parsed.data : null;
7993
9857
  } catch {
7994
9858
  return null;
@@ -7996,7 +9860,7 @@ async function readUpdateState() {
7996
9860
  }
7997
9861
  async function writeUpdateState(state) {
7998
9862
  try {
7999
- await mkdir10(brandHome(), { recursive: true });
9863
+ await mkdir13(brandHome(), { recursive: true });
8000
9864
  await writeThenRename(statePath(), `${JSON.stringify(state, null, 2)}
8001
9865
  `);
8002
9866
  } catch {
@@ -8052,7 +9916,7 @@ function shortMessage(err) {
8052
9916
  // src/commands/update.ts
8053
9917
  init_ui();
8054
9918
  function updateCommand() {
8055
- 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) => {
8056
9920
  const status = await checkForUpdate(VERSION);
8057
9921
  const provenance = await detectProvenance();
8058
9922
  const previous = await readUpdateState();
@@ -8158,117 +10022,140 @@ function reportBelowMinimum(below) {
8158
10022
  }
8159
10023
 
8160
10024
  // src/commands/undo.ts
8161
- import { readFile as readFile17, rm as rm8 } from "fs/promises";
8162
- import { join as join19 } from "path";
8163
- 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";
8164
10028
  init_atomic();
8165
10029
  init_errors();
8166
10030
  init_vault();
8167
10031
  init_ui();
8168
10032
  function undoCommand() {
8169
- 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) => {
8170
- const rationale = sanitizeRationale(opts.reason);
8171
- const { brain } = await resolveBrain(opts.vault);
8172
- await openBrainOrExplain(brain.root);
8173
- const { ops, skipped } = await readOps(brain.root);
8174
- if (opId === void 0 && skipped > 0) {
8175
- throw memgError(
8176
- "ledger_divergence",
8177
- `this brain has ${skipped} unreadable ledger line(s), so "the last change" is not knowable`,
8178
- '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.'
8179
- );
8180
- }
8181
- const matches = opId ? matchIds(
8182
- opId,
8183
- ops.map((o) => o.op_id)
8184
- ) : [];
8185
- if (matches.length > 1) {
8186
- throw memgError(
8187
- "ambiguous_ref",
8188
- `"${opId}" matches ${matches.length} operations`,
8189
- `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.
8190
10076
  ${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "\n \u2026" : ""}`
8191
- );
8192
- }
8193
- const target = opId ? ops.find((o) => o.op_id === matches[0]) : lastReversible(ops);
8194
- if (!target) {
8195
- throw opId ? memgError(
8196
- "nothing_to_undo",
8197
- `no operation here matches: ${opId}`,
8198
- "An id is 26 characters; a prefix must be at least 5 and must be unique. `memgineering log` shows what this brain recorded."
8199
- ) : memgError(
8200
- "nothing_to_undo",
8201
- "nothing to undo in this brain",
8202
- "Only changes made through memgineering are recorded. Edits made in your editor are yours to revert."
8203
- );
8204
- }
8205
- if (target.verb === "undo") {
8206
- throw memgError(
8207
- "nothing_to_undo",
8208
- "that operation is itself an undo",
8209
- "Undoing an undo is a fresh change \u2014 make it with `revise` or `remember`."
8210
- );
8211
- }
8212
- if (ops.some((o) => o.verb === "undo" && o.supersedes === target.op_id)) {
8213
- throw memgError(
8214
- "nothing_to_undo",
8215
- `${target.op_id} has already been undone`,
8216
- "Run `memgineering log` to see the current state of this brain."
8217
- );
8218
- }
8219
- const record = await withWriteLock(brain.root, async () => {
8220
- const absolute = join19(brain.root, target.target);
8221
- const current = await readFile17(absolute, "utf8").catch(() => null);
8222
- const actualHash = current === null ? null : hashContent(current);
8223
- 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") {
8224
10092
  throw memgError(
8225
- "undo_conflict",
8226
- `${target.target} has changed since that operation`,
8227
- 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`."
8228
10096
  );
8229
10097
  }
8230
- const reverted = await reverse(brain, target, current);
8231
- return appendOp(brain.root, {
8232
- verb: "undo",
8233
- target: target.target,
8234
- memoryId: target.memory_id,
8235
- hashBefore: actualHash,
8236
- hashAfter: reverted === null ? null : hashContent(reverted),
8237
- rationale,
8238
- 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
+ });
8239
10126
  });
8240
- });
8241
- printDual({
8242
- json: {
8243
- undone: target.op_id,
8244
- verb: target.verb,
8245
- target: target.target,
8246
- undone_ts: target.ts,
8247
- undone_reason: target.rationale,
8248
- op_id: record.op_id
8249
- },
8250
- human: () => {
8251
- 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}\`
8252
10138
  `);
8253
- if (opId === void 0) {
8254
- printHuman(c.gray(`It was recorded ${target.ts}.`));
8255
- if (target.rationale === null) printHuman(c.gray("No reason was recorded for it."));
8256
- else printHuman(quoteBlock(target.rationale));
8257
- const minutes = minutesSince(target.ts);
8258
- if (minutes !== null && minutes >= STALE_UNDO_MINUTES) {
8259
- printHuman(
8260
- c.gray(
8261
- `
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
+ `
8262
10148
  that operation was ${minutes} minutes old \u2014 pass the op id to be sure it was the one you meant.`
8263
- )
8264
- );
10149
+ )
10150
+ );
10151
+ }
8265
10152
  }
8266
- }
8267
- printHuman(c.gray(`
10153
+ printHuman(c.gray(`
8268
10154
  Recorded as ${record.op_id} \u2014 the history keeps both.`));
8269
- }
8270
- });
8271
- });
10155
+ }
10156
+ });
10157
+ }
10158
+ );
8272
10159
  }
8273
10160
  var STALE_UNDO_MINUTES = 10;
8274
10161
  function minutesSince(iso) {
@@ -8277,7 +10164,7 @@ function minutesSince(iso) {
8277
10164
  return Math.max(0, Math.floor((Date.now() - at) / 6e4));
8278
10165
  }
8279
10166
  async function reverse(brain, op, current) {
8280
- const absolute = join19(brain.root, op.target);
10167
+ const absolute = join22(brain.root, op.target);
8281
10168
  if (op.verb === "remember") {
8282
10169
  if (current === null) {
8283
10170
  throw memgError(
@@ -8342,7 +10229,7 @@ async function conflictHint(vaultRoot, op) {
8342
10229
  Read what it says now, then decide what should stay.`;
8343
10230
  const snapshot = await readSnapshot(vaultRoot, op.op_id);
8344
10231
  return snapshot === null ? base : `${base}
8345
- 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`)}`;
8346
10233
  }
8347
10234
 
8348
10235
  // src/commands/use.ts
@@ -8352,8 +10239,8 @@ init_errors();
8352
10239
  init_config();
8353
10240
  init_vault();
8354
10241
  init_ui();
8355
- import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve9 } from "path";
8356
- 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";
8357
10244
  var VIA_MEANS = {
8358
10245
  flag: "because --vault named it",
8359
10246
  pointer: `because a \`${BRAND.pointerFileName}\` file here points at it`,
@@ -8362,11 +10249,11 @@ var VIA_MEANS = {
8362
10249
  default: "because it is your default brain (`memgineering use --default <path>` changes it)"
8363
10250
  };
8364
10251
  function useCommand() {
8365
- 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(
8366
10253
  "--default",
8367
10254
  "make this the brain used anywhere no pointer applies, instead of binding one folder"
8368
10255
  ).action(async (brainPath, opts) => {
8369
- const where = await canonicalize(resolve9(opts.here ?? process.cwd()));
10256
+ const where = await canonicalize(resolve10(opts.here ?? process.cwd()));
8370
10257
  if (!brainPath) {
8371
10258
  const pointer = await readPointer(where);
8372
10259
  const resolved = await resolveBrain(void 0, where).catch((err) => err);
@@ -8403,7 +10290,7 @@ ${hint}`));
8403
10290
  });
8404
10291
  return;
8405
10292
  }
8406
- const root = await canonicalize(resolve9(brainPath));
10293
+ const root = await canonicalize(resolve10(brainPath));
8407
10294
  const cfg = await loadConfig();
8408
10295
  if (!findBrain(cfg, root)) {
8409
10296
  throw memgError(
@@ -8430,16 +10317,16 @@ A pointer to an unlinked folder would fail on every command instead of at this o
8430
10317
  return;
8431
10318
  }
8432
10319
  const value = isInside(root, where) ? relative3(where, root) || "." : root;
8433
- const file = resolve9(where, BRAND.pointerFileName);
10320
+ const file = resolve10(where, BRAND.pointerFileName);
8434
10321
  await writeThenRename(file, `${value}
8435
10322
  `);
8436
10323
  printDual({
8437
- json: { pointer: file, brain: root, relative: !isAbsolute3(value) },
10324
+ json: { pointer: file, brain: root, relative: !isAbsolute4(value) },
8438
10325
  human: () => {
8439
10326
  printHuman(`This folder now uses \`${root}\`.
8440
10327
  `);
8441
10328
  printHuman(c.gray(` ${file}`));
8442
- if (!isAbsolute3(value)) {
10329
+ if (!isAbsolute4(value)) {
8443
10330
  printHuman(
8444
10331
  c.gray(
8445
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."
@@ -8451,11 +10338,56 @@ A pointer to an unlinked folder would fail on every command instead of at this o
8451
10338
  });
8452
10339
  }
8453
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
+
8454
10386
  // src/program.ts
8455
10387
  init_ui();
8456
10388
  init_brand();
8457
10389
  function buildProgram() {
8458
- 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) => {
8459
10391
  const opts = thisCommand.optsWithGlobals();
8460
10392
  if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
8461
10393
  });
@@ -8467,6 +10399,8 @@ function buildProgram() {
8467
10399
  program.addCommand(reviseCommand());
8468
10400
  program.addCommand(undoCommand());
8469
10401
  program.addCommand(resurfaceCommand());
10402
+ program.addCommand(pushCommand());
10403
+ program.addCommand(pullCommand());
8470
10404
  program.addCommand(onboardCommand());
8471
10405
  program.addCommand(initCommand());
8472
10406
  program.addCommand(linkCommand());
@@ -8480,6 +10414,9 @@ function buildProgram() {
8480
10414
  program.addCommand(excludeCommand());
8481
10415
  program.addCommand(unexcludeCommand());
8482
10416
  program.addCommand(setupCommand());
10417
+ program.addCommand(loginCommand());
10418
+ program.addCommand(logoutCommand());
10419
+ program.addCommand(whoamiCommand());
8483
10420
  program.addCommand(updateCommand());
8484
10421
  return program;
8485
10422
  }