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/CHANGELOG.md +230 -0
- package/README.md +31 -7
- package/assets/MEMGINEERING.md +33 -0
- package/assets/memgineering-memory/SKILL.md +26 -0
- package/assets/memgineering-setup/SKILL.md +14 -0
- package/bin/memgineering.js +0 -0
- package/dist/index.js +2718 -781
- package/package.json +2 -2
- package/scripts/postinstall.mjs +59 -10
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
|
|
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) &&
|
|
1172
|
-
if (inSet.has(rel.to) && !inSet.has(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
|
|
2855
|
-
import { join as
|
|
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
|
|
2900
|
-
for (const dir of FOLDERS) await
|
|
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
|
|
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
|
|
3050
|
-
import { join as
|
|
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
|
|
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/
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
}
|
|
3442
|
-
|
|
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>\
|
|
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
|
|
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\
|
|
3550
|
-
\uC774 \uC124\uC815\
|
|
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>\
|
|
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> \
|
|
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
|
|
3601
|
-
<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. \
|
|
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(
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
4159
|
-
resolveDone =
|
|
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((
|
|
4170
|
-
server.close(() =>
|
|
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 = {
|
|
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({
|
|
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:
|
|
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
|
|
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
|
-
|
|
4363
|
-
|
|
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(
|
|
4366
|
-
await consider(
|
|
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(
|
|
5416
|
+
if (/^brain/i.test(entry)) await consider(join17(home, entry));
|
|
4370
5417
|
}
|
|
4371
|
-
for (const name of ["notes", "Notes"]) await consider(
|
|
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
|
|
5427
|
+
async function countNotes(dir) {
|
|
5428
|
+
const isNote = (n) => /\.(md|markdown)$/i.test(n);
|
|
4381
5429
|
const entries = await entriesOf(dir);
|
|
4382
|
-
|
|
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 =
|
|
5433
|
+
const child = join17(dir, entry);
|
|
4385
5434
|
if (!await isDirectory(child)) continue;
|
|
4386
|
-
|
|
5435
|
+
total += (await entriesOf(child)).filter(isNote).length;
|
|
5436
|
+
if (total >= COUNT_CAP) return COUNT_CAP;
|
|
4387
5437
|
}
|
|
4388
|
-
return
|
|
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("..") && !
|
|
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 =
|
|
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
|
|
4641
|
-
return
|
|
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((
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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 =
|
|
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
|
|
5865
|
+
import { access, mkdir as mkdir12, readFile as readFile18 } from "fs/promises";
|
|
4798
5866
|
import { homedir as homedir4 } from "os";
|
|
4799
|
-
import { dirname as
|
|
5867
|
+
import { dirname as dirname4, join as join18 } from "path";
|
|
4800
5868
|
import { fileURLToPath } from "url";
|
|
4801
|
-
import { Command as
|
|
4802
|
-
import
|
|
5869
|
+
import { Command as Command18 } from "commander";
|
|
5870
|
+
import prompts3 from "prompts";
|
|
4803
5871
|
function setupCommand() {
|
|
4804
|
-
return new
|
|
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({
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6137
|
+
const existing = await readFile18(filePath, "utf8").catch(() => null);
|
|
5050
6138
|
if (existing === null) {
|
|
5051
6139
|
if (!dryRun) {
|
|
5052
|
-
await
|
|
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
|
|
6165
|
+
const existing = await readFile18(filePath, "utf8").catch(() => null);
|
|
5078
6166
|
if (existing === content) return "unchanged";
|
|
5079
6167
|
if (!dryRun) {
|
|
5080
|
-
await
|
|
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 =
|
|
5087
|
-
const raw = await
|
|
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
|
|
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
|
|
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 =
|
|
5126
|
-
const bundled =
|
|
5127
|
-
if (await
|
|
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 =
|
|
5131
|
-
if (await
|
|
5132
|
-
const parent =
|
|
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
|
|
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
|
|
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
|
-
|
|
6803
|
+
function planAddIgnoreEntry(content, relPath) {
|
|
5700
6804
|
const entry = normalizeName(relPath);
|
|
5701
|
-
if (excludesPath(
|
|
5702
|
-
return {
|
|
6805
|
+
if (excludesPath(content, relPath)) {
|
|
6806
|
+
return { content, changed: false };
|
|
5703
6807
|
}
|
|
5704
|
-
const needsNewline =
|
|
5705
|
-
const next = `${
|
|
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 `
|
|
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(
|
|
5719
|
-
return
|
|
6822
|
+
assertNothingLost(content, next, "excluding");
|
|
6823
|
+
return { content: next, changed: true };
|
|
5720
6824
|
}
|
|
5721
|
-
|
|
6825
|
+
function planRemoveIgnoreEntry(content, relPath) {
|
|
5722
6826
|
const entry = normalizeName(relPath);
|
|
5723
|
-
const lines2 =
|
|
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,
|
|
5735
|
-
return
|
|
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
|
|
6104
|
-
import { join as
|
|
6105
|
-
import { z as
|
|
6106
|
-
var EventSchema =
|
|
6107
|
-
ts:
|
|
6108
|
-
verb:
|
|
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:
|
|
7340
|
+
query: z10.string().nullable().default(null),
|
|
6111
7341
|
/** Where the caller was. The signal resurface ranks by. */
|
|
6112
|
-
context_dir:
|
|
6113
|
-
detail:
|
|
6114
|
-
limit:
|
|
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:
|
|
7346
|
+
returned_ids: z10.array(z10.string()).default([]),
|
|
6117
7347
|
/** The one that got opened, when this event is an open. */
|
|
6118
|
-
opened_id:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
6232
|
-
return body !== "" && body ===
|
|
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(
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
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
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
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
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
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
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
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
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
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
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
8373
|
+
import { Command as Command11 } from "commander";
|
|
6919
8374
|
init_vault();
|
|
6920
8375
|
init_ui();
|
|
6921
8376
|
function onboardCommand() {
|
|
6922
|
-
return new
|
|
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/
|
|
6985
|
-
|
|
6986
|
-
|
|
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
|
|
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
|
|
6995
|
-
|
|
6996
|
-
|
|
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
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
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:
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
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
|
-
|
|
7278
|
-
|
|
9064
|
+
"nothing to remember",
|
|
9065
|
+
'Pass the text: `memgineering remember "deploys are manual \u2014 launchctl by hand"`'
|
|
7279
9066
|
);
|
|
7280
9067
|
}
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
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
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
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
|
-
|
|
7297
|
-
|
|
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
|
|
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
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
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
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
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
|
-
|
|
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
|
|
7478
|
-
import { Command as
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
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((
|
|
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", () =>
|
|
9653
|
+
process.stdin.on("end", () => resolve11(buf));
|
|
7784
9654
|
process.stdin.on("error", reject);
|
|
7785
|
-
}) : await
|
|
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
|
|
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
|
|
7813
|
-
var __dir =
|
|
7814
|
-
var pkgPath =
|
|
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
|
|
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(
|
|
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 =
|
|
7897
|
-
const p =
|
|
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
|
|
7906
|
-
import { join as
|
|
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
|
|
9773
|
+
import { z as z11 } from "zod";
|
|
7910
9774
|
var run2 = promisify2(execFile2);
|
|
7911
|
-
var PolicySchema =
|
|
7912
|
-
latest:
|
|
7913
|
-
minimum_supported:
|
|
7914
|
-
notice:
|
|
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 =
|
|
7980
|
-
at:
|
|
7981
|
-
ok:
|
|
7982
|
-
from:
|
|
7983
|
-
to:
|
|
7984
|
-
message:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8162
|
-
import { join as
|
|
8163
|
-
import { Command as
|
|
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
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
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
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
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
|
-
"
|
|
8226
|
-
|
|
8227
|
-
|
|
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
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
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
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
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
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
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 =
|
|
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: ${
|
|
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
|
|
8356
|
-
import { Command as
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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: !
|
|
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 (!
|
|
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
|
|
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
|
}
|